Hero background image
高度なプログラミングとコード・アーキテクチャ
グラフィックスのレンダリングをさらに最適化するために、コード・アーキテクチャを検討しましょう。今回は、Unityプロジェクトの最適化のヒントを紹介する連載の第4回目です。より少ないリソースでより高いフレームレートで動作させるためのガイドとしてご利用ください。これらのベスト・プラクティスを試したら、シリーズの他のページもぜひご覧ください:パフォーマンス向上のためのUnityプロジェクトの設定 ハイエンドグラフィックスのパフォーマンス最適化 PCおよびコンソールゲームのGPU使用量の管理 スムーズなゲームプレイのための物理演算パフォーマンスの向上
UnityのPlayerLoopを理解する

UnityPlayerLoopには、ゲームエンジンのコアと対話するための関数が含まれています。この構造には、初期化とフレームごとの更新を処理するいくつかのシステムが含まれている。すべてのスクリプトは、このPlayerLoopに依存してゲームプレイを作成します。プロファイリングを行うと、プロジェクトのユーザーコードがPlayerLoopの下に表示され、EditorコンポーネントはEditorLoopの下に表示されます。

UnityのFrameLoopの 実行順序を理解することは重要です。すべてのUnityスクリプトは、いくつかのイベント関数を決められた順序で実行します。AwakeStartUpdateなど、スクリプトのライフサイクルを作成する関数の違いを学び、パフォーマンスを強化する。

例えば、Rigidbodyを扱うときにUpdateではなくFixedUpdateを使ったり、ゲーム開始前に変数やゲームステートを初期化するときにStartではなくAwakeを使ったりします。これらを使って、各フレームで実行されるコードを最小限に抑える。Awakeはスクリプトのインスタンスが生きている間に一度だけ呼び出され、常にStart関数の前に呼び出される。つまり、他のオブジェクトと会話できることがわかっているオブジェクトを扱うときや、初期化されたオブジェクトに問い合わせるときは、Startを使うべきだということだ。

イベント関数の具体的な実行順序については、スクリプトのライフサイクルのフローチャートを参照してください。

カスタム Update マネージャーの図
カスタム・アップデート・マネージャーの構築

プロジェクトに厳しいパフォーマンス要件がある場合(オープンワールドゲームなど)、UpdateLateUpdateFixedUpdateを使用してカスタムUpdate Managerを作成することを検討してください。

UpdateまたはLateUpdateの一般的な使用パターンは、何らかの条件が満たされたときにのみロジックを実行することです。このため、この条件をチェックする以外、事実上何のコードも実行しないフレームごとのコールバックが多数発生する可能性がある。

UnityがUpdateやLateUpdateのようなメッセージメソッドを呼び出すときはいつも、インターオプコール、つまりC/C++側からマネージドC#側への呼び出しを行います。少数のオブジェクトについては、これは問題ではない。何千ものオブジェクトがあると、このオーバーヘッドが大きくなってくる。

アクティブなオブジェクトがコールバックを必要とするときはこのUpdate Managerにサブスクライブし、そうでないときはアンサブスクライブする。このパターンによって、Monobehaviourオブジェクトへのインターオプ呼び出しの多くを減らすことができる。

実装例については、ゲームエンジン固有の最適化テクニックを参照してください。

毎フレーム実行されるコードの最小化

コードが毎フレーム実行されなければならないかどうかを検討する。Update、LateUpdate、FixedUpdateから不要なロジックを取り除くことができる。これらのUnityイベント関数は、毎フレーム更新しなければならないコードを置くのに便利な場所ですが、その頻度で更新する必要のないロジックを抽出することができます。

状況が変わったときだけロジックを実行する。特定の関数シグネチャをトリガーするために、イベントの形でオブザーバーパターンなどのテクニックを活用することを忘れないでください。

アップデートが必要な場合は、nフレームごとにコードを実行することになる。これは、重いワークロードを複数のフレームに分散させる一般的な手法であるタイムスライシングを適用する方法のひとつである。

この例では、ExampleExpensiveFunctionを3フレームに1回実行する。

コツは、他のフレームで動く他の仕事と織り交ぜることだ。この例では、Time.frameCount % interval == 1またはTime.frameCount % interval == 2のときに、他の高価な関数を「スケジュール」することができます。

あるいは、カスタム・アップデート・マネージャ・クラスを使って、サブスクライブされたオブジェクトをnフレームごとに更新する。

高価な関数の結果をキャッシュする

2020.2より前のバージョンのUnityでは、GameObject.FindGameObject.GetComponentCamera.mainが高価な場合があるので、Updateメソッドで呼び出すのは避けた方がよいでしょう。

さらに、OnEnableと OnDisableに高価なメソッドを置くのは、頻繁に呼び出されるのであれば避けるようにする。これらのメソッドを頻繁に呼び出すと、CPUスパイクの原因となる。

可能な限り、次のような高価な関数を実行します。 のような高価な関数を実行する。MonoBehaviour.Startなどの高価な関数を初期化フェーズで実行します。必要な参照をキャッシュし、後で再利用する。スクリプトの実行順序の詳細については、以前のUnity PlayerLoopのセクションをチェックしてください。

以下は、繰り返されるGetComponentコールの非効率的な使用を示す例である:

void Update()
{
Renderer myRenderer = GetComponent<Renderer>();
ExampleFunction(myRenderer);
}

代わりに、GetComponentを一度だけ呼び出す。キャッシュされた結果は、GetComponentをさらに呼び出すことなく、Updateで再利用できる。

イベント機能の実行順序については、こちらをご覧ください。

