モバイルゲームのパフォーマンスを最適化する:Unity のトップエンジニアからのプロファイリング、メモリ、コードアーキテクチャに関するヒント

THOMAS KROGH-JACOBSEN / UNITY TECHNOLOGIESSenior Technical Content Marketing Manager
Jun 23, 2021|15 分
モバイルゲームのパフォーマンスを最適化する:Unity のトップエンジニアからのプロファイリング、メモリ、コードアーキテクチャに関するヒント
このウェブページは、お客様の便宜のために機械翻訳されたものです。翻訳されたコンテンツの正確性や信頼性は保証いたしかねます。翻訳されたコンテンツの正確性について疑問をお持ちの場合は、ウェブページの公式な英語版をご覧ください。
当社の Unity Studio Production チームはソースコードを熟知しており、無数に存在する Unity の顧客みんながエンジンを最大限に活用できるようサポートしています。このチームは、クリエイターのプロジェクトに深く入り込み、パフォーマンスを最適化することで、スピード、安定性、効率性を向上させるポイントを見つけ出します。Unity の上級エンジニアで構成されたこのチームに、モバイルゲームの最適化に関するノウハウを教えてもらいました。

Unity のエンジニアたちがモバイルゲームの最適化に関する知見のシェアを始めるとすぐに、元々予定していた 1 本のブログ記事に収まりきらないほど素晴らしい情報があることに気づきました。そこで私たちは、彼らの膨大な知識を e ブックとしてまとめ(ダウンロードはこちら)、そこに収録されている 75 以上の実用的なヒントの一部を紹介する一連のブログ記事を作成することにしました。

このシリーズの最初の記事では、プロファイリング、メモリ、コードアーキテクチャの観点から、ゲームのパフォーマンスを向上させる方法をご紹介します。今後 2、3 週間の間に、さらに 2 本の記事を公開します。1 本目の記事では UI の物理を、2 本目ではオーディオとアセット、プロジェクトの構成、グラフィックスについて説明します。

今すぐ全シリーズをチェックしたいですか?e ブックの全文を無料ダウンロード

それでは、今回の内容に入っていきましょう。

プロファイリング

まず何よりも、プロファイリングや、モバイルゲームのパフォーマンスデータの収集・活用を行うプロセスからスタートさせるべきでしょう。モバイルゲームのパフォーマンス最適化は、まさにここから始まるからです。

早期に、頻繁に、そしてターゲットデバイス上でプロファイリングを行う

Unity プロファイラーは、アプリケーションに関する重要なパフォーマンス情報を提供してくれますが、当然ながら使わないことには役に立ちません。プロジェクトのプロファイリングは、ゲームをまさに出荷しようというタイミングではなく、開発の初期段階から着手するべきです。不具合やスパイクが発生したら、すぐに調査すべきです。プロジェクトの「パフォーマンスシグネチャー」を作成することで、新たな問題をより簡単に発見することができるようになります。

エディターでプロファイリングするだけでも、ゲームをさまざまなシステムで走らせた時の相対的なパフォーマンスが大まかに分かりますが、個別のデバイスでプロファイリングすることでより正確な知見を得ることができます。できる限りターゲットデバイス上で開発ビルドを使ってプロファイリングをするべきでしょう。サポートする予定のデバイスのうち、最高スペックのものと最低スペックのものの両方に対してプロファイリングと最適化を行うようにしてください。

Unity プロファイラーに加えて、iOS や Android のネイティブツールを活用して、各プラットフォームのエンジン上でさらにパフォーマンステストを行うことができます。

ハードウェアによっては、追加のプロファイリングツールを利用することができます(例えば、Arm Mobile StudioIntel VTuneSnapdragon Profiler など)。詳細は、 Unity で作られたアプリケーションのプロファイリング をご覧ください。

適切な領域の最適化に注力する

ゲームのパフォーマンスを低下させている原因を当て推量や決めつけで導き出してはいけません。Unity プロファイラーやプラットフォームの専用ツールを使って、遅くなっている原因を正確に突き止めましょう。

