Unityでゲームを作っていると、次のような現象に遭遇することがあります。

  • たまに一瞬だけ画面が止まる
  • スマホ実機で動かすとカクつく
  • Profilerを見ると GC.Alloc が出ている
  • Instantiate や Destroy を多用している場面で重くなる
  • Update内の処理は軽そうなのにフレーム落ちする

このような場合、原因の一つとして考えられるのが GC です。

GCはUnityやC#に備わっている自動メモリ管理の仕組みです。便利な一方で、ゲーム中にGCが発生すると、カクつきやフレーム落ちの原因になることがあります。

この記事では、UnityにおけるGCの基本、GC Allocの見方、よくある原因、実装で使える対策を解説します。

この記事でわかること

  • UnityのGCとは何か
  • GC Allocとは何か
  • GCがゲームのカクつきにつながる理由
  • Incremental GCとは何か
  • GCが発生しやすいコード例
  • GCを減らす実装方法
  • ProfilerでGC Allocを確認する方法
  • スマホゲームで注意すべきポイント

UnityのGC(Garbage Collection)とは

GCとは Garbage Collection の略です。
簡単に言うと、不要になったメモリを自動で回収する仕組み です。

UnityのC#では、次のようなコードを書くとメモリが確保されます。

string message = "Hello";
GameObject obj = new GameObject();
List<int> numbers = new List<int>();
int[] scores = new int[100];

これらのうち、もう使われなくなったものは、どこかのタイミングでGCによって回収されます。C#は自動メモリ管理を行う言語なので、C++のように毎回手動でメモリを解放する必要はありません。
ただし、ゲーム開発ではこの便利さがパフォーマンス問題につながることがあります。

GCがゲームで問題になる理由

GC自体は悪いものではありません。
問題は、GCがゲームプレイ中に実行されると、処理が一瞬止まることがある という点です。
ゲームは基本的に、1秒間に何十回も画面を更新しています。
たとえば60FPSなら、1フレームあたりに使える時間は約16.6ミリ秒です。
その中で、入力処理、移動処理、物理演算、描画準備、アニメーション、UI更新などを行う必要があります。
そこにGCの処理が入ると、1フレームの処理時間が伸びます。
その結果、プレイヤーには次のように見えます。

  • 一瞬だけ止まる
  • カメラが引っかかる
  • キャラの動きがカクつく
  • タップやスワイプの反応が遅れる
  • スマホで急にフレーム落ちする

特にスマホゲームでは影響が出やすいです。
PCでは気にならない小さなGCでも、スマホ実機では目立つことがあります。

GC Allocとは

UnityのProfilerを見ると、GC.Alloc や GC Alloc という表示が出てくることがあります。
これは、管理メモリ上に新しくメモリ割り当てが発生した量 を表します。

特に注意すべきなのは、毎フレームGC Allocが発生している状態 です。

たとえば、1フレームあたり1KBの割り当てがあるとします。
60FPSなら、1秒で60KB。1分で約3.6MBの一時メモリが発生します。
1回ごとは小さくても、ゲーム中に積み重なるとGCの原因になります。
そのため、UnityのGC対策では、まず Update内や頻繁に呼ばれる処理でGC Allocを出さないこと が重要です。

Incremental GCとは

Unityには Incremental GC という仕組みがあります。
Incremental GCとは、GC処理を一度にまとめて行うのではなく、複数フレームに分散して実行する仕組みです。
通常のGCでは、GC処理が一気に走ることで、大きな停止時間が発生することがあります。
一方でIncremental GCでは、GC処理を小分けにして行います。

Incremental GCを使うことで、1フレームに大きな負荷が集中しにくくなります。

ただし、ここは重要です。
Incremental GCは、GCの総量を減らす仕組みではありません。
あくまで、GC処理を分散してカクつきを目立ちにくくする仕組みです。

つまり、毎フレーム大量にGC Allocを発生させているコードがあれば、Incremental GCを有効にしていても問題は残ります。
本質的な対策は、そもそも不要なメモリ割り当てを減らすこと です。

UnityのGCで覚えておくべきポイント

UnityのGCについて、覚えておきたいポイントは次の通りです。

UnityのGCは通常の.NETとは少し違う

Unityでは、MonoやIL2CPPのスクリプティング環境上でC#が動作します。
そして、UnityはBoehm GCというGCを使用しています。
通常の.NETのGCとは仕組みが異なる部分があります。

特に重要なのは、UnityのGCは一般的な.NETの世代別GCとは違い、Unity特有の挙動を持つという点です。

