オブジェクトプーリング (Object Pooling) 概要と簡易サンプル
以下は、Unity におけるオブジェクトプーリング(Object Pooling)の基本的な実装例および解説をまとめた技術資料です。本ガイドでは、オブジェクトの頻繁な生成および破棄によるパフォーマンス低下を回避するための方法論と、最小限のサンプル実装を提示します。
概要
Unity でのオブジェクトプーリングは、使用頻度の高いオブジェクトをあらかじめ一定数生成(インスタンス化)し、必要に応じて再利用する手法です。頻繁な Instantiate
/ Destroy
呼び出しによるガベージコレクション負荷やフレームの遅延を軽減し、ゲーム実行中のパフォーマンスを安定させることが主な目的となります。
実装例
以下は基本的なオブジェクトプーリングのサンプルです。プール管理用スクリプトと、プールを利用してオブジェクトを取り出し・返却する利用側スクリプトの2つで構成します。
スクリプト構成例
- ObjectPooler.cs(プール管理クラス)
- あらかじめ指定数のプレハブを生成してリストに保持
- 利用要求時に未使用(非アクティブ)オブジェクトを返却
- すべて使用中の場合はプールを拡張(新規生成)するなどのロジックを実装可能
- 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.Range
で poolSize
分だけループを回し、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);
}
}
セットアップ手順
- プール管理オブジェクトの作成
- Hierarchy 上で空の GameObject を作成し、"ObjectPooler" 等と命名
ObjectPooler.cs
をアタッチし、prefab
にプール対象プレハブ、poolSize
に初期プール数を設定
- プール利用オブジェクトの作成
- 新たな GameObject を作成し、"PoolUser" 等と命名
PoolUser.cs
をアタッチし、objectPooler
に先ほど作成したオブジェクトプーラーを参照設定
- 実行
- プレイ開始後、
PoolUser
が定期的にプールからオブジェクトを取得・配置し、一定時間後に返却する動作を確認可能
- プレイ開始後、
カスタマイズ例
- プールサイズ拡張制御
現行コードでは、オブジェクト不足時に新規生成する仕様ですが、固定サイズにしたり、最大拡張数を決めるなどの制御が可能です。 - オブジェクト初期化処理
オブジェクト取得時に位置・回転・状態をリセットする処理を追加し、初期化を徹底できます。 - 複数種類のオブジェクト対応
種類毎に別個のプーラーを用意する、または Dictionary を用いてプレハブごとにプールを管理するなど拡張が可能です。
メリット・デメリット
メリット
Instantiate
/Destroy
多用による GC 負荷を軽減- パフォーマンスの安定化
デメリット
- 事前生成による初期メモリ使用量増加
- 不要なオブジェクトがプール内に大量に存在する場合、メモリリソース圧迫の可能性
まとめ
オブジェクトプーリングは、ゲーム中の頻繁なオブジェクト生成・破棄が必要となるシナリオで効果的なパフォーマンス最適化手法です。本ガイドの基本実装例を起点として、プロジェクト特性やチューニングポリシーに応じて拡張・調整することで、より効率的なゲーム開発・運用を実現できます。
ディスカッション
コメント一覧
まだ、コメントがありません