Version: 2020.1
言語: 日本語
Unity のアーキテクチャ
.NET プロファイルのサポート

Unity における .NET の概要

Unity は、Unity で作成したアプリケーションがさまざまなハードウェア設定で動作するように、オープンソースの .NET プラットフォームを使用しています。.NET は様々な言語や API ライブラリをサポートします。

スクリプティングバックエンド

Unity には、Mono と IL2CPP (C++ への中間言語) という 2 つのスクリプティングバックエンドがあり、それぞれが異なるコンパイル手法を採用しています。

  • Mono はジャストインタイム (JIT) コンパイルを採用しており、実行時に必要に応じてコードをコンパイルします。
  • IL2CPP は AOT (Ahead-of-Time) コンパイルを採用しており、アプリケーション全体を実行前にコンパイルします。

JIT ベースのスクリプティングバックエンドを使用する利点は、コンパイル時間が AOT よりもはるかに短く、プラットフォームに依存しないことです。

Unity エディターは JIT ベースで、スクリプティングバックエンドとして Mono を使用しています。アプリケーション用のプレイヤーをビルドする際に、使用するスクリプティングバックエンドを選択できます。エディターでこれを行うには、Edit > Project Settings > Player と移動し、Other Settings パネルを開き、Scripting Backend ドロップダウンをクリックして使用するバックエンドを選択します。

マネージコードストリッピング

アプリケーションをビルドする際、Unity はコンパイルされたアセンブリ (.DLL) をスキャンして、使用されていないコードを検出して削除します。このプロセスにより、ビルドの最終的なバイナリサイズは小さくなりますが、ビルド時間は長くなります。

Mono を使用する場合、コードストリッピングはデフォルトで無効になっていますが、IL2CPP ではコードストリッピングを無効にすることはできません。コードストリッピングの強度をコントロールすることができます。Edit > Project Settings > Player に行き、Other Settings パネルを開き、 Managed Stripping Level ドロップダウンをクリックして、必要なコードストリッピングのレベルを選択します。コードストリッピングの詳細については、マネージコードストリッピング のドキュメントを参照してください。

ノート: コードストリッピングの適用が強すぎると、特にリフレクションを使用している場合など、依存しているコードが削除されてしまうことがあります。Preserve 属性や link.xml ファイルを使って、特定の型や関数が除去されないようにすることができます。

ガベージコレクター

Unity は Boehm ガベージコレクターを使用しています。 を Mono と IL2CPP の両方のバックエンドに使用しています。Unity はデフォルトで インクリメンタル モードを使用します。Unity では インクリメンタル モードの使用を推奨していますが、インクリメンタル モードを無効にして “stop the world” ガベージコレクションを使用することもできます。

To toggle between Incremental mode and “stop the world”, go to Edit > Project Settings > Player, open the Other Settings panel and click the Use incremental GC checkbox. In Incremental mode, Unity’s garbage collector only runs for a limited period of time and does not necessarily collect all objects in one pass. This spreads the time it takes to collect objects over a number of frames and reduces the amount of stuttering and CPU spikes. For more information, see Understanding Automatic Memory Management.

アプリケーションで、割り当て数や CPU スパイクの可能性を確認するには、Unity Profiler を使用してください。また、GarbageCollector API を使用して、プレイヤーのガベージコレクションを完全に無効にすることもできます。コレクターが無効になっているときは、過剰なメモリを割り当てないように注意する必要があります。

.NET システムライブラリ

Unity は多くのプラットフォームをサポートしており、プラットフォームによって異なるスクリプティングバックエンドを使用する場合があります。.NET システムライブラリが正しく動作するためには、プラットフォーム固有の実装が必要な場合があります。Unity は可能な限り多くの .NET エコシステムをサポートするように努めていますが、.NET システムライブラリの一部には、Unity が明示的にサポートしていない例外があります。

Unity のバージョン間での .NET システムライブラリのパフォーマンスや割り当ては保証されていません。一般的な規則として、Unity は .NET システムライブラリのパフォーマンスリグレッションを修正しません。