空のUnityイベントとデバッグ・ログ文を避ける

ログステートメント(特にUpdate、LateUpdate、FixedUpdate)はパフォーマンスを低下させる可能性があるため、ビルドを行う前にログステートメントを無効にしてください。これを素早く行うには 条件属性を作ることを検討してください。

例えば、以下のようなカスタム・クラスを作成したいとします。

カスタムクラスでログメッセージを生成します。 プレイヤー設定>スクリプトのシンボル定義で ENABLE_LOGプリプロセッサを無効にすると、すべてのログステートメントが一挙に消えます。

文字列やテキストを扱うことは、Unityプロジェクトにおいてパフォーマンス問題の一般的な原因です。だからこそ、ログ・ステートメントとその高価な文字列書式を削除することで、パフォーマンスが大きく向上する可能性があるのだ。

同様に、空のMonoBehavioursはリソースを必要とするので、空白のUpdateメソッドやLateUpdateメソッドを削除する必要があります。テストにこれらの方法を使う場合は、プリプロセッサ・ディレクティブを使う:

#if UNITY_EDITOR
void Update()
{
}
#endif

ここでは、不要なオーバーヘッドがビルドに入り込むことなく、Update in Editorをテストに使うことができる。

10,000回のUpdateコールに関するこのブログ記事では、UnityがどのようにMonobehaviour.Updateを実行するかについて説明しています。

スタックトレースのロギングを無効にする

プレーヤー設定の スタックトレースオプションを使用して、表示されるログメッセージのタイプを制御します。アプリケーションがリリースビルドでエラーや警告メッセージを記録している場合(例えば、クラッシュレポートを生成するため)、パフォーマンスを向上させるためにスタックトレースを無効にしてください。

スタックトレースのロギングについてはこちらをご覧ください。

文字列パラメータの代わりにハッシュ値を使用する

Unityでは、AnimatorMaterialShaderのプロパティのアドレスに文字列名を使用することはありません。スピードアップのために、すべてのプロパティ名はプロパティIDにハッシュ化され、これらのIDはプロパティをアドレスするために使用されます。

アニメーター、マテリアル、シェーダーでSetメソッドやGetメソッドを使用する場合は、文字列値のメソッドではなく、整数値のメソッドを活用してください。文字列値メソッドは文字列ハッシュを実行し、ハッシュされたIDを整数値メソッドに転送する。

使用 Animator.StringToHashを使用します。を使います。 Shader.PropertyToIDを使用します。を使用します。

関連するのはデータ構造の選択で、これはフレームごとに何千回も反復するため、パフォーマンスに影響する。適切な構造を選択するための一般的なガイドとして、C#のデータ構造に関するMSDNガイドに従ってください。

オブジェクトプールのスクリプトインターフェース
オブジェクトをプールする

Instantiateと Destroyはガベージコレクション(GC)のスパイクを発生させる可能性がある。これは一般的に時間のかかる処理なので、GameObjectを定期的にインスタンス化して破棄する(銃で弾を撃つなど)のではなく、再利用やリサイクルが可能な事前割り当てオブジェクトのプールを使用します。

再利用可能なインスタンスは、メニュー画面やローディング画面など、CPUスパイクが目立ちにくいゲーム中のタイミングで作成する。このオブジェクトの「プール」をコレクションで追跡する。ゲームプレイ中は、必要なときに次の利用可能なインスタンスを有効にし、オブジェクトを破壊する代わりに無効にしてからプールに戻すだけでよい。これにより、プロジェクトで管理される割り当ての数が減り、GCの問題を防ぐことができる。

同様に、実行時にコンポーネントを追加するのは避けよう。AddComponentを呼び出すには、それなりのコストがかかる。Unityは、実行時にコンポーネントを追加するたびに、重複するコンポーネントやその他の必要なコンポーネントをチェックする必要があります。必要なコンポーネントがすでにセットアップされたPrefabをインスタンス化する方がパフォーマンスが高いので、Object Poolと組み合わせて使ってください。

関連して、トランスフォームを移動させるときは Transform.SetPositionAndRotationを使用して、位置と回転の両方を一度に更新します。これにより、トランスフォームを2度修正するオーバーヘッドを避けることができる。

実行時にGameObjectをインスタンス化し、最適化のために親オブジェクトにして再配置する必要がある場合は、以下を参照してください。

Object.Instantiateの詳細については、スクリプティングAPIを参照してください。

Unityでシンプルなオブジェクトプーリングシステムを作成する方法はこちらをご覧ください。

スクリプタブルオブジェクトプール
ScriptableObjectsのパワーを利用する

MonoBehaviourの代わりにScriptableObjectに不変の値や設定を保存します。ScriptableObjectはプロジェクト内に存在するアセットです。一度だけ設定する必要があり、GameObjectに直接アタッチすることはできない。

ScriptableObjectにフィールドを作成して値や設定を保存し、MonoBehavioursでScriptableObjectを参照します。ScriptableObjectのフィールドを使用することで、そのMonoBehaviourを持つオブジェクトをインスタンス化するたびに不要なデータが重複するのを防ぐことができます。

ScriptableObjects入門の チュートリアルをご覧ください。

ユニティ・キー・アート21 11
無料電子書籍を入手する

これまでで最も包括的なガイドの1つで、PCとコンソール向けにゲームを最適化する方法について、80以上の実用的なヒントを集めている。サクセスとアクセラレート・ソリューションズの専門エンジニアが作成したこれらの詳細なヒントは、Unityを最大限に活用し、ゲームのパフォーマンスを向上させるのに役立ちます。

このコンテンツにご満足いただけましたか?