モバイルゲームのパフォーマンスを最適化する:グラフィックスとアセットに関するエキスパートからのヒント

Integrated Success チームは、Unity の顧客が抱える複雑な技術的問題をサポートします。シニアソフトウェアエンジニアで構成されるこのチームに、モバイルゲームの最適化に関する知見を聞かせてもらいました。
当社の Unity Studio Production チームはソースコードを熟知しており、無数に存在する Unity の顧客みんながエンジンを最大限に活用できるようサポートしています。このチームは、クリエイターのプロジェクトに深く入り込み、パフォーマンスを最適化することで、スピード、安定性、効率性を向上させるポイントを見つけ出します。Unity のエンジニアたちがモバイルゲームの最適化に関する知見のシェアを始めるとすぐに、元々予定していた 1 本のブログ記事に収まりきらないほど素晴らしい情報があることに気づきました。そこで私たちは、彼らの膨大な知識を e ブックとしてまとめ(ダウンロードはこちら)、そこに収録されている 75 以上の実用的なヒントの一部を紹介する一連のブログ記事を作成することにしました。
最適化に関する本シリーズの最終回となるこの記事では、アセット、プロジェクト構成、グラフィックスのパフォーマンスを向上させる方法に焦点を当てます。ゲームの最適化については、プロファイリング、メモリ、コードアーキテクチャ、物理、UI、オーディオについての過去の記事をご覧ください。また、無料の e ブックをダウンロードすると、これらのすべてのトピックに関する内容に触れることができます。
モバイルのパフォーマンスに影響を与える可能性のあるプロジェクト設定がいくつかあります。
加速度センサーの周波数を下げる、または無効にする
Unity では、携帯電話の加速度センサーからのデータを 1 秒間に数回プールしています。アプリケーションで使用されていない場合はこれを無効にするか、その頻度を下げてパフォーマンスを向上させてください。

