オブジェクトプーリング (Object Pooling) 実装サンプル
目次
概要
オブジェクトプーリングは、ゲーム内で頻繁に生成・破棄されるオブジェクトを事前にインスタンス化(プール化)し、必要時に再利用することでパフォーマンスを向上させる手法です。
メリット
- パフォーマンス向上
Instantiate()
とDestroy()
を削減し、GC負荷・ヒープアロケーションを抑制- フレームレートの安定化やカクつき低減
- 再利用性
- 既存オブジェクトを初期化して再利用可能
- 弾丸、足跡、エフェクト等、同種オブジェクトが大量に必要な場面で効果的
- メモリアロケーションの安定化
- 初期生成で所定数のオブジェクトを確保しておくことで、実行中のメモリ変動を抑制
適用シナリオ
- 大量の同種オブジェクト(足跡、弾丸、エフェクト)を頻繁に生成・破棄する場合
- オブジェクトをシーン中で繰り返し使い回す場合
- ハイパフォーマンスが要求される大規模なゲームシーン
導入時の考慮点
- 初期コスト増
- 起動・シーン読み込み時に多めのオブジェクトを生成するため、初期処理が増える
- コードの複雑化
- プール管理クラスや返却処理が必要で、コード量や依存関係が増加
- メモリ使用量
- 過剰なプールサイズは未使用オブジェクトをメモリに抱え込み続ける
サンプル実装例
以下は、足跡オブジェクト(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()
:
読み込み済みの足跡を全てプールへ返却する。シーン移動や再読込みの際に呼び出すと、有効なリソース再利用が可能。
全体を通したポイント
- オブジェクトプーリングの基本構造:
- 初期生成 → キュー管理 → 取得時に
Dequeue()
, 返却時にEnqueue()
というシンプルな流れ。
- 初期生成 → キュー管理 → 取得時に
- 全オブジェクト管理:
- キューは「利用可能オブジェクト」のみを管理するため、セーブ・ロードのため全インスタンスへのアクセスには
allPoolObjects
リストを用意。 - 拡張時に生成したオブジェクトも
allPoolObjects
に追加することで、全オブジェクト追跡が常に可能になる。
- キューは「利用可能オブジェクト」のみを管理するため、セーブ・ロードのため全インスタンスへのアクセスには
- セーブ・ロード対応:
GetSaveData()
とLoadFromSaveData()
で状態を外部化・再構築できるようにしている。- 各
ObjectStateData
には位置・回転・アクティブ状態を記録。個別に必要なデータ(FootPrintData等)があれば、そこに拡張可能。
- 実用拡張:
- ここで示したコードは基本的な例。実際のプロジェクトでは、
- 足跡専用の初期化処理や状態管理処理
- 最大インスタンス数の制限
- メモリアロケーションのさらなる最適化
- 複数種のプレハブ管理
- など、状況に応じてカスタマイズが必要。
- ここで示したコードは基本的な例。実際のプロジェクトでは、
まとめ
- オブジェクトプーリングは、負荷軽減や安定動作に役立つテクニック。
- 全オブジェクトの管理・保存・読み出しを行いたい場合、利用可能キューとは別に、全インスタンスを追跡するためのリスト構造を導入すると、セーブ・ロード処理がスムーズになる。
- 必要に応じて独自のセーブデータ構造を定義し、位置・回転・アクティブ状態などを記録・復元することで、再現性の高い状態管理が可能となる
ディスカッション
コメント一覧
まだ、コメントがありません