もちろん、ここで説明した最適化手法のすべてが、皆さんご自身のアプリケーションに当てはまるわけではありません。あるプロジェクトではうまくいったことでも、ご自身のプロジェクトではうまくいかないかもしれません。真のボトルネックを見極め、自分の仕事に役立つことに力を注ぐようにしましょう。

Unity プロファイラーの仕組みを理解する

Unity プロファイラーは、実行時のラグやフリーズの原因を検出し、特定のフレームや時刻で何が起こっているかをよりよく理解するのに役立ちます。デフォルトでは、CPU と Memory のトラックが有効になっています。ゲームに応じて、Renderer、Audio、Physics などの補助的なプロファイラーモジュールを監視することができます(物理演算が多いゲームや、音楽ベースのゲームなどでの使用を想定)。

Unity プロファイラーを使用して、アプリケーションのパフォーマンスとリソースの割り当てをテストします。
Unity プロファイラーを使用して、アプリケーションのパフォーマンスとリソースの割り当てをテストします。

Development BuildAutoconnect Profiler をチェックして、デバイスにアプリケーションをビルドします。手動で接続してアプリの起動時間を短縮することもできます。

エディターでのビルド設定

プロファイルするプラットフォームターゲットを選択します。Record ボタンは、アプリケーションの再生中の数秒間(デフォルトでは 300 フレーム)をトラッキングします。より長くキャプチャーする必要がある場合は Unity > Preferences > Analysis > Profiler > Frame Count から、フレーム数を 2000 まで増やすことができます。キャプチャーするフレーム数を増やすと、Unity エディターがより多くの CPU 処理とメモリを占有するようになりますが、これが役に立つシナリオもあります。

これは、ProfileMarkers で明示的にラップされたコードのタイミング(MonoBehaviour の Start メソッドや Update メソッド、特定の API コールなど)のプロファイリングを行う、インストルメンテーションベースのプロファイラーです。また、Deep Profiling 設定を使用すると、Unity はスクリプトコード内のすべての関数呼び出しの最初と最後をプロファイリングすることができ、アプリケーションのどの部分が速度低下の原因になっているかを正確に知ることができます。

エディター内の Timeline ビュー
Timeline ビューを使って、CPU バウンドか GPU バウンドかを判断する。

ゲームをプロファイリングする際には、スパイクと平均的なフレームのコストの両方をカバーすることをお勧めします。各フレームで発生する高価な処理の把握とその最適化は、目標フレームレートが実現できていないアプリケーションの動作を改善する上で有用です。スパイクを探すときは、まず高価な処理(物理、AI、アニメーションなど)とガベージコレクションについて調べます。

特定のフレームを分析するには、ウィンドウ内をクリックします。次に、Timeline ビューまたは Hierarchy ビューのいずれかを使用して次のことを行います。

  • Timeline は、特定のフレームのタイミングの内訳を視覚化します。これにより、アクティビティ同士が互いにどのように関連しているか、また異なるスレッド間でどのように関連しているかを可視化することができます。このオプションは、CPU バウンドか GPU バウンドかを判断するために使います。
  • Hierarchy は、ProfileMarker の階層をグループ化して表示します。これにより、ミリ秒単位の時間コスト(Time msSelf ms)に基づいてサンプルをソートすることができます。また、関数に対する呼び出しの数や、フレームのマネージヒープメモリ(GC Alloc)を数えることもできます。
ProfileMarker を時間コストでソートする
Hierarchy ビューでは、ProfileMarker を時間コストでソートすることができる。

Unity プロファイラーの概要はこちらでご覧ください。プロファイリングを初めて行う方には、こちらの Introduction to Unity Profiling も参考になるでしょう。

プロジェクト内の何らかの要素を最適化する前に、プロファイラーの .data ファイルを保存します。変更の実装を行い、保存した .data ファイルを使って、変更を行う前と後を比較します。プロファイリング、最適化、比較というこのサイクルを活用してパフォーマンスを改善しましょう。これを繰り返して、アプリケーションを洗練させていきましょう。

Profile Analyzer を使用する