不要なプレイヤー設定や品質設定を無効にする
Player の設定で、サポートされていないプラットフォームで Auto Graphics API は無効にして、過剰なシェーダーバリアントの生成を防ぎます。古い CPU をアプリケーションでサポートしない場合は、Target Architectures を無効にします。
Quality 設定で、不要な Quality レベルを無効にします。
不要な物理演算を無効にする
ゲームが物理演算を使用していない場合は、Auto Simulation と Auto Sync Transforms のチェックを外してください。チェックを付けていてもアプリケーションを遅くするだけで、目に見えた効果はありません。
適切なフレームレートを選択する
モバイルプロジェクトでは、バッテリー消費やサーマルスロッティングの対策を考えつつ、ちょうどいいフレームレートを定める必要があります。デバイスの限界に挑戦して 60 fps を出すより、30 fps での運用で妥協することを検討したほうがいいこともあります。Unity のモバイルプラットフォームのデフォルトは 30 fps です。
また、Application.targetFrameRate で実行時にフレームレートを動的に調整することもできます。例えば、ゆっくりとしたシーンや比較的静かなシーンでは 30 fps 以下に落とし、ゲームプレイではより高い fps に設定するよう指定することができます。
大きな階層構造を避ける
階層構造は小分けにしましょう。ゲームオブジェクトを階層化する必要がない場合は、親子関係を単純化しましょう。階層構造を小さくしておけば、シーン内のトランスフォームの更新でマルチスレッドの恩恵を受けることができます。階層構造が複雑になると、不必要なトランスフォームの計算が発生し、ガベージコレクションのコストが増大します。
階層構造の最適化に関するベストプラクティスや、こちらの Unite 講演を見て、トランスフォームに関するベストプラクティスを学びましょう。
トランスフォームは 2 回ではなく、1 回だけ
さらに、トランスフォームを動かす時は、Transform.SetPositionAndRotation を使って、位置と回転の両方を一度に更新するようにします。これにより、トランスフォームを 2 回修正するために起きるオーバーヘッドを回避できます。
実行時にゲームオブジェクトに対して Instantiate を使う必要がある場合、簡単な最適化として、インスタンス化の際に親子関係の設定と位置の設定を行うというものがあります。
GameObject.Instantiate(prefab, parent);
GameObject.Instantiate(prefab, parent, position, rotation);
Object.Instantiate の詳細については、スクリプティング API を参照してください。
Vsync が有効になっているものと考える
モバイル端末ではハーフフレームがレンダリングされません。エディターで Vsync を無効にしても(Project Settings > Quality)、ハードウェアレベルでは Vsync は有効になります。GPU が十分な速度で画面をリフレッシュできない場合は、現在のフレームが保持され、事実上、fps が低下します。
アセットパイプラインは、アプリケーションのパフォーマンスに劇的な影響を与える可能性があります。経験豊富なテクニカルアーティストなら、アセットのフォーマット、仕様、インポート設定を定義し、それを運用することで、スムーズなプロセスを実現させます。
デフォルトの設定に頼らないようにしましょう。プラットフォーム固有のオーバーライドタブを使用して、テクスチャやメッシュジオメトリなどのアセットを最適化します。設定を誤ると、ビルドサイズが大きくなったり、ビルド時間が長くなったり、雑なメモリの使い方になってしまったりします。プリセット機能を使って、特定のプロジェクトを強化するベースライン設定をカスタマイズすることを検討してください。
アートアセットのベストプラクティスに関するガイドや、Unity Learn の「モバイルアプリケーションのための 3D アートの最適化」コースをチェックしてください。
テクスチャを正しくインポートする
メモリのほとんどはテクスチャに使われるので、テクスチャのインポート設定は重要です。一般的には、次のガイドラインに従うようにしてください。
- 最大サイズを小さくする:視覚的に問題のない結果が得られる最小の設定を使用しましょう。これは非破壊的な処理で、かつテクスチャメモリをすぐに減らすことができる対策です。
- 2 の累乗(POT)を使用する:Unity では、モバイルのテクスチャ圧縮形式(PVRTC や ETC)を使う時にテクスチャの寸法を POT にする必要があります。
- Atlas your textures:複数のテクスチャを 1 つのテクスチャにまとめることでドローコールを減らし、レンダリングを高速化することができます。Unity のスプライトアトラスや、サードパーティ製の TexturePacker を使用してテクスチャアトラスを作りましょう。
- 「Read/Write Enabled」オプションをオフにします。このオプションを有効にすると、CPU と GPU の両方のアドレス可能なメモリにコピーが作成され、テクスチャのメモリフットプリントが 2 倍になります。ほとんどの場合、このオプションは無効にしておくことをおすすめします。実行時にテクスチャを生成している場合は、Texture2D.Apply で、makeNoLongerReadable を true に設定することで、このオプションを強制的に無効にします。
- 不要なミップマップを無効にする:ミップマップは、2D スプライトや UI グラフィックスなど、画面上で一定の大きさを保つテクスチャには必要ありません(カメラからの距離が変化する 3D モデルには、ミップマップを有効にしておきます)。

テクスチャの圧縮
同じモデル、同じテクスチャーを使った 2 つの例を考えてみましょう。左側の設定は、右側の設定に比べて約 8 倍のメモリを消費しますが、画質はそれほど向上しません。

