📑 目次
Unityでゲームを開発していると、必ず直面するのがパフォーマンスの問題です。 開発初期は順調に動いていたゲームが、コンテンツが増えるにつれて重くなり、 気づけばカクカクした動きになってしまう…そんな経験はありませんか?
特にモバイルゲームやVRコンテンツでは、安定した60FPSの維持が ユーザー体験を大きく左右します。フレームレートが低下すると、 プレイヤーはストレスを感じ、最悪の場合はゲームをやめてしまうかもしれません。
本記事では、実際のプロジェクトで効果が実証された最適化テクニックを、 初心者にも分かりやすく、そして実践的に解説していきます。 これらのテクニックを組み合わせることで、多くの場合、 パフォーマンスを2倍以上向上させることが可能です。
1. Unity Profilerで問題を見つける
最適化の第一歩は、問題の特定です。 「なんとなく重い」という感覚だけでは、効果的な最適化はできません。 Unity Profilerは、ゲームのどの部分がボトルネックになっているかを 科学的に分析できる強力なツールです。
Profilerを使い始める前に知っておくべきこと
多くの開発者が犯す間違いは、エディタ上でのパフォーマンスだけを見て 最適化を進めてしまうことです。実は、Unityエディタとビルドされたアプリケーションでは、 パフォーマンス特性が大きく異なります。エディタは開発用の多くの機能を バックグラウンドで実行しているため、実機より重くなる傾向があります。
そのため、必ずDevelopment Buildとして 実機にビルドし、そこでProfilerを接続して測定することが重要です。 特にモバイルゲームの場合、PCとスマートフォンではハードウェア性能が 桁違いに異なるため、この作業は欠かせません。
よくあるパフォーマンス問題のパターン
長年の開発経験から、パフォーマンス問題には典型的なパターンがあることがわかっています。 例えば、ゲームプレイ中に定期的にカクつく場合、それはほぼ確実に ガベージコレクション(GC)が原因です。 GCは不要なメモリを自動的に解放する便利な機能ですが、 実行時に一瞬処理が止まるため、フレームレートの低下を引き起こします。
症状 | 考えられる原因 | 確認すべきProfilerタブ |
---|---|---|
定期的なカクつき | ガベージコレクション | Memory (GC Alloc) |
常に低いFPS | Draw Callsが多すぎる | Rendering |
特定のシーンで重い | 高ポリゴンモデル、重いシェーダー | GPU Usage |
徐々に重くなる | メモリリーク | Memory (Total) |
これらのパターンを知っているだけで、問題解決の時間を大幅に短縮できます。 次は、最も一般的な問題である「Draw Calls」について詳しく見ていきましょう。
2. Draw Call削減の実践方法
Draw Callは、CPUがGPUに対して「これを描画してください」と命令を送る回数です。 この数が多いほど、CPUの負荷が高くなり、結果的にフレームレートが低下します。 モバイルゲームでは、Draw Callを100〜200以下に 抑えることが推奨されています。
なぜDraw Callは増えてしまうのか
Unityは基本的に、異なるマテリアルを持つオブジェクトを別々に描画します。 例えば、赤い立方体と青い立方体があれば、それは2回のDraw Callになります。 さらに、同じマテリアルでも、動的に動くオブジェクトはバッチング(まとめて描画) できないため、個別にDraw Callが発生します。
典型的な失敗例として、「森のシーン」を考えてみましょう。 100本の木があり、それぞれが葉、幹、枝で3つのマテリアルを使っている場合、 単純計算で300のDraw Callが発生します。これだけで、 モバイルゲームの推奨値を大きく超えてしまいます。
Static Batchingという魔法
最も簡単で効果的な最適化方法がStatic Batchingです。 動かないオブジェクト(建物、地形、装飾など)をStaticに設定するだけで、 Unityが自動的にDraw Callをまとめてくれます。
Static Batchingを使うことで、先ほどの森の例では、 300のDraw Callを3つ(葉用、幹用、枝用)まで削減できる可能性があります。 これは100倍の効率化です!
テクスチャアトラスでさらなる最適化
複数の小さなテクスチャを1枚の大きなテクスチャにまとめる 「テクスチャアトラス」という技術も非常に効果的です。 例えば、UIアイコンが50個ある場合、それぞれ別のテクスチャにすると 50回のDraw Callが必要ですが、1枚のアトラスにまとめれば 1回のDraw Callで済みます。
Draw Call最適化は、即効性があり効果も大きいため、 最初に取り組むべき最適化項目です。次は、大量の同じオブジェクトを 効率的に描画する方法を見ていきましょう。
3. GPU Instancingで大量描画を効率化
草原に生える無数の草、群衆シミュレーション、弾幕シューティングの弾丸… ゲームでは同じオブジェクトを大量に描画する場面が頻繁にあります。 GPU Instancingは、このような状況で 驚異的なパフォーマンス向上をもたらす技術です。
GPU Instancingの仕組みと効果
通常、1000個の同じオブジェクトを描画するには1000回のDraw Callが必要です。 しかし、GPU Instancingを使えば、たった1回のDraw Callで すべてを描画できます。これは単純計算で1000倍の効率化です。
実際のプロジェクトでの例を紹介しましょう。 あるモバイルゲームで、画面に500体の敵キャラクターを表示する必要がありました。 最初は5FPSしか出ませんでしたが、GPU Instancingを実装したところ、 安定した60FPSを達成できました。これは12倍のパフォーマンス向上です。
Draw Calls削減
FPS向上事例
1バッチの最大数
GPU Instancingを使うための条件
GPU Instancingは強力ですが、すべての状況で使えるわけではありません。 以下の条件を満たす必要があります:
- 同じメッシュを使用している
- 同じマテリアルを使用している(色などは個別に変更可能)
- 使用するシェーダーがInstancing対応している
- SkinnedMeshRenderer(アニメーションするキャラクターなど)では使用不可
Unity標準のシェーダーの多くはInstancingに対応していますが、 マテリアルの設定で「Enable GPU Instancing」にチェックを入れる必要があります。 この小さなチェックボックスを見逃すだけで、 大きなパフォーマンス向上の機会を失ってしまいます。
GPU Instancingが適している場面
GPU Instancingが特に効果的なのは以下のような場面です:
- 自然環境:草、木、岩などの配置
- 建築物:同じ窓やブロックの繰り返し
- エフェクト:爆発の破片、魔法のパーティクル
- 群衆:観客、軍隊、ゾンビの大群
次は、カメラからの距離に応じて品質を調整する LODシステムについて解説します。
4. LODシステムで遠近法を活用
人間の目は、遠くのものの細部まで認識できません。 この特性を利用したのがLevel of Detail(LOD)システムです。 カメラから遠いオブジェクトには低品質のモデルを使用することで、 視覚的な品質を保ちながらパフォーマンスを向上させます。
LODがもたらす劇的な効果
実際の数字で効果を見てみましょう。 10万ポリゴンの精密な城のモデルがあるとします。 これが画面に10個表示されれば、合計100万ポリゴンです。 しかし、LODシステムを使えば:
- 近距離(LOD 0):10万ポリゴン(1個のみ)
- 中距離(LOD 1):2万ポリゴン × 3個 = 6万ポリゴン
- 遠距離(LOD 2):5千ポリゴン × 6個 = 3万ポリゴン
合計19万ポリゴンとなり、81%の削減を達成できます。 プレイヤーは違いにほとんど気づかないのに、 これだけの最適化ができるのです。
効果的なLOD設定の考え方
LODの設定で最も重要なのは、「プレイヤーが違和感を覚えない範囲で 積極的に品質を下げる」ことです。多くの開発者は品質にこだわりすぎて、 LODの切り替え距離を遠くに設定しがちですが、 実際のゲームプレイではプレイヤーは細部まで見ていません。
私の経験では、画面の高さの50%以下になったらLOD 1に、 20%以下になったらLOD 2に切り替えても、 ほとんどのプレイヤーは気づきません。 これは思い切った設定に感じるかもしれませんが、 実際にテストしてみると違和感がないことがわかるはずです。
LODとカリングの組み合わせ
LODシステムは、次に説明するOcclusion Cullingと組み合わせることで、 さらに効果を発揮します。見えないものは描画せず、 見えるものも距離に応じて品質を調整する。 この2つの技術を組み合わせることで、 複雑な大規模シーンでも快適なパフォーマンスを実現できます。
5. Occlusion Cullingで見えないものを除外
建物の裏側にある敵キャラクターや、壁の向こうにある宝箱。 これらはプレイヤーから見えないのに、通常は描画処理が行われています。 Occlusion Cullingは、 カメラから見えないオブジェクトを自動的に描画対象から除外する技術です。
Occlusion Cullingが威力を発揮する場面
この技術が特に効果的なのは、遮蔽物が多い環境です。 例えば、迷路のようなダンジョンや、ビルが立ち並ぶ街並み、 室内が複雑に入り組んだ建物などです。
実際のプロジェクトでの例を紹介しましょう。 100部屋ある大きな屋敷を舞台にしたホラーゲームで、 最初は全部屋のオブジェクトを描画していたため15FPSしか出ませんでした。 Occlusion Cullingを適用したところ、プレイヤーがいる部屋と 隣接する部屋だけを描画するようになり、 60FPSで安定動作するようになりました。
効果的なOcclusion Culling設定
Occlusion Cullingの設定で最も重要なのは、 「Occluder(遮る側)」と「Occludee(遮られる側)」の 適切な設定です。大きな壁や建物はOccluderに、 小物や敵キャラクターはOccludeeに設定するのが基本です。
また、「Smallest Occluder」の値も重要です。 この値より小さいオブジェクトは遮蔽物として扱われません。 値を小さくしすぎるとベイク時間が長くなり、 大きくしすぎると小さな壁の向こうが見えてしまいます。 一般的には5〜10メートル程度が適切です。
Frustum CullingとOcclusion Cullingの違い
Unityにはデフォルトで「Frustum Culling」という機能があり、 カメラの視野外のオブジェクトは自動的に描画されません。 Occlusion Cullingはこれをさらに発展させ、 視野内でも他のオブジェクトに隠れているものを除外します。
これら2つのカリング技術と、前述のLODシステムを組み合わせることで、 描画負荷を劇的に削減できます。次は、CPU側の最適化について見ていきましょう。
6. スクリプト最適化の考え方
美しいグラフィックも、スムーズなアニメーションも、 すべてはスクリプトによって制御されています。 スクリプトの最適化は、ゲーム全体のパフォーマンスに直結する 重要な要素です。ここでは、実践的な最適化の考え方を紹介します。
Update()の呼び出しを減らす
Unityで最も基本的な関数であるUpdate()は、 毎フレーム呼ばれるため、わずかな無駄も大きな負荷になります。 1000個のオブジェクトがそれぞれUpdate()を持っていれば、 毎フレーム1000回の関数呼び出しが発生します。
よくある失敗例として、「常に画面外にいる敵もUpdate()で 位置を更新し続ける」というものがあります。 画面外の敵は、プレイヤーが近づくまで更新を停止したり、 更新頻度を下げたりすることで、大幅な最適化が可能です。
キャッシュの重要性
GetComponent()やGameObject.Find()などの検索系関数は、 想像以上に重い処理です。毎フレーム実行すると、 それだけでパフォーマンスの問題を引き起こす可能性があります。
解決策は簡単で、Start()やAwake()で一度だけ取得して、 変数に保存(キャッシュ)しておくことです。 この単純な対策だけで、スクリプトの処理速度が 10倍以上高速化することもあります。
物理演算の最適化
物理演算は非常に重い処理です。特に、複雑な形状のコライダーや、 大量のRigidbodyが相互作用する場面では、 簡単にパフォーマンスの限界に達してしまいます。
対策として、以下のような方法があります:
- シンプルなコライダーを使用:MeshColliderより、BoxやSphereColliderの方が軽い
- Fixed Timestepの調整:0.02秒(50Hz)から0.03秒(約33Hz)に変更するだけで負荷が33%減少
- レイヤーマスクの活用:不要な衝突判定を行わないよう、Physics設定で制御
- スリープ設定の活用:動かないオブジェクトは自動的にスリープ状態になるよう設定
スクリプトの最適化は、小さな改善の積み重ねが大きな効果を生みます。 次は、その中でも特に重要なメモリ管理について詳しく見ていきましょう。
7. メモリ管理でスムーズな動作を実現
ゲームが突然カクッと止まる現象を経験したことはありませんか? これは多くの場合、ガベージコレクション(GC)が 原因です。GCは不要になったメモリを自動的に解放する便利な機能ですが、 実行時に一瞬すべての処理が停止するため、 プレイヤーにとっては不快な体験となります。
GCが発生する仕組みと影響
C#では、新しいオブジェクトを作成するたびにメモリが確保されます。 このメモリが一定量に達すると、GCが自動的に実行され、 使われていないメモリを解放します。問題は、この処理に 10〜100ミリ秒かかることがあり、60FPSを維持するための 16.6ミリ秒を大きく超えてしまうことです。
特にモバイルデバイスでは、GCの影響が顕著に現れます。 PCでは気にならない程度のメモリアロケーションでも、 モバイルでは致命的なカクつきの原因となることがあります。
メモリアロケーションを避ける実践テクニック
最も効果的な対策は、そもそもメモリアロケーションを 発生させないことです。以下に、よくある問題と解決策を紹介します:
1. 文字列の連結を避ける
スコア表示などで「Score: 100」のような文字列を作る際、
単純に連結すると新しい文字列が作られ、GCの対象となります。
StringBuilderを使うか、事前にフォーマットを用意しておくことで
この問題を回避できます。
2. 配列やリストの再利用
Physics.OverlapSphereのような関数は、呼ぶたびに新しい配列を作成します。
代わりにNonAllocバージョンを使い、事前に用意した配列を
再利用することで、メモリアロケーションを完全に避けられます。
3. 構造体の活用
小さなデータの集まりは、クラスではなく構造体(struct)で
定義することを検討しましょう。構造体は値型なので、
GCの対象になりません。ただし、コピーコストがあるため、
大きなデータには適しません。
理想的なGC Alloc
モバイルでの典型的なGC時間
適切な対策での削減率
テクスチャとオーディオのメモリ管理
スクリプトだけでなく、アセットのメモリ管理も重要です。 特にテクスチャは、気づかないうちに大量のメモリを消費します。 2048×2048のテクスチャ1枚で16MB(非圧縮時)も使用するため、 適切な圧縮設定が欠かせません。
モバイルゲームでは、以下の設定を推奨します:
- UI用テクスチャ:RGB Compressed ETC2(Android)/ RGB Compressed PVRTC(iOS)
- 3Dモデル用:必要に応じてMip Mapを有効化、最大サイズは1024×1024以下
- オーディオ:短い効果音はDecompress On Load、BGMはStreamingに設定
メモリ管理は地味な作業ですが、安定したパフォーマンスのためには 欠かせない要素です。これまでに紹介したすべての最適化技術を 組み合わせることで、プロフェッショナルなゲーム体験を提供できます。
まとめ:継続的な改善の重要性
ここまで、Unity最適化の7つの重要なテクニックを詳しく解説してきました。 これらの技術を適切に組み合わせることで、多くのプロジェクトで 2倍から10倍のパフォーマンス向上を 実現できます。
最適化の優先順位
すべての最適化を一度に行う必要はありません。 効果と実装の容易さを考慮した、推奨される優先順位は以下の通りです:
- Profilerでボトルネックを特定 - まずは現状を正確に把握
- Draw Call削減 - Static BatchingやAtlasで即効性のある改善
- LODシステムの導入 - 大きなオブジェクトから順に適用
- スクリプトの最適化 - Update()の削減とキャッシュの活用
- GPU Instancing - 同じオブジェクトが大量にある場合に検討
- Occlusion Culling - 複雑な室内環境で効果大
- メモリ管理の改善 - GCによるカクつきが目立つ場合に対応
- 測定なくして最適化なし - 必ずProfilerで効果を確認
- 早すぎる最適化は悪 - まず動くものを作ってから最適化
- 80/20の法則 - 20%の努力で80%の改善を狙う
- 実機でのテストを忘れずに - エディタとは性能が全く異なる
継続的な最適化プロセス
最適化は一度やれば終わりではありません。新しい機能を追加するたびに、 新しいボトルネックが生まれる可能性があります。 以下のようなプロセスを開発に組み込むことをお勧めします:
- 週次パフォーマンステスト - 定期的に主要シーンのFPSを測定
- 機能追加時の影響評価 - 新機能導入前後でProfilerを比較
- ターゲットデバイスの明確化 - 最低スペックの端末で定期的にテスト
- チーム全体での意識共有 - パフォーマンス目標を全員で共有
最後に
パフォーマンス最適化は、プレイヤーに最高の体験を提供するための 重要な投資です。技術的に優れたゲームも、カクカクした動作では その魅力を十分に伝えることができません。
本記事で紹介した技術は、すべて実際のプロジェクトで 効果が実証されたものです。まずは自分のプロジェクトで Profilerを開き、最大のボトルネックを見つけることから始めてください。 小さな改善の積み重ねが、やがて大きな成果となって現れるはずです。
快適な60FPSで動作するゲームを作り、プレイヤーに 最高のゲーム体験を届けられることを願っています。 Happy Optimizing!