このツールでは、複数のフレームのプロファイラーデータを集約し、参考になる情報を含むフレームを探し出すことができます。プロジェクトに変更を加えた後にプロファイラーがどうなるかを見てみたいと思いませんか。Compare ビューでは、2 つのデータセットを読み込み区別することで、変更をテストしてその結果を改善することができます。Profile Analyzer は、Unity のパッケージマネージャーから利用できます。

エディター内で Profile Analyzer を詳しく見る
従来のプロファイラーを補完する Profile Analyzer を使って、フレームやマーカーのデータをさらに深く掘り下げることができます。

フレームごとに決められた時間予算で動く

各フレームには、目標とする fps(毎秒ごとのフレーム数)に応じた時間予算が設定されています。理想的には、30 fps で動作するアプリケーションの場合、1 フレームあたり約 33.33 ms(1000ms / 30fps)使えることになります。同様に、60 fps を目標とすると、1 フレームあたり使える時間は約 16.66 ms となります。

限られた時間(カットシーンや読み込みシーケンスなど)であれば、デバイスがこの予算を超えても許容できますが、それを超えて予算を超えたままにしてはいけません。

デバイスの温度を考慮

また、モバイル環境ではデバイスが過熱したり、OS が CPU や GPU にサーマルスロットルを掛けたりするため、ずっと時間予算を使えるだけ使うことはお勧めできません。フレーム間で冷却できるように、使える時間予算の約 65% くらいに抑えることをお勧めします。一般的なフレーム予算は、30 fps なら約 22 ms / フレーム、60 fps なら約 11 ms / フレームとなります。

ほとんどのモバイルデバイスには、デスクトップデバイスのようなアクティブな冷却機能がありません。物理的な熱レベルは、パフォーマンスに直接影響を与える可能性があります。

デバイスが高温で動作している場合、長期的な懸念材料ではなくても、プロファイラーがパフォーマンスの低下と認識して報告することがあります。プロファイリングによるオーバーヒートを防ぐには、短時間のプロファイリングを繰り返し行います。これにより、デバイスが冷却する時間を取ることができ、実際にアプリを走らせた時に近い状況をシミュレーションできます。一般的な推奨事項として、一度プロファイリングを行った後は、デバイスを 10~15 分ほど冷やしてから再度プロファイリングを行うことをお勧めします。

GPU バウンドか CPU バウンドかを見極める

プロファイラーは、CPU が割り当てられたフレーム予算を超えているのか、それとも GPU が原因なのかを明らかにしてくれます。これは、以下のように Gfx を先頭にしたマーカーを表示することで実現しています。

  • Gfx.WaitForCommands マーカーが表示されている場合は、レンダリングスレッドの準備ができていることを意味しますが、メインスレッドのボトルネックを待っている可能性があります。
  • Gfx.WaitForPresent が頻繁に出てくる場合は、メインスレッドの準備はできていたが、GPU がフレームを表示するのを待っていたことを意味します。
メモリ

Unity では、ユーザーが作成したコードやスクリプトに対する自動メモリ管理を採用しています。値型ローカル変数などの小さなデータがスタックに割り当てられます。サイズの大きなデータや長期間保存されるデータは、マネージヒープに割り当てられます。

ガベージコレクターは、使用されていないヒープメモリを特定し、定期的に解放します。これは自動的に実行されますが、ヒープ内のすべてのオブジェクトを調査するプロセスにより、ゲームが止まったり、動作が遅くなったりすることがあります。

メモリ使用量の最適化とは、ヒープメモリの割り当てと割り当て解除のタイミングと、ガベージコレクションの影響を最小限に抑える方法を意識することを意味します。詳しくは、 マネージヒープの理解 の項をご覧ください。

エディター内のメモリプロファイラー
メモリプロファイラーでスナップショットをキャプチャ、検査、比較することができます。

メモリプロファイラーを使用する

この独立したアドオン(パッケージマネージャーで実験的パッケージまたはプレビューパッケージとして利用可能)は、管理されているヒープメモリのスナップショットを取り、断片化やメモリリークなどの問題を特定するのに役立ちます。

