【Unity】最適化に必須!オブジェクトプーリングでInstantiateとDestroyを撲滅する

作成: 2025-12-07

ゲームのパフォーマンスを劇的に向上させるための必須テクニック「オブジェクトプーリング」。InstantiateとDestroyがなぜ重いのか、そしてオブジェクトを再利用するプーリングシステムの具体的な実装方法を、クラス図とコードを交えて解説します。

概要

シューティングゲームの弾、敵の群れ、破壊されたときのエフェクト... ゲーム中には、短時間で大量のオブジェクトを生成し、不要になったら破棄する、という処理が頻繁に発生します。多くの初心者は、この処理をInstantiate()(生成)とDestroy()(破棄)メソッドを使って素直に実装します。

しかし、このInstantiate/Destroyの頻繁な呼び出しは、Unityにおける二大パフォーマンスキラーです。これらのメソッドは、メモリの確保(ヒープアロケーション)と解放を伴うため、CPUに大きな負荷をかけるだけでなく、ガベージコレクション(GC)を引き起こし、ゲームに深刻なカクつき(スパイク)をもたらす最大の原因となります。

この問題を解決するための、プロのゲーム開発現場では必須とされているテクニックがオブジェクトプーリング (Object Pooling) です。この記事では、オブジェクトプーリングの概念と、その具体的な実装方法について詳しく解説します。

オブジェクトプーリングとは?

オブジェクトプーリングの考え方は、「一度作ったオブジェクトは捨てずに、使い回す」という非常にシンプルなものです。

  1. 事前生成 (Pre-population): ゲーム開始時など、負荷が問題にならないタイミングで、必要になるであろう数のオブジェクト(例: 弾30個)を予めInstantiate()しておきます。生成したオブジェクトは、すべて非アクティブ状態 (SetActive(false)) にして、「プール」と呼ばれるリストやキューに保管しておきます。
  2. 取得 (Get/Rent): オブジェクトが必要になったとき(例: 弾を発射するとき)、Instantiate()で新しく生成する代わりに、プールから非アクティブなオブジェクトを一つ取り出します。そして、それをアクティブ状態 (SetActive(true)) にし、位置や向きを再設定して使います。
  3. 返却 (Return/Release): オブジェクトが不要になったとき(例: 弾が敵に当たった、画面外に出たとき)、Destroy()で破棄する代わりに、再び非アクティブ状態にしてプールに戻します。これにより、オブジェクトは次の出番を待つことになります。

このサイクルを繰り返すことで、ゲームプレイ中のInstantiate()Destroy()の呼び出しをゼロにすることができ、パフォーマンスを劇的に改善できます。

シンプルなオブジェクトプーリングの実装

それでは、汎用的なオブジェクトプーリングシステムを実装してみましょう。

ステップ1: プール本体のクラスを作成する

まず、特定のPrefabに対応するオブジェクトプールを管理するクラスを作成します。ここでは、Queue(キュー)を使って、非アクティブなオブジェクトを効率的に管理します。

// ObjectPool.cs
using System.Collections.Generic;
using UnityEngine;

public class ObjectPool : MonoBehaviour
{
    public GameObject objectToPool; // プールする対象のPrefab
    public int amountToPool;        // 最初に生成しておく数

    private Queue<GameObject> pooledObjects;

    void Start()
    {
        pooledObjects = new Queue<GameObject>();

        for (int i = 0; i < amountToPool; i++)
        {
            GameObject obj = Instantiate(objectToPool);
            obj.SetActive(false);
            pooledObjects.Enqueue(obj);
        }
    }

    // プールからオブジェクトを取得するメソッド
    public GameObject GetPooledObject()
    {
        // プールに利用可能なオブジェクトがあれば、それを取り出す
        if (pooledObjects.Count > 0)
        {
            GameObject obj = pooledObjects.Dequeue();
            obj.SetActive(true);
            return obj;
        }
        // プールが空の場合(予備が尽きた場合)
        // ここではnullを返すか、あるいは動的に新しいオブジェクトを生成する選択肢もある
        // return Instantiate(objectToPool); // 動的に拡張する場合
        return null;
    }

    // オブジェクトをプールに戻すメソッド
    public void ReturnObjectToPool(GameObject obj)
    {
        obj.SetActive(false);
        pooledObjects.Enqueue(obj);
    }
}

ステップ2: プールを利用するクラスの実装

次に、このObjectPoolを利用して弾を発射するスクリプトを作成します。

// PlayerShooter.cs
using UnityEngine;

public class PlayerShooter : MonoBehaviour
{
    public ObjectPool bulletPool; // Inspectorで弾のプールをセット
    public Transform firePoint;
    public float fireRate = 0.5f;

    private float nextFireTime = 0f;

    void Update()
    {
        if (Input.GetButton("Fire1") && Time.time >= nextFireTime)
        {
            nextFireTime = Time.time + fireRate;
            Fire();
        }
    }

    void Fire()
    {
        // プールから弾を取得
        GameObject bullet = bulletPool.GetPooledObject();
        if (bullet != null)
        {
            bullet.transform.position = firePoint.position;
            bullet.transform.rotation = firePoint.rotation;

            // 弾のスクリプトにプールへの参照を渡すなどして、
            // 弾自身が自分をプールに戻せるようにする
            Bullet bulletScript = bullet.GetComponent<Bullet>();
            if (bulletScript != null)
            {
                bulletScript.SetPool(bulletPool);
            }
        }
    }
}

ステップ3: プールに戻る処理の実装

最後に、弾自身が壁に当たったり、一定時間経過したらプールに戻る処理を実装します。

// Bullet.cs
using UnityEngine;

public class Bullet : MonoBehaviour
{
    private ObjectPool myPool;

    public void SetPool(ObjectPool pool)
    {
        myPool = pool;
    }

    void OnCollisionEnter(Collision collision)
    {
        // 何かに衝突したらプールに戻る
        if (myPool != null)
        {
            myPool.ReturnObjectToPool(gameObject);
        }
        else
        {
            // プールが設定されていない場合のフォールバック
            Destroy(gameObject);
        }
    }
}

より高度なプーリングシステムへ

上記は基本的な実装ですが、実際のプロジェクトではさらに以下のような機能を備えた、より汎用的なプーリングマネージャーを作成することが多いです。

  • 複数の異なるPrefabを一つのマネージャーで管理する機能(Dictionary<string, Queue<GameObject>>などを使用)。
  • プールが空になったときに、動的に新しいオブジェクトを生成してプールを拡張する機能。
  • オブジェクト取得時(Get)や返却時(Return)に、特定の初期化・クリーンアップ処理を呼び出すためのイベントやインターフェース

まとめ

オブジェクトプーリングは、一見すると複雑に見えるかもしれませんが、その原理は単純明快で、得られるパフォーマンス上のメリットは計り知れません。

  • InstantiateDestroyは重い処理であり、GCの原因となる。
  • オブジェクトプーリングは、オブジェクトを事前に生成し、再利用することでこれらの呼び出しを避けるテクニック。
  • 「事前生成」「取得」「返却」がプーリングの3つの基本ステップ。

特にモバイルゲーム開発においては、オブジェクトプーリングは「推奨テクニック」ではなく「必須テクニック」と言っても過言ではありません。ゲームのパフォーマンスに問題を感じたら、まずはInstantiateを多用している箇所がないかを確認し、オブジェクトプーリングの導入を検討してみてください。