概要
Unityでゲームを開発していると、特に長時間プレイした後や、特定の場面でゲームが一瞬カクつく(スパイクが発生する)現象に悩まされることがあります。その原因の多くは、ガベージコレクション (Garbage Collection, GC) と呼ばれるメモリ管理の仕組みにあります。
C#のようなマネージド言語では、プログラマが手動でメモリを解放する必要がなく、不要になったメモリはガベージコレクタが自動的に見つけてクリーンアップしてくれます。これは非常 に便利な機能ですが、この「クリーンアップ処理」が実行される間、プログラムの実行が一時的に停止してしまうという副作用があります。これがスパイクの正体です。
この記事では、ガベージコレクションがなぜパフォーマンスの問題を引き起こすのか、どのようなコードが不要なメモリ(ガベージ)を生成してしまうのか、そしてGCの発生を最小限に抑えるためのプロフェッショナルなコーディングテクニックについて、Unity公式ドキュメントや海外のトップデベロッパーの知見を基に詳しく解説します。
なぜGCは悪者なのか?
C#のメモリは、大きく分けてスタック (Stack) とヒープ (Heap) の2つの領域に分かれています。
- スタック:
int,float,boolなどの値型や、関数内のローカル変数が置かれる領域。管理が非常に高速で、関数の終了と共に自動的に解放されます。 - ヒープ:
classのインスタンス(オブジェクト)、文字列(string)、配列など、参照型のデータが置かれる領域。より大きなデータを長期間保持できますが、管理が複雑です。
問題となるのはヒープ領域です。プログラムが新しいオブジェクトをnewで作成すると、ヒープにメモリが確保(アロケーション)されます。やがてそのオブジェクトが使われなくなると、それは「ガベージ(ゴミ)」となります。ヒープ領域が一杯になってくると、ガベージコレクタが起動し、このゴミを掃除して空き容量を確保します。この掃除中に、ゲームの実行がStop-The-World(完全に停止)してしまうのです。
ガベージを生成する一般的な原因と対策
GCの発生を抑える最も効果的な方法は、そもそもヒープアロケーションを減らすことです。つまり、不要なガベージを極力作らないコーディングを心掛ける必要があります。
1. 文字列の結合
悪い例: stringの+演算子は、内部で新しい文字列オブジェクトを毎回生成するため、非常に多くのガベージを生みます。
void Update()
{
// 毎フレーム新しい文字列がヒープに生成される
string message = "Player Health: " + currentHealth;
uiText.text = message;
}
対策: StringBuilderクラスを使いましょう。StringBuilderは内部的なバッファを持ち、文字列を追加しても新しいオブジェクトを生成しません。
private StringBuilder sb = new StringBuilder();
void Update()
{
// ガベージを生成しない
sb.Clear();
sb.Append("Player Health: ");
sb.Append(currentHealth);
uiText.text = sb.ToString(); // 最後にToString()する時だけアロケーションが発生
}
2. newキーワードの多用
悪い例: Update内で配列やクラスのインスタンスをnewで生成すると、毎フレームガベージが発生します。
void Update()
{
// 毎フレーム新しい配列が生成される
Vector3[] positions = new Vector3[10];
// ...
}
対策: オブジェクトプーリング (Object Pooling) を使いましょう。弾や敵、エフェクトなど、頻繁に生成・破棄されるオブジェクトは、予め一定数を生成して非アクティブにしておき、必要になったら再利用します。これにより、実行中のInstantiateとDestroy(どちらも内部でヒープアロケーションを伴う)を劇的に減らすことができます。
3. コルーチンでのyield return new WaitForSeconds()
悪い例: コルーチンのループ内でnew WaitForSeconds()を使うと、ループのたびに新しいWaitForSecondsオブジェクトが生成されます。
IEnumerator FadeOut()
{
for (float f = 1f; f >= 0; f -= 0.1f)
{
// 毎回新しいオブジェクトが生成される
yield return new WaitForSeconds(0.1f);
}
}
対策: WaitForSecondsオブジェクトを一度だけ生成し、キャッシュして使い回します。
private WaitForSeconds delay = new WaitForSeconds(0.1f);
IEnumerator FadeOut()
{
for (float f = 1f; f >= 0; f -= 0.1f)
{
// キャッシュしたインスタンスを再利用
yield return delay;
}
}
4. foreachループ
古いバージョンのUnity(2020.1以前)では、foreachループが内部でガベージを生成することがありました。最新のバージョンではほとんどの場合で解決されていますが、古いプロジェクトを扱う場合や、パフォーマンスを極限まで追求する場合は、昔ながらのforループを使う方が安全です。
5. LINQやラムダ式
LINQ (Where, Selectなど) はコードを簡潔にしますが、内部で多くのヒープアロケーションを伴うことがあります。パフォーマンスが重要な場面(Update内など)での多用は避けるべきです。
ProfilerでGCアロケーションを確認する方法
コードのどこでガベージが生成されているかを特定するには、UnityのProfiler (Window > Analysis > Profiler) を使います。
- Profilerウィンドウを開き、対象のプラットフォーム(Editorまたは接続した実機)を選択します。
CPU Usageモジュールを選択し、Recordボタンを押してゲームを実行します。- グラフのスパイクが発生しているフレームをクリックします。
- 下部の詳細ビューで、
GC Allocというカラムに注目します。この列にゼロ以外の数値が表示されている関数が、ヒープアロケーション(ガベージの原因)を発生させている犯人です。 - 関数名をクリックしてドリルダウンしていくことで、具体的な原因箇所を特定できます。
まとめ
ガベージコレクションは、Unity開発における永遠のテーマの一つです。特にリソースの限られたモバイルプラットフォームでは、GCの管理がゲームの品質を直接左右します。
- GCは、ヒープメモリの掃除中にゲームを一時停止させるため、カクつきの原因となる。
- GCを避けるには、ヒープアロケーション(ガベージの生成)を減らすことが最も重要。
Update内での文字列結合、newの多用、キャッシュしないコルーチンは避ける。- オブジェクトプーリングは非常に効果的なテクニック。
- Profilerを使って、どこでガベージが生成されているかを常に監視する。
これらの「ガベージを意識したコーディング」を習慣づけることで、ユーザー体験を損なうカクつきのない、スムーズでプロフェッショナルなゲームを開発できるようになります。