オブジェクトプーリング (Object Pooling) 実装サンプル

概要

オブジェクトプーリングは、ゲーム内で頻繁に生成・破棄されるオブジェクトを事前にインスタンス化(プール化)し、必要時に再利用することでパフォーマンスを向上させる手法です。

メリット

  1. パフォーマンス向上
    • Instantiate()Destroy()を削減し、GC負荷・ヒープアロケーションを抑制
    • フレームレートの安定化やカクつき低減
  2. 再利用性
    • 既存オブジェクトを初期化して再利用可能
    • 弾丸、足跡、エフェクト等、同種オブジェクトが大量に必要な場面で効果的
  3. メモリアロケーションの安定化
    • 初期生成で所定数のオブジェクトを確保しておくことで、実行中のメモリ変動を抑制

適用シナリオ

  • 大量の同種オブジェクト(足跡、弾丸、エフェクト)を頻繁に生成・破棄する場合
  • オブジェクトをシーン中で繰り返し使い回す場合
  • ハイパフォーマンスが要求される大規模なゲームシーン

導入時の考慮点

  1. 初期コスト増
    • 起動・シーン読み込み時に多めのオブジェクトを生成するため、初期処理が増える
  2. コードの複雑化
    • プール管理クラスや返却処理が必要で、コード量や依存関係が増加
  3. メモリ使用量
    • 過剰なプールサイズは未使用オブジェクトをメモリに抱え込み続ける

サンプル実装例

以下は、足跡オブジェクト(FootPrint)をオブジェクトプーリングで管理する簡易例です。

各クラス概要

  • ObjectPoolクラス:
    プール管理を行う基幹クラス。初期生成したオブジェクトはキューに格納し、GetObjectFromPool()で利用可能なオブジェクトを取得。オブジェクトが不足すれば拡張し、ReturnObjectToPool()で使い終わったオブジェクトをキューへ戻すことで再利用を実現します。また、GetSaveData()LoadFromSaveData()でプール内全オブジェクトの状態保存・復元が可能です。
  • FootPrintControllerクラス:
    足跡オブジェクトの挙動を司るクラス。LoadFootPrintData()FootPrintDataを反映して初期化します。オブジェクトプールと連動して、同じインスタンスを異なる足跡として再利用できます。
  • FootPrintLoaderクラス:
    コルーチンを用いて大量の足跡データを非同期的にロードし、プールからオブジェクトを取得して初期化する担当クラスです。HideAllFootprints()で全足跡をプールに返却する機能も提供します。
  • PoolSaveData / ObjectStateDataクラス:
    セーブ・ロード時に使用するデータコンテナ。オブジェクト位置・回転・アクティブ状態などの基本情報を保持し、ObjectPoolからアクセスされます。

オブジェクトプール管理クラス例

概要
オブジェクトプーリングを管理するためのクラスです。
指定した数のオブジェクトを初期生成し、利用可能なオブジェクトをキューで管理します。また、全てのオブジェクトを追跡するためのリストを持たせることで、セーブ・ロードの際に全オブジェクトにアクセスできるようにしています。

using UnityEngine;
using System.Collections.Generic;

public class ObjectPool : MonoBehaviour
{
    [SerializeField] private GameObject prefab;
    [SerializeField] private int poolSize = 50;

    private Queue<GameObject> poolQueue = new Queue<GameObject>();
    private List<GameObject> allPoolObjects = new List<GameObject>(); // 全オブジェクト追跡用

    void Awake()
    {
        // 初期化時に指定数のオブジェクトを生成・プール
        for (int i = 0; i < poolSize; i++)
        {
            GameObject obj = Instantiate(prefab, transform);
            obj.SetActive(false);
            poolQueue.Enqueue(obj);
            allPoolObjects.Add(obj);
        }
    }

    /// <summary>
    /// プールから使用可能なオブジェクトを取得
    /// 利用可能なオブジェクトがない場合は動的に拡張
    /// </summary>
    public GameObject GetObjectFromPool()
    {
        if (poolQueue.Count > 0)
        {
            GameObject obj = poolQueue.Dequeue();
            obj.SetActive(true);
            return obj;
        }
        else
        {
            // プール拡張
            GameObject obj = Instantiate(prefab, transform);
            allPoolObjects.Add(obj);
            return obj;
        }
    }

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

