ドキュメント

Unity Engine

Unity Engine

Unityプログラミング ベストプラクティス

Unityアプリケーションのコードを記述する際に注意すべきキー問題と、よくある落とし穴を回避するのに役立つベストプラクティス。
読み終わるまでの所要時間 22 分最終更新 3日前

Unity のプログラミング環境の一意の機能の中には、標準の C#/.NET プロジェクトと比較してコード作成時に余分な考慮が必要なものがあります。以下のサマリーは、Unityアプリケーションのコードを記述する際に注意すべきキー問題と、よくある落とし穴を回避するのに役立つベストプラクティスです。

Unity オブジェクトライフサイクルと参照

Unity で C# を記述する場合、他のオブジェクトや null と等しいオブジェクトを比較するときは注意してください。UnityEngine.オブジェクトから継承する型の場合、Unity は C# の等式および不等式演算子のカスタムバージョンを使用します。つまり、null チェック
myGameObject == null
は、技術的には有効な C# オブジェクトリファレンスを保持している場合でも、
myGameObject
が
true
を評価できます (また、逆に、
myGameObject != null
は
false
を評価できます)。この動作の詳細については、カスタム等値演算子 を参照してください。
Unity のカスタムの等値動作とオブジェクト ライフサイクルは、コードにいくつかの影響を与えます。
  • チェックで破棄されたオブジェクトを確実に除外したい場合は、Unity オブジェクトに
    if (obj == null)
    ではなく
    ReferenceEquals
    を使用してください。
  • 実際の C# null 参照を確認する場合は、最初に
    ReferenceEquals
    または
    System.Object
    にキャストします。
  • 2 つの Unity オブジェクトが等しいかどうかを比較する場合、例えば
    Undo
    によって、一方または両方が破棄されて再作成されると、両方の参照が異なる C# オブジェクトであっても
    obj1 == obj2
    が
    true
    を返す場合があります。
  • カスタムの等値演算子は、標準の C# 演算子よりも遅くなります。これは通常問題ではありませんが、スケールとホット パスでは注意が必要です。
  • 保護せずにシーンのアンロード間でコンポーネントをキャッシュしません。なぜなら、コンポーネントは破棄される可能性があるが、アンマネージの相手を持たない C# ラッパーオブジェクトとして残っているからです。
  • 静的フィールドで大きなアセットへの強い参照を保持しないでください。なぜなら、アセットはシーン全体に保持され、アンロードできなくなるからです。
  • オブジェクトを破棄する場合、
    Object.DestroyImmediate
    はエディターのみであるため、ランタイムに
    Object.Destroy
    を使用し、Unity に破棄のスケジュールを設定させます。

C# ファイナライザーを避ける

以下の理由により、ランタイムコードで C# ファイナライザーを使用しないでください。
  • これらは別のファイナライザースレッドで実行され、Unity API には通常メインスレッドが必要です。
  • これらは非決定論的に実行されるため、予測できない動作が発生します。
  • アプリケーションのガベージが収集されて待機しない限り、まったく実行されない可能性があります。
  • ファイナライザーからスローされた例外は、特殊な処理を行わない限り、アプリケーションの停止を引き起こす可能性があります。
  • その存在自体が、ガベージ コレクターのオーバーヘッドを増加させます。

ガベージ コレクターのオーバーヘッドと割り当て

Unityアプリケーションで最も重要なパフォーマンス リスクの1つは、メモリを割り当て、特にホット パスでガベージ コレクターのオーバーヘッドを増加させるランタイム コードです。
これを回避するには、以下のコーディング方法を適用します。
  • リストをキャッシュして再利用し、可能な場合は割り当てないバージョンのメソッドを使用して、フレームごとの割り当てを回避します。これには、
    WaitForSeconds
    のキャッシュや、コルーチン使用時のその他の yield 命令が含まれます。
  • 可能な場合は、割り当てないバージョンのメソッドを使用し、
    Update
    から繰り返し呼び出すのではなく、
    Awake
    内の
    GameObject.GetComponent
    や返されたオブジェクトへのキャッシュ参照などの負荷の高い操作を実行してください。
  • ランタイム コード、特にフレームごとの
    Update
    や
    FixedUpdate
    、その他のホット パスのコンテキストで LINQ を使用することは避けてください。
    System.Linq
    名前空間のメソッドは不必要な割り当てを作成し、ボックス化やクロージャを伴う場合があります。
  • 連結などの文字列操作の繰り返しを避けます。
  • リフレクションのインスタンスを特定して回避します。詳細は、「 C# リフレクション オーバーヘッドの回避 」を参照してください。
これらの問題の詳細なガイダンスと例については、 マネージメモリーのコード最適化 を参照してください。
ガベージコレクターのオーバーヘッドの追跡と削減については、マネージメモリ を参照してください。

MonoBehaviour 更新ループ最適化

多くの Unity プロジェクトでは、MonoBehaviour スクリプトコンポーネントを使用して、
MonoBehaviour.Update
、
MonoBehaviour.FixedUpdate
、
MonoBehaviour.LateUpdate
などのビルトインコールバックを使用してゲームステートを定期的に更新するのが従来のパターンです。これらのコールバックは通常、1 秒間に何度も実行されます。
これは、適切に使用すればまだうまく機能する単純なモデルですが、経験の浅い開発者が陥りやすいいくつかのキーなパフォーマンスリスクがあります。
  • Unity のフレーム単位やその他の通常のイベント機能のデフォルト実装は、スケールが不良な場合があります。これらの各
    Update
    機能には、Unityの内部管理とネイティブ レイヤーとのインタラクションによるわずかなオーバーヘッドが発生します。このような MonoBehaviour スクリプトが多数ある場合は、累積オーバーヘッドがパフォーマンスに大きな影響を与える可能性があります。
  • ビルトイン更新は頻繁に実行されるため、ホットコードパスとなり、そこに配置した非効率的でメモリー負荷の高い操作のエフェクトが大きくなります。経験の浅いユーザーが陥りやすい悪いパターンは、
    Update
    関数を持つ MonoBehaviour スクリプトが多くあり、そのスクリプトがほとんどの場合に不必要に実行されたり、実行時にメモリに不必要に負荷がかかることです。