そのため、通常のC#アプリと同じ感覚で「GCはそこまで気にしなくていい」と考えるのは危険です。Unityでは、ゲーム中のフレーム時間が重要です。そのため、GCの影響がユーザー体験に直結します。

IL2CPPにしてもGC問題は消えない

iOS向けUnity開発では、基本的にIL2CPPを使うことが多いです。

ここで勘違いしやすいのが、「IL2CPPにすればGC問題は消えるのでは?」という点です。

結論として、IL2CPPにしてもGC対策は必要です。

IL2CPPはC#コードをC++へ変換してビルドする仕組みですが、管理メモリやGCの問題が完全になくなるわけではありません。
MonoでもIL2CPPでも、ゲーム中に不要なメモリ割り当てを大量に発生させれば、GCやメモリ負荷の問題は起きます。

GCが発生しやすいコード例

ここからは、UnityでGC Allocが発生しやすいコード例と改善方法を紹介します。

Update内でnewする

まず避けたいのが、Update内で毎フレーム new することです。

using UnityEngine;

public class BadExample : MonoBehaviour
{
    private void Update()
    {
        Vector3[] positions = new Vector3[100];

        // 毎フレーム新しい配列を作成している
    }
}

このコードでは、Updateが呼ばれるたびに新しい配列を作成しています。

改善する場合は、配列をフィールドとして用意して使い回します。

using UnityEngine;

public class GoodExample : MonoBehaviour
{
    private Vector3[] positions = new Vector3[100];

    private void Update()
    {
        // positionsを使い回す
    }
}

Listを呼び出す度にnewする

Listも同じです。
この処理がたまにしか呼ばれないなら、大きな問題にならない場合もあります。
しかし、Update内や索敵処理で頻繁に呼ばれる場合は、GC Allocの原因になります。

using System.Collections.Generic;
using UnityEngine;

public class EnemySearcher : MonoBehaviour
{
    private void SearchEnemies()
    {
        List<GameObject> enemies = new List<GameObject>();

        // 敵を検索する処理
    }
}

改善する場合は、Listをフィールドとして持ち、Clear() して再利用します。

using System.Collections.Generic;
using UnityEngine;

public class EnemySearcher : MonoBehaviour
{
    private readonly List<GameObject> enemies = new List<GameObject>();

    private void SearchEnemies()
    {
        enemies.Clear();

        // 敵を検索してenemiesに追加する
    }
}

Clear() はListの中身を消すだけです。
List本体を毎回作り直すわけではないため、GC Allocを抑えやすくなります。

UIテキストを毎フレーム更新する

UIの文字列更新もGC Allocの原因になりやすいです。
このコードでは、スコアが変わっていなくても毎フレーム文字列を作成しています。

using TMPro;
using UnityEngine;

public class ScoreView : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI scoreText;

    private int score;

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

改善する場合は、値が変わったときだけUIを更新します。
UI更新で大切なのは、毎フレーム更新するのではなく、変化したときだけ更新すること です。
※Action等で呼び出すのもいいかもしれません。

using TMPro;
using UnityEngine;

public class ScoreView : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI scoreText;

    private int score;
    private int lastScore = -1;

    private void Update()
    {
        if (score == lastScore)
        {
            return;
        }

        scoreText.text = "Score: " + score;
        lastScore = score;
    }
}

InstantiateとDestroyを大量に使う

UnityでGCや負荷の原因になりやすい代表例が、Instantiate と Destroy の多用です。

弾、敵、エフェクト、ダメージ表示、アイテムなどを毎回生成・破棄していると、GCやCPU負荷の原因になります。

using UnityEngine;

public class BulletShooter : MonoBehaviour
{
    [SerializeField] private GameObject bulletPrefab;

    public void Fire()
    {
        GameObject bullet = Instantiate(bulletPrefab);
        Destroy(bullet, 3f);
    }
}

この場合は、オブジェクトプール を使います。
オブジェクトプールとは、あらかじめオブジェクトを生成しておき、必要なときに有効化し、不要になったら無効化して再利用する仕組みです。

Input.touchesよりInput.GetTouchを使う

スマホ向けゲームでは、タッチ入力でも注意が必要です。
Input.touchesは配列を返します。
そのため、使い方によっては不要な割り当てが発生する可能性があります。

using UnityEngine;

public class TouchInput : MonoBehaviour
{
    private void Update()
    {
        for (int i = 0; i < Input.touches.Length; i++)
        {
            Touch touch = Input.touches[i];

            // タッチ処理
        }
    }
}

改善する場合は、Input.touchCount と Input.GetTouch() を使います。
スマホゲームではタッチ処理が毎フレーム走るため、こうした細かい差が積み重なります。

using UnityEngine;