iOS と Android の両方で Adaptive Scalable Texture Compression(ATSC)を使いましょう。現在開発中のゲームの大半は、ATSC 圧縮に対応した最小スペックのデバイスをターゲットにしています。
ただし、以下の場合は例外です。
- A7 チップ搭載デバイスやそれ以前のデバイスを対象とした iOS ゲーム(例:iPhone 5、5S など)。この場合は PVRTC を使ってください。
- 2016 年以前のデバイスを対象とした Android ゲーム。この場合は ETC2 (Ericsson Texture Compression)を使用してください。
PVRTC や ETC などの圧縮フォーマットでは十分な品質が得られない場合や、ターゲットプラットフォームで ASTC が完全にサポートされていない場合は、32 ビットテクスチャではなく 16 ビットテクスチャを試してみてください。
推奨テクスチャ圧縮フォーマットの詳細については、マニュアルを参照してください。
メッシュインポート設定の調整
テクスチャと同様に、メッシュも慎重にインポートしないと過剰なメモリを消費する可能性があります。メッシュのメモリ消費を最小限に抑えるためには、以下に挙げることを実行するようにしてください。
- メッシュを圧縮します。圧縮率を高くすることで、ディスクスペースの使用量を減らすことができます(ただし、実行時のメモリ使用量の削減効果はありません)。なお、メッシュの量子化をした結果メッシュが不正確になる可能性があるので、モデルに合わせて圧縮レベルを調整して試すようにしてください。
- 読み取り/書き込みを無効にする:このオプションを有効にするとメモリ内のメッシュが複製され、メッシュのコピーがシステムメモリに 1 つ、GPU メモリに 1 つ保持されるようになります。ほとんどの場合、これを無効にしたほうがよいでしょう(Unity 2019.2 以前では、このオプションはデフォルトでチェックされています)。
- リグとブレンドシェイプを無効にする:スケルトンやブレンドシェイプのアニメーションを必要としないメッシュの場合は、可能な限りこれらのオプションを無効にしてください。
- 法線と接線を無効にする:メッシュのマテリアルに法線や接線が必要ないことが確実な場合は、これらのオプションのチェックを外しておくと、さらにリソースを節約できます。

ポリゴン数を確認する
解像度の高いモデルはメモリ使用量が多く、GPU の処理時間も長くなる可能性があります。背景のジオメトリに 50 万ポリゴンも必要でしょうか。DCC パッケージの中のモデルで、削れるものがないか検討しましょう。カメラ視点で見えないポリゴンを削除したり、高密度のメッシュの代わりにテクスチャや法線マップで細かいディテールを表現できないか検討したり、工夫できる部分があります。
AssetPostprocessor を使ってインポート設定を自動化する
AssetPostprocessor を使って、アセットをインポートする際にスクリプトを実行することができます。モデル、テクスチャ、オーディオなどを読み込む前後に、設定をカスタマイズするよう促されます。
Addressable Asset System を使用する
Addressable Asset System は、コンテンツを管理するためのシンプルな方法を提供します。この統一されたシステムでは、アセットバンドルを「アドレス」またはエイリアスごとに、ローカルのパスまたはリモートのコンテンツデリバリーネットワーク(CDN)から非同期的に読み込みます。

コード以外のアセット(モデル、テクスチャ、プレハブ、オーディオ、さらにはシーン全体)をアセットバンドルに分割すれば、ダウンロードコンテンツ(DLC)として分離することができます。
次に、Addressables を使用して、モバイルアプリケーション用の小さな初期ビルドを作成します。Cloud Content Deliveryを使用すると、ゲームコンテンツをホストして、ゲームの進行に合わせてプレイヤーに配信できます。

こちらをクリックして、Addressable アセットシステムを使って煩わしいアセット管理を単純化する方法をご覧ください。
フレームごとに、Unity はレンダリングすべきオブジェクトを決定し、ドローコールを作成します。ドローコールとは、オブジェクト(三角形など)を描画するためのグラフィックス API への呼び出しであり、バッチとは、まとめて実行するドローコールのグループです。
プロジェクトが複雑になってくると、GPU のワークロードを最適化するパイプラインが必要になります。ユニバーサルレンダーパイプライン(URP)は、現状シングルパスのフォワードレンダラーを使用して、高品質なグラフィックスをモバイルプラットフォームに提供しています(将来のリリースでは、ディファードレンダリングが可能になる予定です)。家庭用ゲーム機や PC で使われている物理ベースのライティングやマテリアルを、スマートフォンやタブレットでも使うことができます。
以下のガイドラインは、グラフィックスの高速化に役立ちます。
ドローコールをバッチにまとめる
描画するオブジェクトをバッチにまとめておくことで、バッチ内の各オブジェクトの描画に必要な状態変化を最小限に抑えることができます。これにより、オブジェクトをレンダリングするための CPU コストが削減され、パフォーマンスの向上につながります。Unity では、複数のオブジェクトをより少ないバッチにまとめるテクニックがいくつか存在します。
- 動的バッチ処理:小さなメッシュの場合、Unity は CPU 上で頂点をグループ化して変換した後、一度にすべてを描画することができます。注意:このテクニックはポリゴン数が十分に小さいメッシュ(頂点アトリビュートが 900 以下、頂点数が 300 以下)に対してのみ使うようにしてください。Dynamic Batcher はこれより大きなメッシュをバッチ処理しないため、大きなメッシュに対して有効にすると、毎フレームバッチ処理する小さなメッシュを探すようになり、CPU 時間を浪費することになります。
- 静的バッチ処理:動かないジオメトリについては、Unity は同じマテリアルを共有するメッシュのドローコールを減らすことができます。動的バッチ処理よりも効率的ですが、より多くのメモリを使用します。
- GPU インスタンシング:同一のオブジェクトが多数ある場合、グラフィックスハードウェアを使用してより効率的にバッチ処理を行う手法です。
- SRP バッチング:Advanced 設定から、SRP Batcher を ユニバーサルレンダーパイプラインのアセットに対して有効にします。これにより、シーンにもよりますが、CPU のレンダリング時間を大幅に短縮することができます。