ツリーマップビューでクリックすると、変数をトレースして、メモリ上に保持されているネイティブオブジェクトをたどることができます。ここで、大き過ぎるテクスチャや重複したアセットなど、一般的なメモリ消費の問題を特定することができます。

Unity のメモリプロファイラーを活用すれば、より効率よくメモリを使えるようになります。また、公式のメモリプロファイラーのドキュメントもご覧ください。

ガベージコレクション(GC)の影響を軽減する

Unity は、Boehm-Demers-Weiser ガベージコレクターを使用しています。このガベージコレクターは、プログラムコードの実行を停止し、その作業が完了した後に通常の実行を再開します。

不必要なヒープの割り当てがあると、GC スパイクが発生する可能性があるので注意が必要です。

  • 文字列:C# では、文字列は値型ではなく、参照型です。不要な文字列の作成や操作は無くしましょう。JSON や XML のような文字列ベースのデータファイルの解析は避け、代わりに ScriptableObject、MessagePack、Protobuf のようなフォーマットでデータを保存するようにしましょう。実行時に文字列を構築する必要がある場合は、StringBuilder クラスを使用します。
  • Unity 関数呼び出し:一部の関数はヒープ割り当てを作成します。ループの途中で配列を割り当てるのではなく、配列への参照をキャッシュするようにしましょう。また、ゴミを発生させない関数を使うようにしましょう。たとえば、文字列を GameObject.tag と手動で比較するのではなく、GameObject.CompareTag を使用します(新しい文字列を返すとガベージが発生するため)。
  • ボクシング:参照型の変数の代わりに値型の変数を渡すことは避けてください。これにより、一時的なオブジェクトが作成され、それに伴う潜在的なゴミが、暗黙のうちに値型を object 型に変換します(例:int i = 123; object o = i)。その代わりに、渡したい値型の具体的なオーバーライドを提供するようにしてください。これらのオーバーライドにはジェネリックも使用できます。
  • コルーチン:yield はゴミを出しませんが、新しい WaitForSeconds オブジェクトを作成するとゴミを出してしまいます。WaitForSeconds オブジェクトを yield 行で作成するのではなく、キャッシュして再利用しましょう。
  • LINQ と正規表現:これらは両方とも、バックグラウンドでボックス化によってゴミを生成します。パフォーマンスを気にする場合は、LINQ と正規表現を避けてください。for ループを記述し、新しい配列を作成する代わりにリストを使用します。

可能ならガベージコレクションの時間

ガベージコレクションのフリーズがゲームの特定のポイントで影響を与えないことが分かっているなら、System.GC.Collect でガベージコレクションをトリガーすることができます。

これをうまく活用する例については、「 自動メモリ管理について 」を参照してください。

インクリメンタルガベージコレクターを使用して、GC の作業を分割する

インクリメンタルガベージコレクションでは、プログラムの実行中に 1 回長い中断を行うのではなく、それよりはるかに短い中断を複数回行い、ワークロードを多くのフレームに分散させます。ガベージコレクションがパフォーマンスに影響を与えている場合は、このオプションを有効にして、GC によるスパイクの問題を軽減できるかどうかを試してみてください。Profile Analyzer を使用して、アプリケーションにどのようなメリットがあるかを確認してください。

インクリメンタルガベージコレクターの概要
インクリメンタルガベージコレクターを使って、ガベージコレクションによるスパイクを減らしましょう。
プログラミングおよびコードアーキテクチャ

Unity の PlayerLoop には、ゲームエンジンのコアとやり取りするための関数が含まれています。この構造体には、初期化やフレームごとの更新を処理するいくつかのシステムが含まれています。すべてのスクリプトは、この PlayerLoop に依存してゲームプレイを生成します。

プロファイリングでは、PlayerLoop にプロジェクトのユーザーコードが表示されます(EditorLoop には Editor コンポーネントが表示されます)。

プロファイラーを拡大
プロファイラーは、カスタムスクリプト、設定、グラフィックを、エンジン全体の実行コンテキストにおいて表示します。
PlayerLoop のビュー