Unity は System.Drawing ライブラリをサポートしておらず、すべてのプラットフォームでの使えることを保証するものではありません。

JIT スクリプトバックエンドでは、アプリケーションのランタイム中に動的な C#/.NET 中間言語 (IL) のコード生成を行うことができますが、AOT スクリプトバックエンドでは動的なコード生成をサポートしていません。サードパーティのライブラリを使用する際には、この点を考慮する必要があります。理由は、JIT と AOT でコードパスが異なる場合や、動的に生成されるコードに依存するコードパスを使う場合があるためです。実行時にコードを生成する方法の詳細については、Microsoft の ModuleBuilder のドキュメントを参照してください。

Unity は複数の .NET API プロファイルをサポートしていますが、以下の理由から、すべての新規プロジェクトには、.NET Standard 2.0 API Compatibility Level を使用する必要があります。

  • .NET Standard 2.0 は、API サーフェスが小さいため、実装も小さくなります。これにより、最終的な実行ファイルのサイズが小さくなります。
  • .NET Standard 2.0では、クロスプラットフォームのサポートが強化されています。そのため、作成したコードがすべてのプラットフォームで動作する可能性が高くなります。
  • .NET Standard 2.0 は、すべての .NET ランタイムでサポートされています。そのため、コードはより多くの VM/ランタイム環境 (.NET Framework、.NET Core、Xamarin、Unity など) で動作します。
  • .NET Standard では、より多くのエラーをコンパイル時に処理します。.NET 4.7.1 の多くの API は、コンパイル時に利用可能ですが、一部のプラットフォームでは実行時に例外をスローする実装があります。

他のプロファイルは、例えば、古い既存のアプリケーションのサポートを提供する必要がある場合などに便利です。別の API 互換性レベルが必要な場合は、Player Settings の .NET Profile を変更します。これを行うには、Edit > Project Settings > Player > Other Settings の順に移動し、Api Compatibility Level のドロップダウンから必要なレベルを選択します。

サードパーティの .NET ライブラリの使用

サードパーティ製の .NET ライブラリは、さまざまな Unity の設定やプラットフォームで広範にテストされたものだけを使用するようにしてください。

ノート: サードパーティ製ライブラリの JIT コードパスと AOT コードパスの性能特性は大きく異なる場合があります。AOT は一般的に起動時間を短縮することができ、大きなアプリケーションに適していますが、コンパイルされたコードを格納するためにバイナリファイルのサイズが大きくなります。また、AOT は開発中のビルドに時間がかかり、コンパイルされたコードの動作を変更して、特定のプラットフォームをターゲットにすることはできません。JIT は、実行中のプラットフォームに基づいてランタイムに調整するため、アプリケーションの起動時間が長くなる可能性がありますが、実行パフォーマンスを向上させることができます。そのため、エディターとターゲットプラットフォームの両方でアプリケーションのプロファイルを作成する必要があります。詳細については、Unity プロファイラー のドキュメントを参照してください。

すべてのターゲットプラットフォームの .NET システムライブラリの使用状況をプロファイリングする必要があります。なぜなら、使用するスクリプトバックエンド、.NET バージョン、プロファイルによってパフォーマンスの特性が異なる可能性があるためです。

サードパーティ製のライブラリを検討する際には、以下の点を考慮してください。

  • 互換性。サードパーティのライブラリは、一部の Unity プラットフォームやスクリプトバックエンドと互換性がない場合があります。
  • パフォーマンス。サードパーティのライブラリは、他の .NET ランタイムと比較して、Unity でのパフォーマンス特性が大きく異なる場合があります。
  • AOT のバイナリサイズ。サードパーティのライブラリは、ライブラリが使用する依存関係の数によって、AOT のバイナリサイズが大幅に増加する場合があります。

C#のリフレクションのオーバーヘッド

