UnityUnityメモ

【徹底解説】Unityのガベージコレクション(GC)を最適化してゲームの処理落ちを防ぐ方法

Unity

はじめに

Unityでゲームを開発していると、「なんだか動作がカクつく…」「フレームレートが不安定…」と感じることはありませんか? その原因の一つとして考えられるのが GC(ガベージコレクション) の影響です。

ガベージコレクションは、不要になったメモリを自動的に回収してくれる仕組みですが、この処理が発生するタイミングによっては、ゲームのパフォーマンスに悪影響を及ぼすことがあります。特に、GCが実行されるときに一時的にフレームレートが大きく落ち込む スパイク(急激な処理負荷の上昇) が発生し、結果としてゲームがカクついてしまうことがあります。

では、どのようにすればUnityのGCを最適化し、スムーズなゲーム体験を実現できるのでしょうか? この記事では、GCの仕組みを理解し、Unityのパフォーマンスを向上させるための具体的な最適化テクニックを詳しく解説していきます。あなたのゲームの動作を軽量化し、より快適なプレイ環境を提供できるよう、一緒に学んでいきましょう!




2. GC(ガベージコレクション)とは?

GC(ガベージコレクション)は、不要になったメモリを自動的に回収する仕組み です。C#で書かれたUnityのスクリプトでは、開発者が明示的にメモリを管理する必要はなく、使わなくなったオブジェクトは自動的にGCによって削除されます。

例えば、new を使って新しいオブジェクトを作成した場合、そのオブジェクトはメモリ上に確保されます。しかし、使わなくなったオブジェクトが増え続けるとメモリが圧迫されるため、一定のタイミングでGCが実行され、それらを一斉に回収するのです。

GCの仕組み

GCが動作する基本的な流れは以下の通りです。

  1. オブジェクトが生成される
    • new キーワードを使ってオブジェクトを作成すると、メモリ上にデータが確保されます。
  2. オブジェクトが不要になる
    • 参照がなくなったオブジェクト(変数からアクセスできなくなったもの)は「不要」とみなされます。
  3. GCが不要なオブジェクトを検出
    • 一定の条件を満たすとGCが実行され、不要なオブジェクトを見つけ出します。
  4. メモリの回収
    • 検出された不要オブジェクトを削除し、空いたメモリを他の処理に再利用できるようにします。

UnityにおけるGCの影響

GCは便利な機能ですが、その動作がゲームのパフォーマンスに悪影響を与えることがあります。特に問題になるのは、GCが実行されるときにフレームレートが急激に低下する「スパイク」 です。これは、GCの処理が一時的にCPUに負荷をかけるため、ゲームの処理が一時停止することが原因です。

例えば、次のようなケースでGCが頻繁に発生することがあります。

  • Instantiate()Destroy() を頻繁に使う
  • string を結合するたびに新しいメモリを確保してしまう
  • List<>Dictionary<> のサイズ変更を頻繁に行う

このような問題を防ぐためには、GCが発生しにくいコードを書き、できるだけ効率よくメモリを管理することが重要です。次のセクションでは、GCが引き起こすパフォーマンス低下の具体的な原因について詳しく解説していきます。




3. GCによるパフォーマンス低下の原因

Unityのゲーム開発において、GC(ガベージコレクション) が原因でパフォーマンスが低下することがあります。特に、ゲーム中に急なカクつき(スパイク)が発生する場合、GCの処理負荷が影響している可能性が高いです。ここでは、GCが頻繁に発生してしまう主な原因を解説します。


1. オブジェクトの頻繁な生成と破棄 (Instantiate() & Destroy())

Unityでは Instantiate() を使ってオブジェクトを生成し、 Destroy() を使って削除することができます。しかし、これらの処理を頻繁に行うと、メモリの確保と解放が繰り返され、GCの負担が大きくなります。

例:毎フレーム弾を生成&削除しているケース