    /// <summary>
    /// 全オブジェクトの状態を保存用データに変換
    /// </summary>
    public PoolSaveData GetSaveData()
    {
        var saveData = new PoolSaveData();
        foreach (var obj in allPoolObjects)
        {
            var data = new ObjectStateData
            {
                position = obj.transform.position,
                rotation = obj.transform.rotation,
                isActive = obj.activeSelf
            };
            // FootPrintDataなど個別オブジェクトのデータもここで収集可能
            saveData.allObjectsData.Add(data);
        }
        return saveData;
    }

    /// <summary>
    /// 保存データからプール状態を復元
    /// </summary>
    public void LoadFromSaveData(PoolSaveData saveData)
    {
        // 必要に応じて拡張や削減などを行う
        for (int i = 0; i < saveData.allObjectsData.Count; i++)
        {
            ObjectStateData data = saveData.allObjectsData[i];
            GameObject obj;
            if (i < allPoolObjects.Count)
            {
                obj = allPoolObjects[i];
            }
            else
            {
                // 足りなければ拡張
                obj = Instantiate(prefab, transform);
                allPoolObjects.Add(obj);
            }

            obj.transform.position = data.position;
            obj.transform.rotation = data.rotation;
            obj.SetActive(data.isActive);
            // FootPrintData等の固有データ復元も必要に応じて実施
        }
    }
}

ポイント解説

  • prefab/poolSize: プール対象オブジェクトと初期生成数をInspector上から設定可能。
  • poolQueue: 利用可能な非アクティブなオブジェクトを先入れ先出しのキューで管理。Dequeue()する度に使えるオブジェクトを取り出せる。
  • allPoolObjects: 全オブジェクトを記録するリスト。セーブ・ロード時に参照するために必要。poolQueueは使用可能オブジェクトのみを扱うが、allPoolObjectsは使用中・非使用中問わず全てを格納する。
  • GetObjectFromPool(): 利用可能なオブジェクトがある場合はキューから取り出し、なければ新規Instantiate()で拡張。
  • ReturnObjectToPool(): 利用終了後、非アクティブ化してキューに戻し、再利用可能な状態にする。
  • GetSaveData() / LoadFromSaveData(): 全オブジェクトの状態を保存・復元するためのメソッド。位置、回転、アクティブ状態などをObjectStateDataとしてまとめる。

PoolSaveData/ObjectStateDataクラス例

概要
セーブ・ロードで必要となるデータコンテナクラスです。ObjectPoolの全オブジェクト状態を格納し、シリアライズ可能な形式にしています。

[System.Serializable]
public class PoolSaveData
{
    public List<ObjectStateData> allObjectsData = new List<ObjectStateData>();
}

[System.Serializable]
public class ObjectStateData
{
    public Vector3 position;
    public Quaternion rotation;
    public bool isActive;
}

ポイント解説

  • System.Serializable属性: Unityでシリアライズ可能となり、JSON等への変換が容易になる。
  • PoolSaveDataは全オブジェクト分のObjectStateDataを保持。
  • ObjectStateDataは1つのオブジェクトについて、位置・回転・アクティブ状態など基本的な状態を記録。

FootPrintControllerクラス例

概要
足跡オブジェクトそのもののコントローラクラスです。LoadFootPrintData()で、与えられたFootPrintDataに従って足跡を初期化します。

public class FootPrintController : MonoBehaviour
{
    public void LoadFootPrintData(FootPrintData data)
    {
        // 足跡オブジェクトの位置・状態などを初期化
        transform.position = data.position;
        // FootPrintDataの他のパラメータがあればここで適用
    }
}

ポイント解説

  • FootPrintDataには位置や足跡固有の情報(方向やテクスチャパターンなど)が入っている想定です。
  • LoadFootPrintData()は再利用時にも呼び出され、同じオブジェクトを別の足跡として再構築できます。