public class TouchInput : MonoBehaviour
{
    private void Update()
    {
        int count = Input.touchCount;

        for (int i = 0; i < count; i++)
        {
            Touch touch = Input.GetTouch(i);

            // タッチ処理
        }
    }
}

LINQをUpdate内で多用しない

LINQは便利ですが、ゲーム中の高頻度処理では注意が必要です。

Where や ToList は読みやすいですが、内部的に一時オブジェクトや新しいListを作ることがあります。Update内で毎フレーム実行するには不向きです。

using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class EnemyManager : MonoBehaviour
{
    private List<Enemy> enemies = new List<Enemy>();

    private void Update()
    {
        List<Enemy> aliveEnemies = enemies
            .Where(enemy => enemy.IsAlive)
            .ToList();
    }
}

改善する場合は、通常のfor文と再利用Listを使います。

using System.Collections.Generic;
using UnityEngine;

public class EnemyManager : MonoBehaviour
{
    private readonly List<Enemy> enemies = new List<Enemy>();
    private readonly List<Enemy> aliveEnemies = new List<Enemy>();

    private void Update()
    {
        aliveEnemies.Clear();

        for (int i = 0; i < enemies.Count; i++)
        {
            if (enemies[i].IsAlive)
            {
                aliveEnemies.Add(enemies[i]);
            }
        }
    }
}

ただし、LINQを完全に禁止する必要はありません。
初期化処理やエディタ拡張ではLINQを使っても問題ない場面は多いです。
重要なのは、ゲームプレイ中のホットパスでは慎重に使うこと です。

foreachは絶対にダメなのか

UnityのGC対策でよくある話に、「foreachは使うな」というものがあります。

これは半分正しく、半分は言い過ぎです。

現代のC#やUnityでは、配列や具体的な List<T> に対する foreach が必ずGC Allocを発生させるとは限りません。

ただし、次のようなケースでは注意が必要です。

  • IEnumerable<T> として扱っている
  • IList<T> などインターフェース経由で回している
  • 独自コレクションをforeachしている
  • LINQと組み合わせている

安全重視で書くなら、パフォーマンスが重要な箇所ではfor文を使うのが無難です。

for (int i = 0; i < enemies.Count; i++)
{
    enemies[i].Move();
}

ただし、すべてのforeachを禁止するとコードの可読性が落ちます。
そのため、使用する判断基準はシンプルです。

ProfilerでGC Allocが出るなら修正する。出ないなら過剰に気にしない。

これで問題ありません。

ProfilerでGC Alloc・GC Collectを確認する方法

GC対策は、勘でやるものではありません。
必ずProfilerで確認します。

UnityでGC Allocを確認する基本手順は次の通りです。

Unity上部メニューから 「Window」 → 「Analysis」 → 「Profiler」

デバッグを実行する → 上部にあるフレームを選択 → Allocationsを確認

見るべきポイントは、毎フレームGC Allocが発生しているかどうか です。
たまに少しだけ発生する程度なら、問題にならないこともあります。
しかし、Update内で毎フレーム発生している場合は、優先的に修正した方がよいです。

もう少し原因を見る場合は、Allocationsの View を選択する。

実機で確認

Unity Editor上で問題がなくても、スマホ実機ではカクつくことがあります。
逆に、Editor上ではGC Allocが出ていても、実機ビルドでは挙動が違うこともあります。
そのため、最終的な判断は必ず実機で行います。

特にiOSやAndroid向けに作る場合は、次の流れがおすすめです。

  1. Development Buildでビルドする
  2. 実機にインストールする
  3. Unity Profilerを接続する
  4. 実際のプレイ中にGC Allocを見る
  5. カクつく場面のフレームを確認する
  6. 原因メソッドを特定する

スマホゲームでは、PCよりもGCの影響が目立ちやすいです。
そのため、Editorだけで最適化判断をしない ことが重要です。

まとめ

UnityのGCは、不要になったメモリを自動で回収する便利な仕組みです。
しかし、ゲーム中にGCが発生すると、カクつきやフレーム落ちの原因になることがあります。特に注意すべきなのは、毎フレーム呼ばれる処理でGC Allocを出している状態です。

UnityでGCを減らすためには、次の対策が有効です。

  • Update内でnewしない
  • Listや配列を使い回す
  • UIテキストは値が変わったときだけ更新する
  • 弾やエフェクトはオブジェクトプール化する
  • LINQを高頻度処理で使わない
  • Debug.LogをUpdate内で出さない
  • 実機Profilerで確認する

Incremental GCは便利ですが、万能ではありません。

GC対策の本質は、GCをうまく発生させることではなく、そもそも不要なメモリ割り当てを減らすこと です。