これらのリスクを軽減するには、以下のオプションを検討してください。
  • 多くのエンティティでスケーラビリティを向上させるために、UnityのECS(エンティティ コンポーネント システム)を使用してプロジェクトをデータ指向アーキテクチャに変換することを検討してください。
  • MonoBehaviour ベースのアーキテクチャを使用する場合
    • ホットパスでのマネージメモリの影響を最小限に抑えるには、「 マネージメモリのコード最適化 」を参照してください。
    • 集中管理された更新マネージャを使用するかプレイヤーループをカスタマイズすることで、アクティブな
      Update
      機能の数を最小化。詳細については、「 カスタム更新マネージャーの使用 」および「 プレイヤーループのカスタマイズ 」を参照してください。
    • MonoBehaviour ベースのプロジェクトでも、Unity のデータ指向システムの特定の機能を使用することがよくあります。コードのジョブやBurstコンパイル セクションの使用、コードのパフォーマンス クリティカルなセクションでの
      NativeArray
      などのより効率的なデータ構造の使用、Transform操作などのアンマネージAPIの代替手段の選択が可能です。

スレッドセーフティ

Unity はマルチスレッド機能を備えていますが、コアランタイムはシングルスレッドであり、
UnityEngine
名前空間と
UnityEditor
名前空間のほとんどの API はメインスレッドからのみ呼び出すことができます。背景グラウンドスレッドからGameObjectsオブジェクト、Transform、コンポーネント、アセット API をリファレンスしないでください。メインスレッドで
Task.Result
や
Task.Wait
を
await
しないでください。デッドロックにつながるからです。
本質的に非同期で長時間実行される操作を処理する場合、Unityは.NET
Task
に代わるUnity固有のオプションとして
Awaitable
クラスを提供します。
Awaitable
はオブジェクトプールを使用して割り当てを削減し、
Update
や
FixedUpdate
などのUnity固有の概念を認識します。これにより、タスクを
await
し、プレイヤーループの特定の時点で再開するようにスケジュール設定することができます。詳細は、「 Awaitable クラスを使用した非同期プログラミング 」を参照してください。
生存期間が短いが演算負荷が高い並列作業のために、UnityはBurstコンパイル可能なジョブ システムを提供します。詳細については、「 ジョブシステムでのマルチスレッドコードの記述 」を参照してください。

コンパイルに関する考慮事項

最適なパフォーマンスを得るには、コードの記述方法だけでなく、コンパイル方法も考えることが重要です。コードの特定のソースファイルまたは地域がどのコンテキストに関連するかをアクティブに定義するのではなく、コードを単純コンパイルすると、以下のコストがかかります。
  • 不要なコードが含まれている場合はビルドサイズを増やす。
  • 変更を適用するためのコンパイルと再コンパイルに要した時間。これは特に、エディターでのイテレーション時間に影響します。
  • 特定のプラットフォームまたはコンテキストに不適切なコードが含まれている場合、ランタイム エラーが発生する可能性があります。
Unity には、コードのどの部分を異なるプラットフォームやコンテキスト用にコンパイルするかのコントロールに役立つメカニズムがいくつか用意されています。
  • アセンブリ定義を使用すると、ソースファイルを、個別にコンパイルできるアセンブリにグループ化できます。これにより、エディターのみのコードをランタイムコードから、プラットプラットフォーム固有のコードをクロスプラットフォームコードから分離できます。詳細については、スクリプトのアセンブリへの整理 を参照してください。
  • スクリプトシンボルとともに
    #if
    指示子を使用すると、ターゲットプラットフォームまたはコンテキストに基づいて、特定のコード地域をコンパイルから除外できます。例えば、
    #if UNITY_EDITOR
    ディレクティブの背後にあるエディターのみのコードをガードして、ランタイムビルドから除外します。Unity が条件付きでコードを含む/除外するために提供するさまざまな方法の詳細は、条件付きコンパイル を参照してください。
  • Unity 固有のキー概念は、再生モードの開始と終了、またはスクリプトの再コンパイル時のエディターでのドメインリロードです。ドメインの再ロードは時間がかかり、エディターでの記述やテストのイテレーション時間に影響する可能性があります。再生モードの開始時にドメインのリロードを使用不可にしてイテレーション時間を短縮できますが、静的ステートを手動でリセットする必要があります。詳細は、『 コードと再生モード開始時のシーンリロード 』を参照してください。

Unityの分析ツール

Unity は、ボトルネックを特定し、よりパフォーマンスの高いコードを書くのにヘルプつさまざまなツールを提供しています。Project Auditor ツールは、プロジェクトコードを分析して、一般的なパフォーマンスの問題を特定し、修正を提案できます。プロファイラーは、CPU と GPU の使用状況、メモリーアロケーションなどの詳細情報を提供することで、コードのランタイムパフォーマンスのボトルネックを特定するのに役立ちます。また、Roslyn アナライザーを作成して、コーディング標準を適用し、プロジェクトに固有のパフォーマンスの問題を特定することもできます。
カスタム Roslyn アナライザーとソースジェネレーターの作成の詳細は、Roslyn アナライザーとソースジェネレーター を参照してください。
Unityの分析ツール スイートの詳細については、最適化を参照してください。

追加リソース