オブジェクトプーリング (Object Pooling) 概要と簡易サンプル

以下は、Unity におけるオブジェクトプーリング(Object Pooling)の基本的な実装例および解説をまとめた技術資料です。本ガイドでは、オブジェクトの頻繁な生成および破棄によるパフォーマンス低下を回避するための方法論と、最小限のサンプル実装を提示します。


概要

Unity でのオブジェクトプーリングは、使用頻度の高いオブジェクトをあらかじめ一定数生成(インスタンス化)し、必要に応じて再利用する手法です。頻繁な Instantiate / Destroy 呼び出しによるガベージコレクション負荷やフレームの遅延を軽減し、ゲーム実行中のパフォーマンスを安定させることが主な目的となります。


実装例

以下は基本的なオブジェクトプーリングのサンプルです。プール管理用スクリプトと、プールを利用してオブジェクトを取り出し・返却する利用側スクリプトの2つで構成します。

スクリプト構成例

  1. ObjectPooler.cs(プール管理クラス)
    • あらかじめ指定数のプレハブを生成してリストに保持
    • 利用要求時に未使用(非アクティブ)オブジェクトを返却
    • すべて使用中の場合はプールを拡張(新規生成)するなどのロジックを実装可能
  2. PoolUser.cs(プール利用側クラス)
    • プールからオブジェクトを取得し、使用後にプールへ返却する流れを実演するサンプル

ObjectPooler.cs(サンプルコード)

using System.Collections.Generic;
using UnityEngine;

public class ObjectPooler : MonoBehaviour
{
    [SerializeField] private GameObject prefab;   // プール対象プレハブ
    [SerializeField] private int poolSize = 10;   // 初期プール数

    private List<GameObject> pool;

    private void Awake()
    {
        pool = new List<GameObject>();

        // 初期プール生成
        for (int i = 0; i < poolSize; i++)
        {
            GameObject obj = Instantiate(prefab);
            obj.SetActive(false);
            pool.Add(obj);
        }
    }

    /// <summary>
    /// 未使用のオブジェクトをプールから取得
    /// 使用中が全て埋まっている場合は新規生成しプールに追加する
    /// </summary>
    public GameObject GetObjectFromPool()
    {
        foreach (var obj in pool)
        {
            if (!obj.activeInHierarchy)
            {
                obj.SetActive(true);
                return obj;
            }
        }

        // すべて使用中だった場合、新規生成(必要に応じて拡張しない実装も可)
        GameObject newObj = Instantiate(prefab);
        pool.Add(newObj);
        return newObj;
    }

    /// <summary>
    /// 使用済みオブジェクトをプールに返却
    /// </summary>
    public void ReturnObjectToPool(GameObject obj)
    {
        obj.SetActive(false);
    }
}

LINQを用いて初期プールの生成を行うことも可能です。ただし、Instantiate でオブジェクトを生成し、その後 SetActive(false) を呼ぶような処理は、LINQ内でのラムダ式内で行う必要があります。以下はその一例です。

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

public class ObjectPooler : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private int poolSize = 10;
    private List<GameObject> pool;

    private void Awake()
    {
        pool = Enumerable.Range(0, poolSize)
                         .Select(_ => {
                             var obj = Instantiate(prefab);
                             obj.SetActive(false);
                             return obj;
                         })
                         .ToList();
    }

    // 取得メソッドやその他処理は従来どおり
}

このように、Enumerable.RangepoolSize 分だけループを回し、Select 内でオブジェクト生成・初期化を行い、最後に ToList()List<GameObject> を構築します。結果として、従来の for ループと同様の処理を、より関数型スタイルの記述で行うことができます。
ただし、LINQを多用することで必ずしもコードがわかりやすくなるとは限らず、パフォーマンス面の影響もわずかながらあります。可読性やチームのコーディングスタイルに合わせて選択してください。

LINQを使用して同等の処理を行うことは可能です。
下記は pool からまだ使用されていないオブジェクトを FirstOrDefault を用いて取得する例です。

public GameObject GetObjectFromPool()
{
    // LINQを使って、非アクティブなオブジェクトを一つ取得
    var obj = pool.FirstOrDefault(o => !o.activeInHierarchy);
    if (obj != null)
    {
        obj.SetActive(true);
        return obj;
    }

    // すべて使用中の場合は新規生成
    var newObj = Instantiate(prefab);
    pool.Add(newObj);
    return newObj;
}

注意点として、using System.Linq; をスクリプトの冒頭で記述する必要があります。LINQを使用することで、コードがより簡潔で読みやすくなりますが、パフォーマンス的には foreach を用いたループと比較して若干のオーバーヘッドが発生する可能性があります。そのため、必要に応じてパフォーマンスと可読性のバランスを考慮してください。

PoolUser.cs(サンプルコード)

using UnityEngine;
using System.Collections;

public class PoolUser : MonoBehaviour
{
    [SerializeField] private ObjectPooler objectPooler;
    [SerializeField] private float spawnInterval = 1f; // スポーン間隔
    private float timer = 0f;

    private void Update()
    {
        timer += Time.deltaTime;
        if (timer >= spawnInterval)
        {
            timer = 0f;
            // プールからオブジェクト取得
            GameObject pooledObj = objectPooler.GetObjectFromPool();
            pooledObj.transform.position = Random.insideUnitSphere * 5f; // 適当な位置へ配置

            // 一定時間後にプールへ返却
            StartCoroutine(ReturnAfterSeconds(pooledObj, 2f));
        }
    }

    private IEnumerator ReturnAfterSeconds(GameObject obj, float seconds)
    {
        yield return new WaitForSeconds(seconds);
        objectPooler.ReturnObjectToPool(obj);
    }
}

セットアップ手順

  1. プール管理オブジェクトの作成
    • Hierarchy 上で空の GameObject を作成し、"ObjectPooler" 等と命名
    • ObjectPooler.cs をアタッチし、prefab にプール対象プレハブ、poolSize に初期プール数を設定
  2. プール利用オブジェクトの作成
    • 新たな GameObject を作成し、"PoolUser" 等と命名
    • PoolUser.cs をアタッチし、objectPooler に先ほど作成したオブジェクトプーラーを参照設定
  3. 実行
    • プレイ開始後、PoolUser が定期的にプールからオブジェクトを取得・配置し、一定時間後に返却する動作を確認可能

カスタマイズ例

  • プールサイズ拡張制御
    現行コードでは、オブジェクト不足時に新規生成する仕様ですが、固定サイズにしたり、最大拡張数を決めるなどの制御が可能です。
  • オブジェクト初期化処理
    オブジェクト取得時に位置・回転・状態をリセットする処理を追加し、初期化を徹底できます。
  • 複数種類のオブジェクト対応
    種類毎に別個のプーラーを用意する、または Dictionary を用いてプレハブごとにプールを管理するなど拡張が可能です。

メリット・デメリット

メリット

  • Instantiate / Destroy 多用による GC 負荷を軽減
  • パフォーマンスの安定化

デメリット

  • 事前生成による初期メモリ使用量増加
  • 不要なオブジェクトがプール内に大量に存在する場合、メモリリソース圧迫の可能性

まとめ

オブジェクトプーリングは、ゲーム中の頻繁なオブジェクト生成・破棄が必要となるシナリオで効果的なパフォーマンス最適化手法です。本ガイドの基本実装例を起点として、プロジェクト特性やチューニングポリシーに応じて拡張・調整することで、より効率的なゲーム開発・運用を実現できます。