Mono および IL2CPP は、すべての C# リフレクション (System.Reflection) オブジェクトを内部的にキャッシュしますが、設計上、Unity はそれらをガベージコレクションしません。この動作の結果、ガベージコレクターはアプリケーションの生存期間中にキャッシュされた C# リフレクションオブジェクトを継続的にスキャンすることになり、ガベージコレクターの不必要で潜在的に大きなオーバーヘッドの原因になります。

ガベージコレクターのオーバーヘッドを最小限にするために、アプリケーションで Assembly.GetTypesType.GetMethods() などのメソッドを避けてください。これらのメソッドは、ランタイムに多くの C# リフレクションオブジェクトを作成します。代わりに、必要なデータのためにエディターでアセンブリをスキャンし、ランタイムに使用するためにそれをシリアライズおよび/またはコード化する必要があります。

UnityEngine.Object の特殊な動作

UnityEngine.Object は、ネイティブ C++ 対応オブジェクトにリンクされているため、Unity の特殊なタイプの C# オブジェクトです。例えば、Camera コンポーネントを使用する場合、Unity はオブジェクトの状態を C# オブジェクトに保存するのではなく、ネイティブ C++ 対応オブジェクトに保存します。

Unity は現在、UnityEngine.Objects を伴う C# の WeakReference クラスの使用をサポートしていません。このため、ロードされたアセットを参照するために WeakReference を使用するべきではありません。WeakReference クラスの詳細については、Microsoft の WeakReference のドキュメント を参照してください。

Unity C# と Unity C++ は UnityEngine オブジェクトを共有

Object.DestroyObject.DestroyImmediate などのメソッドを使ってUnityEngine.Object 派生オブジェクトを破壊すると、Unity はネイティブの対応するオブジェクトを破棄 (アンロード) します。ガベージコレクターがメモリを管理しているため、明示的な呼び出しで C# オブジェクトを破棄することはできません。マネージオブジェクトへの参照がなくなると、ガベージコレクターがオブジェクトを回収して破棄します。

破棄された UnityEngine.Object が再びアクセスされた場合、Unity はほとんどの型でネイティブの相対するオブジェクトを再作成します。この再現動作に関する 2 つの例外は、MonoBehaviourScriptableObject です。これらは一度破棄されると、決して再ロードできません。

MonoBehaviour と ScriptableObject は、等式 (==) と不等式 (!=) の演算子をオーバーライドします。つまり、破棄された MonoBehaviour や ScriptableObject と null を比較すると、マネージオブジェクトがまだ存在し、ガベージコレクションされていなければ、演算子は true を返します。

その理由は、???. の演算子はオーバーロードできないため、UnityEngine.Object から派生したオブジェクトとの互換性はありません。マネージオブジェクトがまだ存在するときに、破棄された MonoBehaviour や ScriptableObject に対してこれらの演算子を使用しても、等式演算子や不等式演算子と同じ結果は得られません。

async と await の使用を避ける

Unity の API はスレッドセーフではないため、async タスクや await タスクを使用してはいけません。非同期タスクは起動時にオブジェクトを割り当てることが多いので、使いすぎるとパフォーマンスに問題が生じる可能性があります。また、再生モードを終了しても、マネージスレッド上で実行中の非同期タスクは自動的には停止しません。

Unity は、デフォルトの SynchronizationContext をカスタムの UnitySynchronizationContext で上書きし、EditPlay の両方のモードで、メインスレッド上のすべてのタスクを実行します。非同期タスクを利用するには、TaskFactory で独自のスレッドを手動で作成して処理する必要があります。また、Unity バージョンではなくデフォルトの SynchronizationContext を使用する必要があります。プレイモードの開始と終了のイベントをリッスンしてタスクを手動で停止するためには、EditorApplication.playModeStateChanged を使用します。ただし、この方法を取ると、UnitySynchronizationContext を使用していないため、ほとんどの Unity スクリプティング API を使用できません。

Unity のアーキテクチャ
.NET プロファイルのサポート