void Update() {
if (Input.GetKeyDown(KeyCode.Space)) {
GameObject bullet = Instantiate(bulletPrefab);
Destroy(bullet, 2f); // 2秒後に削除
}
}

このコードでは、プレイヤーがスペースキーを押すたびに新しい弾を生成し、2秒後に削除しています。この処理を繰り返すとGCが頻繁に発生し、ゲームのフレームレートが不安定になってしまいます。


2. string の結合を多用する

C#の string不変(immutable) なので、一度作成された文字列は変更できません。そのため、 + 演算子を使って string を結合すると、新しい string インスタンスが毎回メモリ上に作られます。

例:スコア表示の更新

void Update() {
scoreText.text = "Score: " + score;
}

このコードでは scoreText.text を毎フレーム更新するたびに、新しい string インスタンスが作成されます。これが積み重なると、GCが頻繁に発生してしまいます。


3. リストや配列の頻繁なサイズ変更 (List<> の Add() & Remove())

List<>Dictionary<> などの動的コレクションを使うと、メモリの確保や解放が自動で行われます。しかし、リストのサイズが増減するたびに、新しいメモリ領域が確保され、不要なメモリがGCによって回収されます。

例:敵リストの管理

List<GameObject> enemies = new List<GameObject>();

void SpawnEnemy() {
GameObject enemy = Instantiate(enemyPrefab);
enemies.Add(enemy);
}

void RemoveEnemy(GameObject enemy) {
enemies.Remove(enemy);
Destroy(enemy);
}

このコードでは、敵をリストに追加・削除するたびにメモリの再確保と解放が発生します。敵が大量に発生・消滅するとGCが頻繁に走り、ゲームのパフォーマンスが低下してしまいます。




4. foreach ループの使用

C#の foreach は内部的に イテレーター(IEnumerator) を生成します。このイテレーターは一時的なオブジェクトとしてメモリを消費するため、foreach を多用するとGCの負担が増加 します。

例:foreach を使ったオブジェクトの処理

void Update() {
foreach (GameObject enemy in enemies) {
enemy.transform.Translate(Vector3.forward * Time.deltaTime);
}
}

このようなコードを 毎フレーム実行すると、無駄なメモリ確保が発生し、GCの頻度が高まります。

対策
代わりに for ループを使うことで、余計なメモリの確保を抑えることができます。

for (int i = 0; i < enemies.Count; i++) {
enemies[i].transform.Translate(Vector3.forward * Time.deltaTime);
}

5. System.GC.Collect() を手動で呼び出す

System.GC.Collect() を使うと、強制的にGCを実行 することができます。しかし、ゲームのパフォーマンスを安定させるためには、手動でGCを呼ぶのは推奨されません。

GCは必要なタイミングで自動的に実行されるため、開発者が明示的に呼び出すと予期しないスパイクを引き起こし、ゲームがカクつく原因 になります。


まとめ

GCによるパフォーマンス低下の主な原因をまとめると、次のようになります。

原因説明解決策
Instantiate()Destroy() の多用オブジェクトの生成と破棄の繰り返しでメモリ負荷が増大オブジェクトプール(Object Pooling)を使用する
string の結合 (+ の使用)string は不変なため、結合のたびに新しいメモリを確保StringBuilder を使用する
List<>Dictionary<> のサイズ変更要素の追加・削除時にメモリ確保と解放が発生事前に適切なサイズを確保する (List.Capacity を設定)
foreach の使用IEnumerator の生成でメモリを消費for ループを使う
System.GC.Collect() の手動実行強制GCの実行でフレームレートが低下原則として使用しない

次のセクションでは、GCの影響を最小限に抑える具体的な最適化テクニック を紹介します!




4. GCの最適化テクニック

UnityのGC(ガベージコレクション)が原因でゲームがカクつくのを防ぐためには、できるだけGCを発生させないコードを書くことが重要 です。ここでは、具体的な最適化テクニックを紹介します。