PlayerLoop とスクリプトのライフサイクルについて理解しておきましょう。

以下のヒントやコツを参考にしてスクリプトを最適化できます。

Unity PlayerLoop を理解する

Unity のフレームループの実行順序を理解していることを確認してください。すべての Unity スクリプトは、いくつかのイベント関数をあらかじめ決められた順序で実行します。AwakeStartUpdate など、スクリプトのライフサイクルを作る関数の違いを理解する必要があります。

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

毎フレーム実行されるコードを最小限に抑える

毎フレーム実行する必要があるコードはどれか、ということはよく吟味する必要があります。UpdateLateUpdateFixedUpdate から不要なロジックを削除します。これらのイベント関数は、フレームごとに更新しなければならないコードを置くのに便利な場所ではありますが、そんなに高い頻度で更新する必要のないロジックは排除する必要があります。可能な限り、状況が変化したときにのみロジックを実行するようにしましょう。

Update を使用する必要ある場合は、n フレームごとにコードを実行する方法も検討してください。これは、重いワークロードを複数のフレームに分散させる一般的な手法であるタイムスライシングを適用する方法の一つです。この例では、3 フレームに 1 回、ExampleExpensiveFunction を実行します。

private int interval = 3;

void Update()
{
    if (Time.frameCount % interval == 0)
    {
        ExampleExpensiveFunction();
    }
}

Start/Awake に重いロジックを置くのを避ける

最初のシーンがロードされると、各オブジェクトに対して以下の関数が呼び出されます。

  • Awake
  • OnEnable
  • 開始

アプリケーションが最初のフレームをレンダリングするまでは、これらの関数で負荷の高いロジックは避けてください。そうしないと、必要以上にロード時間が長くなる可能性があります。

最初のシーンの読み込みに関する詳細については、イベント機能の実行順序 を参照してください。

空の Unity イベントを避ける

空の MonoBehaviour でもリソースが必要なので、空の Update メソッドや LateUpdate メソッドは削除してください。

こういうメソッドをテストする時は、以下のようにプリプロセッサーディレクティブを使用してください。

#if UNITY_EDITOR
void Update()
{
}
#endif

こうすると、Update をエディター内で自由に使ってテストすることができます。不要なオーバーヘッドがビルドに混入することはありません。

Debug Log 命令の削除

Log ステートメント(特に UpdateLateUpdate、または FixedUpdate)は、パフォーマンスを大きく低下させる原因となります。ビルドを行う前に Log ステートメントを無効にしてください。

これを簡単に行うには、プリプロセッサーディレクティブと一緒に Conditional 属性を作ることも検討してください。例えば、次のようなカスタムクラスを作成します。

public static class Logging
{
    [System.Diagnostics.Conditional("ENABLE_LOG")]
    static public void Log(object message)
    {
        UnityEngine.Debug.Log(message);
    }
}
ENABLE_LOG のビュー
カスタムプリプロセッサーディレクティブを追加すると、スクリプトを分割できます。

カスタムクラスでログメッセージを生成します。ENABLE_LOG プリプロセッサーを Player Settings で無効にすると、Log ステートメントを一挙に除去することができます。

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

Unity は Animator、Material、Shader のプロパティを内部的に扱うのに文字列名を使いません。高速化のために、すべてのプロパティ名はハッシュ化されプロパティ ID となります。この ID は実際にプロパティを指すために使用されます。

Animator、Material、Shader の Set または Get メソッドを使用する場合、文字列値のメソッドではなく、整数値のメソッドを使用してください。文字列値メソッドは、単純に文字列のハッシュ化を行い、ハッシュ化された ID を整数値メソッドに転送します。

Animator のプロパティ名には Animator.StringToHash を、Material と Shader のプロパティ名には Shader.PropertyToID を使用してください。

適切なデータ構造の選択

データ構造の選択は、1 フレームあたり数千回の反復処理を行う際の効率に影響します。コレクションに List、Array、Dictionary のどれを使うか迷っていませんか?正しい構造を選択するための一般的なガイドとして、C# の MSDN ガイドに従うことをおすすめします。

