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

Unity における .NET の概要

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

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

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

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

JIT ベースのスクリプティングバックエンドを使用する利点は、コンパイル時間が通常、AOT よりもはるかに短いことです。

デフォルトでは、Unity は Mono をサポートするプラットフォームでは、Mono バックエンドを使用します。アプリケーションのプレイヤーを構築するときに、使用するスクリプティングバックエンドを選択することができます。これをエディターで行うには、Edit > Project Settings > Player の順に移動し、Other Settings パネルを開き、 Scripting Backend ドロップダウンをクリックし、使用したいバックエンドを選択します。詳細については、スクリプティングバックエンド を参照してください。

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

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

Mono を使用する場合、コードストリッピングはデフォルトで無効になりますが、IL2CPP ではコードストリッピングを無効にすることはできません。Unity が削除するコードの量は、Managed Stripping Level プロパティを使用して制御できます。

このプロパティを変更するには、Edit > Project Settings > PlayerOther Settings の順にパネルを開き、 Managed Stripping Level ドロップダウンをクリックし、ストリッピングレベルを選択します。

Managed Stripping Level を上げると、Unity はより多くのコードを削除するようになります。これは、特にリフレクションを使用したり、ランタイムにコードを生成する場合、アプリケーションが依存しているコードが削除されてしまうリスクを増加させます。

コードの特定の要素にアノテーションを使用すると、Unity によるコードのストリッピングを防ぐことができます。詳細については、マネージコードストリッピング を参照してください。

ガベージコレクター

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

インクリメンタルモードと “stop the world” を切り替えるには、Edit > Project Settings > Player の順に移動し、Other Settings パネルを開き、Use incremental GC チェックボックスをクリックします。インクリメンタルモードでは、Unity のガベージコレクターは限られた時間だけ実行され、必ずしも 1 回のパスですべてのオブジェクトを収集するわけではありません。これにより、オブジェクトの収集にかかる時間が複数のフレームに分散され、スタッターと CPU スパイクが減少します。詳しくは、マネージメモリ を参照してください。

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

.NET システムライブラリ

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

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

Unity は System.Drawing ライブラリをサポートしておらず、すべてのプラットフォームで動作することが保証されているわけではありません。

Mono スクリプトバックエンドが使用する JIT コンパイルでは、アプリケーションのランタイムに動的な C#/.NET 中間言語 (IL) コード生成を行うことができます。IL2CPP スクリプティングバックエンドが使用する AOT コンパイルは、動的なコード生成をサポートしません。

サードパーティのライブラリを使用する場合、この点を考慮することが重要です。なぜなら、JIT と AOT でコードパスが異なったり、動的に生成されたコードに依存するコードパスを使用する場合があるためです。ランタイムにコードを生成する方法の詳細については、Microsoft 社のModuleBuilder のドキュメントを参照してください。

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

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

他のプロファイルは、例えば、古い既存のアプリケーションのサポートを提供する必要がある場合に便利です。Api Compatibility Level の設定を変更するには、Edit > Project Settings > Player に移動します。Other Settings のセクションで、Api Compatibility Level を希望の設定にします。

詳しくは、.NET プロファイルのサポート を参照してください。

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

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

サードパーティ製ライブラリの JIT コードパスと AOT コードパスの性能特性は大きく異なる場合があります。AOT は一般的に起動時間を短縮することができ、大きなアプリケーションに適していますが、コンパイルされたコードを格納するためにバイナリファイルのサイズが大きくなります。また、AOT は開発中のビルドに時間がかかります。

JIT は、実行中のプラットフォームに基づいてランタイムに調整するため、アプリケーションの起動時間が長くなる可能性がありますが、実行パフォーマンスを向上させることができます。そのため、エディターとターゲットプラットフォームの両方でアプリケーションのプロファイルを作成する必要があります。詳細については、プロファイラー概要 のドキュメントを参照してください。

すべてのターゲットプラットフォームの .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.Object のインスタンスでの 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 はスレッドセーフではないため、UnitySynchronizationContext 内からの async タスクと await タスクのみを使用する必要があります。async タスクは呼び出し時にオブジェクトを割り当てることが多いため、使いすぎるとパフォーマンスの問題が発生する可能性があります。

Unity は、デフォルトのSynchronizationContext をカスタムのUnitySynchronizationContext で上書きし、デフォルトでは EditPlay の両方のモードでメインスレッド上のすべてのタ スクを実行するようになっています。async タスクを使用するには、Task.Run API を使用して独自のスレッドを手動で作成および処理し、Unity バージョンではなく、デフォルトの SynchronizationContext を使用する必要があります。

Unity は、再生モードを終了するときに、マネージスレッドで実行される async タスクを自動的に停止しません。再生モードの開始および終了イベントをリッスンしてタスクを手動で停止するには、EditorApplication.playModeStateChanged を使用します。 このアプローチを採用する場合、コンテキストを UnitySynchronizationContext に移行しない限り、Unity スクリプト API のほとんどは使用できなくなります。

開発ビルドにおいて、マルチスレッドコードで Unity の API を使用しようとすると、以下のようなエラーメッセージが表示されます。


UnityException: Internal_CreateGameObject can only be called from the main thread. \

Constructors and field initializers will be executed from the loading thread when loading a scene. \

Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.

パフォーマンス上の理由から、Unity は非開発ビルドではマルチスレッドの動作チェックを行わず、ライブビルドではこのエラーを表示しません。つまり、Unity はライブビルドでのマルチスレッドコードの実行を防止しませんが、マルチスレッドを使用する場合、ランダムなクラッシュやその他の予測不可能なエラーが発生する可能性があります。

このため、独自のマルチスレッドを使用せず、代わりに Unity の ジョブシステム を使用する必要があります。ジョブシステムは複数のスレッドを安全に使用してジョブを並行して実行し、マルチスレッドのパフォーマンス上の利点を実現します。詳細については、ジョブシステムの概要 を参照してください。

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