1. Instantiate() & Destroy() を減らし、オブジェクトプールを活用する

Unityでオブジェクトを Instantiate() して Destroy() すると、そのたびにメモリが確保・解放され、GCの負担が増えます。特に弾やエフェクトのように何度も出し入れするオブジェクトは、オブジェクトプール(Object Pooling)を使うのがベスト です。

オブジェクトプールの実装例

public class ObjectPool : MonoBehaviour {
public GameObject prefab;
private Queue<GameObject> pool = new Queue<GameObject>();

public GameObject GetObject() {
if (pool.Count > 0) {
GameObject obj = pool.Dequeue();
obj.SetActive(true);
return obj;
} else {
return Instantiate(prefab);
}
}

public void ReturnObject(GameObject obj) {
obj.SetActive(false);
pool.Enqueue(obj);
}
}

🔹 ポイント

  • Instantiate() の回数を減らし、オブジェクトを再利用することで、GCの発生頻度を抑える。
  • Destroy() せずに SetActive(false) で非表示にすることで、メモリを維持したまま管理。

2. string の結合には StringBuilder を使う

C#の string変更できない(immutable) ため、+ を使って結合すると 新しいメモリが確保 され、GCが発生しやすくなります。

NG例:string の直接結合

string message = "スコア: " + score;

これは、毎回新しい string インスタンスが作られるため、GCの負担が大きくなります。

OK例:StringBuilder を使う

StringBuilder sb = new StringBuilder();
sb.Append("スコア: ");
sb.Append(score);
string message = sb.ToString();

🔹 ポイント

  • StringBuilder はメモリを再利用しながら文字列を結合できるため、GCの発生を抑えられる。

3. リストのサイズ変更を減らす

List<> は動的にサイズ変更が可能ですが、サイズを超えて追加されると、新しいメモリが確保されるためGCの負担が増えます。

NG例:デフォルトの List<> 使用

List<int> numbers = new List<int>();
for (int i = 0; i < 1000; i++) {
numbers.Add(i); // メモリが足りなくなると再確保される
}

OK例:事前に Capacity を設定

List<int> numbers = new List<int>(1000); // 1000個のメモリを最初に確保
for (int i = 0; i < 1000; i++) {
numbers.Add(i);
}

🔹 ポイント

  • List<>Capacity を事前に設定し、メモリの再確保を防ぐ。



4. foreach ではなく for を使う

foreach は内部的に イテレーター(IEnumerator) を生成するため、GCの負担が増える原因 になります。

NG例:foreach を使用

foreach (GameObject obj in objects) {
obj.SetActive(false);
}

OK例:for ループを使用

for (int i = 0; i < objects.Count; i++) {
objects[i].SetActive(false);
}

🔹 ポイント

  • for ループなら余計なメモリを使わない。

5. Update() で不要なメモリ確保を避ける

Update() は毎フレーム呼ばれるため、不要なオブジェクト生成やメモリ確保を行うと、GCの負担が大きくなります。

NG例:毎フレーム新しい配列を作成

void Update() {
int[] tempArray = new int[1000]; // 毎フレーム新しい配列を作成
}

OK例:事前にメモリを確保

private int[] tempArray = new int[1000]; // 使い回せる配列を事前に確保

void Update() {
for (int i = 0; i < tempArray.Length; i++) {
tempArray[i] = 0;
}
}

🔹 ポイント

  • Update() 内で new を使わないようにし、メモリを再利用する

6. System.GC.Collect() を使わない

System.GC.Collect() を手動で呼び出すと、強制的にGCを実行できますが、意図しないタイミングでゲームがカクつく原因 になります。

NG例:強制GCの実行

void Update() {
if (Input.GetKeyDown(KeyCode.Space)) {
System.GC.Collect(); // フレームレート低下の原因
}
}

OK例:GCの発生を抑えるコードを書く

  • オブジェクトプールを使う
  • StringBuilder を活用する
  • List.Capacity を事前に設定する
  • for ループを使う