実行時にコンポーネントを追加しない

実行時の AddComponent の呼び出しには、いくらかコストがかかります。Unity は、実行時にコンポーネントを追加する際に、重複するコンポーネントやその他の必須コンポーネントをチェックする必要があります。

必要なコンポーネントがすでにセットアップされているプレハブをインスタンス化するほうが、一般的にパフォーマンスが高くなります。

ゲームオブジェクトとコンポーネントのキャッシュ

GameObject.FindGameObject.GetComponentCamera.main(2020.2 より前のバージョンの場合)はコストがかかるので、Update メソッドの中では呼び出さないようにした方が良いでしょう。代わりに、Start で呼び出し、結果をキャッシュします。

以下は、繰り返し行われる GetComponent 呼び出しの非効率的な使い方を示す例です。

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

その代わりに、GetComponent を一度だけ呼び出し、関数の結果をキャッシュするようにします。キャッシュされた結果は、Update で再利用することができます。その際、さらに GetComponent を呼び出す必要はありません。

private Renderer myRenderer;

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

void Update()
{
    ExampleFunction(myRenderer);
}

オブジェクトプールの使用

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

ObjectPool の拡大図
この例では、ObjectPool は 20 個の PlayerLaser インスタンスを作成して再利用する。

こうした再利用可能なインスタンスは、CPU のスパイクが目立たないゲーム中のある時点(例えば、メニュー画面にいる間)で作成します。このオブジェクトの「プール」をコレクションで追跡します。ゲームプレイ中は、必要に応じて次に利用可能なインスタンスを有効にし、オブジェクトを破壊する代わりにオブジェクトを無効にして、プールに戻すだけです。

SampleScene 階層の拡大図
PlayerLaser オブジェクトのプールは無効にされており、いつでも撃ち出せる状態になっています。

これにより、プロジェクト内のマネージ割り当ての数を減らし、ガベージコレクションの問題を防ぐことができます。

Unity でシンプルなオブジェクトプールシステムを作成する方法はこちらで解説しています。

Use ScriptableObjects

不変の値や設定は MonoBehaviour ではなく、ScriptableObject に保存しましょう。ScriptableObject は、プロジェクト内に常駐するアセットで、一度だけ設定すればよいものです。ゲームオブジェクトに直接アタッチすることはできません。

ScriptableObject にフィールドを作成して値や設定を保存し、その ScriptableObject を MonoBehaviour で参照します。

Inventory という名前の ScriptableObject がさまざまなゲームオブジェクトの設定を保持しているフローチャート
Inventory という名前の ScriptableObject が、様々なゲームオブジェクトの設定を保持します。

これらのフィールドを ScriptableObject から使用することで、その MonoBehaviour を持つオブジェクトをインスタンス化するたびに発生する、不要なデータの重複を防ぐことができます。

こちらの Introduction to ScriptableObjects チュートリアルを見て、ScriptableObject がどのように皆さんのプロジェクトに役立てられるか、考えてみましょう。また、関連ドキュメントはこちらでご覧いただけます。

モバイルゲームのパフォーマンス向上のヒントのリスト全体をダウンロードする

次回のブログ記事では、グラフィックスと GPU の最適化について詳しくご紹介します。今すぐチームからのヒントとコツをすべて乗せたリスト全体を見てみたい方は、こちらのページからフル版の e ブックをダウンロードしてご覧ください。

e ブックカバー「Optimize Your Mobile Game Performance」

e ブックをダウンロード

Integrated Support サービスについてもっと知りたい、あるいはチームにエンジニアと直接やりとりする窓口や、専門家のアドバイスやプロジェクトのベストプラクティスガイダンスを提供したいとお考えの方は、こちらのページでご案内している Unity のサクセスプランをご検討ください。

今後もパフォーマンス向上のためのヒントをお届けしていきますので、ぜひご期待ください。

私たちは皆さんの Unity アプリケーションに最大限のパフォーマンスを発揮させるお手伝いをしたいと考えています。他にも取り上げてほしい最適化に関わるトピックがありましたら、コメントセクションでお知らせください。