フレームデバッガーを使用する
フレームデバッガーを使うと、各フレームが個別のドローコールによってどのように構成されているかが分かります。これは、シェーダーのプロパティのトラブルシューティングを行い、ゲームがどのようにレンダリングされるかを分析するのに役立つ非常に貴重なツールです。

フレームデバッガーを使うのが初めての方は、こちらの入門チュートリアルをご覧ください。
動的ライトを多用しない
モバイルアプリケーションには、あまり多くの動的ライトを追加しないようにすることが重要です。動的メッシュにはカスタムシェーダーによるエフェクトやライトプローブ、静的メッシュにはベイク済みライトを使うなどの代替手段を検討してください。
URP とビルトインパイプラインにおけるリアルタイムライトの限界については、この機能比較表を参照してください。
シャドウを無効にする
シャドウキャストは、MeshRenderer とライトごとに個別に無効にすることができます。ドローコールを減らすために、可能な限りシャドウを無効にします。
また、キャラクターの下に置いた単純なメッシュや四角形にぼかしたテクスチャを適用して、影に見せかけた表現をすることもできます。それ以外にも、カスタムシェーダーでブロブシャドウを作るという方法があります。

ライティングをライトマップにベイクする
グローバルイルミネーション(GI)を使用して、静的なジオメトリにドラマチックなライティングを付加しましょう。オブジェクトを Contribute GI でマークすることで、高品質なライティングをライトマップの形で保存することができます。
ベイクされたシャドウとライティングは、実行時のパフォーマンスを低下させることなくレンダリングできます。プログレッシブ CPU/GPU ライトマッパーは、グローバルイルミネーションのベイクを高速化することができます。

マニュアルガイドとライティングの最適化に関するこちらの記事を参考にして、Unity でライトマッピングを始めてみましょう。
ライトレイヤーの使用
複数のライトを使用する複雑なシーンでは、レイヤーでオブジェクトを分離し、各ライトの影響を個別のカリングマスクに絞り込みます。

移動するオブジェクトにライトプローブを使用する
ライトプローブは、高品質なライティング(直接光、間接光)を提供しながら、シーン内の何もない空間のライティング情報をベイクして保存します。球面調和関数を使っているので、動的ライトに比べて計算が非常に速いです。

Level of Detail(LOD)の使用
オブジェクトが遠くに移動すると、Level of Detail は、GPU パフォーマンスを支援するために、よりシンプルなマテリアルとシェーダーでよりシンプルなメッシュを使用するように調整または切り替えることができます。