まとめ

GCによるカクつきを減らすには、できるだけ不要なメモリ確保を避けること が重要です。以下のテクニックを活用しましょう。

最適化ポイント方法
オブジェクトの再利用オブジェクトプール(Object Pooling)を使う
文字列の結合を最適化StringBuilder を使う
リストのサイズ変更を減らすList<>Capacity を事前に設定
foreach の代わりに for を使うイテレーターの生成を避ける
Update() 内でメモリ確保をしないnew を使わず、メモリを事前に確保
System.GC.Collect() を使わないGCの発生を抑えるコードを書く

次のセクションでは、Unityのプロファイラーを使ってGCの影響を可視化する方法 を紹介します!




5. プロファイラーを使ってGCを可視化する

UnityのGC(ガベージコレクション)を最適化するためには、どこでメモリの確保・解放が行われているのかを正確に把握することが重要 です。ここでは、GCの発生を確認し、最適化のヒントを得るためのツールを紹介します。


🛠 Unity Profiler の活用方法

まず、Unityに標準で搭載されているProfilerを使って、GCの影響を確認する方法を紹介します。

  1. Profilerを開く
    Unityのメニューから
    「Window」 → 「Analysis」 → 「Profiler」 を選択します。
  2. GC Alloc のチェック
    「CPU Usage」タブ で「GC Alloc」を選択し、スパイク(急激な処理負荷の増加)が発生しているか確認します。
    GC Alloc の値が大きい 場合は、メモリの確保と解放が頻繁に行われ、GCの発生回数が多いことを示しています。
  3. メモリ使用量を確認する
    「Memory」タブ を開くと、アプリ全体のメモリ使用状況が確認できます。
    特に 「Reserved Heap」や「Used Heap」 の値が急増していないかチェックしましょう。

🔍 Memory Profiler パッケージの活用

標準のProfilerだけでは、細かいメモリの動きが見えないことがあります。
Memory Profiler パッケージ を使うと、どのオブジェクトがどれだけメモリを消費しているかを可視化できるので、メモリリークの原因を特定するのに便利です。

💡 Memory Profilerのインストール方法

  1. Unityの 「Package Manager」 を開く
  2. 「Unity Registry」 から Memory Profiler を検索
  3. 「Install」ボタンをクリック してインストール

📊 より詳細な分析をしたいなら「Profiler Memory Plus」がおすすめ!

「もっと詳細なGCの影響を確認したい!」「標準のProfilerでは足りない!」 という場合は、Profiler Memory Plus というアセットを導入すると、より深くメモリ管理の最適化が可能になります。

🟢 Profiler Memory Plus のメリットリアルタイムのメモリ使用状況を監視
スナップショット機能でGCの影響を比較
どのオブジェクトやスクリプトがメモリを消費しているか特定
GCの発生タイミングを視覚的にチェックできる

例えば、Instantiate()Destroy() の影響を確認したり、string の連結が原因でGC Allocが発生しているかどうかを調べるのに便利です。

🎯 メモリ管理を本格的に最適化したいなら、「Profiler Memory Plus」が強力な味方になります!

👉 Profiler Memory Plusはこちらアセットストアのリンク


このように、GCの最適化はまず「可視化」することが最優先です。Profilerやアセットを活用して、どこで無駄なメモリ確保が行われているのかを特定し、効率的に対策を打ちましょう! 🚀



よくある質問(FAQ)

Q
GCが原因でゲームがカクついているかどうかを調べる方法は?
A

Unity Profilerを使って、GC Alloc や GCの発生回数をチェックする。

Q
System.GC.Collect() を手動で呼び出すのは悪いの?
A

原則としてNG。意図しないタイミングでGCが走り、フレームレートが低下する可能性がある。

Q
オブジェクトプールを使うメリットは?
A

不要な Instantiate()Destroy() の回数を減らし、メモリ確保の負荷を軽減できる。

タイトルとURLをコピーしました