FootPrintLoaderクラス例

概要
ObjectPoolを用いて、足跡を非同期的にロード・配置するサンプルクラスです。
コルーチンを使用して、足跡データリストをもとにオブジェクトを取得・初期化しながら進捗バーを更新します。

public class FootPrintLoader : MonoBehaviour
{
    [SerializeField] private ObjectPool footprintPool;
    [SerializeField] private Slider progressSlider;
    [SerializeField] private GameObject progressBar;

    private List<FootPrintController> loadedFootprints = new List<FootPrintController>();

    public IEnumerator LoadFootprintsAsync(List<FootPrintData> footprintDataList)
    {
        progressBar.SetActive(true);
        progressSlider.value = 0;

        int totalFootprints = footprintDataList.Count;
        int loadedCount = 0;

        foreach (var footprintData in footprintDataList)
        {
            // プールから足跡オブジェクト取得
            GameObject footprintObj = footprintPool.GetObjectFromPool();
            FootPrintController footprint = footprintObj.GetComponent<FootPrintController>();

            // データをロードし、オブジェクトを初期化
            footprint.LoadFootPrintData(footprintData);
            loadedFootprints.Add(footprint);

            loadedCount++;
            // 一定数ごとにフレームを待つことで処理負荷分散し、進捗バーを更新
            if (loadedCount % 20 == 0)
            {
                progressSlider.value = (float)loadedCount / totalFootprints;
                yield return null; // 1フレーム待機
            }
        }

        // 全ロード完了時処理
        progressSlider.value = 1;
        progressBar.SetActive(false);
    }

    /// <summary>
    /// 全ての足跡オブジェクトをプールに戻す
    /// シーン切り替えや再読み込み時に利用
    /// </summary>
    public void HideAllFootprints()
    {
        foreach (var fp in loadedFootprints)
        {
            footprintPool.ReturnObjectToPool(fp.gameObject);
        }
        loadedFootprints.Clear();
    }
}

ポイント解説

  • LoadFootprintsAsync():
    IEnumerableを使ったコルーチンで、非同期的に大量の足跡オブジェクトをロードします。
    一定個数ごとにyield return null;でフレームをまたぐことで、フリーズを防ぎながらプログレスバーを更新。
  • HideAllFootprints():
    読み込み済みの足跡を全てプールへ返却する。シーン移動や再読込みの際に呼び出すと、有効なリソース再利用が可能。

全体を通したポイント

  1. オブジェクトプーリングの基本構造:
    • 初期生成 → キュー管理 → 取得時にDequeue(), 返却時にEnqueue() というシンプルな流れ。
  2. 全オブジェクト管理:
    • キューは「利用可能オブジェクト」のみを管理するため、セーブ・ロードのため全インスタンスへのアクセスにはallPoolObjectsリストを用意。
    • 拡張時に生成したオブジェクトも allPoolObjectsに追加することで、全オブジェクト追跡が常に可能になる。
  3. セーブ・ロード対応:
    • GetSaveData()LoadFromSaveData()で状態を外部化・再構築できるようにしている。
    • ObjectStateDataには位置・回転・アクティブ状態を記録。個別に必要なデータ(FootPrintData等)があれば、そこに拡張可能。
  4. 実用拡張:
    • ここで示したコードは基本的な例。実際のプロジェクトでは、
      • 足跡専用の初期化処理や状態管理処理
      • 最大インスタンス数の制限
      • メモリアロケーションのさらなる最適化
      • 複数種のプレハブ管理
    • など、状況に応じてカスタマイズが必要。

まとめ

  • オブジェクトプーリングは、負荷軽減や安定動作に役立つテクニック。
  • 全オブジェクトの管理・保存・読み出しを行いたい場合、利用可能キューとは別に、全インスタンスを追跡するためのリスト構造を導入すると、セーブ・ロード処理がスムーズになる。
  • 必要に応じて独自のセーブデータ構造を定義し、位置・回転・アクティブ状態などを記録・復元することで、再現性の高い状態管理が可能となる