オクルージョンカリングで隠れたオブジェクトを消す
他のオブジェクトの後ろに隠れているオブジェクトも裏でレンダリングされ、リソースを消費します。こうしたオブジェクトはオクルージョンカリングで消しましょう。
カメラの視野外の錐台カリングは自動で行われますが、オクルージョンカリングはベイク工程で行われます。オブジェクトを Static Occluder または Occludees としてマークし、Window > Rendering > Occlusion Culling ダイアログでベイクするだけ。すべてのシーンで必要というわけではありませんが、カリングによってパフォーマンスが向上するケースも多数あります。
詳しくは、「オクルージョンカリングの使用方法」チュートリアルをご覧ください。
モバイルのネイティブ解像度を避ける
スマートフォンやタブレットの高性能化に伴い、新しいデバイスは非常に高解像度になる傾向があります。
Screen.SetResolution(width, height, false) を使用して出力解像度を下げ、パフォーマンスをいくらか回復させましょう。複数の解像度でプロファイルを取ることで、画質と速度の最適なバランスを見つけることができます。
カメラの使用を制限する
カメラはそこにあるだけで、何か意味のある仕事をしているかどうかに関わらずオーバーヘッドを発生させます。レンダリングに必要な Camera コンポーネントだけを使うようにしましょう。ローエンド寄りのモバイルプラットフォームでは、カメラ 1 つあたり最大で 1ms の CPU 時間を消費することもあります。
シェーダーをシンプルに保つ
ユニバーサルレンダーパイプラインには、モバイルプラットフォーム向けに最適化された軽量の Lit および Unlit シェーダーがいくつか含まれています。実行時のメモリ使用量に劇的な影響を与えるので、シェーダーのバリエーションはできるだけ少なくするようにしてください。URP のデフォルトシェーダーでは物足りないという方は、シェーダーグラフを使ってマテリアルの見た目をカスタマイズすることができます。シェーダーグラフを使ってシェーダーを視覚的に構築する方法はこちらをご覧ください。

オーバードローとアルファブレンディングを最小限に抑える
不要な透明や半透明の画像は描かないようにしましょう。結果として生じるオーバードローとアルファブレンドは、モバイルプラットフォームに大きな影響を与えます。ほとんど見えないような画像やエフェクトが重ならないようにします。オーバードローは、RenderDoc グラフィックスデバッガーを使用して確認できます。
ポストプロセッシングエフェクトを制限する
グローなどのポストプロセッシングエフェクトを全画面にかけると、パフォーマンスが劇的に低下します。タイトルのアートディレクションに使いたくなるかもしれませんが、慎重に検討した上で使うようにしてください。

Renderer.material に気を付ける
スクリプトで Renderer.material にアクセスすると、マテリアルが複製され、新しいコピーへの参照が返されます。これにより、すでにマテリアルが含まれている既存のバッチが壊れることになります。バッチされたオブジェクトのマテリアルにアクセスしたい場合は、代わりに Renderer.sharedMaterial を使用します。
SkinnedMeshRenderer の最適化
スキンドメッシュのレンダリングにはコストがかかります。SkinnedMeshRenderer を使用しているすべてのオブジェクトが、それを必要としているか確認しましょう。時々アニメーションする程度のゲームオブジェクトの場合は、BakeMesh 関数を使ってスキンドメッシュを固定し、実行時にはよりシンプルな MeshRenderer に切り替えます。
リフレクションプローブの最小化
リフレクションプローブを使うとリアルな反射表現が可能になりますが、バッチのコストが非常に高くなります。実行時のパフォーマンスを向上させるために、低解像度のキューブマップ、カリングマスク、およびテクスチャ圧縮を活用しましょう。
このブログ記事は、モバイルパフォーマンス最適化シリーズの最終回となります。ヒントとコツの完全なリストを見てみたい方は、こちらのページからフル版の e ブックをダウンロードしてご覧ください。

Integrated Support サービスについてもっと知りたい、あるいはチームにエンジニアと直接やりとりする窓口や、専門家のアドバイスやプロジェクトのベストプラクティスガイダンスを提供したいとお考えの方は、こちらのページでご案内している Unity のサクセスプランをご検討ください。
お探しのものが見つかりませんでしたか?
私たちは、皆さんの Unity アプリケーションのパフォーマンスを可能な限り引き出すためのお手伝いをしたいと考えています。詳しく知りたい最適化のトピックがありましたら、ぜひコメントでお知らせください。
