Unityのオブジェクトプールは、弾やエフェクトのように何度も出し入れするオブジェクトを、毎回作って消すのではなく、使い終わったら取っておいて使い回す仕組みです。
Unityには、そのための ObjectPool<T> というクラスが UnityEngine.Pool に用意されています。
- 取り出す:pool.Get()(空なら新しく作る)
- 返す:pool.Release(オブジェクト)(Destroyの代わりに呼ぶ)
- 作り方・取り出し時・返却時・破棄時の処理を、コンストラクターに関数で渡す
- 多くの場合、取り出すときにSetActive(true)、返すときにSetActive(false)にする
この記事は、Unity 6.3 LTSのスクリプトリファレンスと公式マニュアル「Pooling and reusing objects」の記載(2026年10月18日に確認)に基づきます。
Unityのエディタでの実機での再現は含まず、掲載したコードもUnityでの実行は確かめていません。
2026年10月26日に、Unity 6.3のObjectPoolとGetの公式リファレンスを再照合しました。
オブジェクトプールを使う理由
公式マニュアルでは、オブジェクトプールは、作成と破棄をくり返すことによる負荷を減らし、メモリの確保と解放の回数を抑えると説明されています。
その結果、ガベージコレクションの負荷とCPUの負荷が下がるとされています。
マニュアルでは、弾のように何度も出てくるが、同時に画面にあるのは少しだけ、というオブジェクトが例に挙がっています。
撃つたびにPrefabからInstantiateするのではなく、最初にプールを作っておき、必要なときに取り出して使い回します。
ObjectPoolの作り方
ObjectPool<T> は、コンストラクターに次の値を渡して作ります。
| 引数 | 役割 |
|---|---|
| createFunc | プールが空のときに、新しいオブジェクトを作る関数 |
| actionOnGet | プールから取り出したときに呼ばれる。準備をして有効にする |
| actionOnRelease | プールに返したときに呼ばれる。後片付けをして無効にする |
| actionOnDestroy | プールが最大数に達していて返せないときに呼ばれる。ここで破棄する |
| collectionCheck | trueにすると、既にプールにある物をもう一度返したときに例外を出す |
| defaultCapacity | 最初に用意する入れ物の大きさ |
| maxSize | プールに置いておける最大の数 |
スクリプトリファレンスでは、defaultCapacityの数だけ最初にオブジェクトが作られるわけではなく、Getを呼んだときに1つずつ作られると説明されています。
maxSizeは待機中の数の上限であり、使用中を含む生成数の上限ではありません。
待機中の数が上限に達した後の返却ではactionOnDestroyが呼ばれます。
GameObjectを破棄したい場合は、この例のようにDestroyする処理を渡します。
Unity 6.3のマニュアルではcollectionCheckはエディタ内のみと説明されています。
ただし、これを二重返却を防ぐ仕組みの代わりにせず、自分のコードでも使用中か返却済みかを管理してください。
公式マニュアルでは、検出のための負荷はあるものの、開発中はtrueにしておくことが勧められています。
弾を使い回す例
次の例は、一定の間隔で弾を出し、2秒たったらプールに返すものです。
公式マニュアルの弾の例と同じく、弾に自分のプールを持たせ、弾が自分で返る形にしています。
弾のスクリプト
using UnityEngine;
using UnityEngine.Pool;
public class Bullet : MonoBehaviour
{
[SerializeField] float speed = 10f;
[SerializeField] float lifeTime = 2f;
IObjectPool<Bullet> pool;
float timer;
// どのプールに返すかを、作ったときに受け取る
public void SetPool(IObjectPool<Bullet> owner)
{
pool = owner;
}
// 取り出すたびに、位置と経過時間を初めの状態に戻す
public void ResetState(Vector3 position, Quaternion rotation)
{
transform.SetPositionAndRotation(position, rotation);
timer = 0f;
}
void Update()
{
transform.Translate(Vector3.forward * speed * Time.deltaTime);
timer += Time.deltaTime;
if (timer >= lifeTime)
{
pool.Release(this); // Destroyの代わりにプールへ返す
}
}
}弾を出すスクリプト
using UnityEngine;
using UnityEngine.Pool;
public class BulletSpawner : MonoBehaviour
{
[SerializeField] Bullet bulletPrefab;
[SerializeField] float interval = 0.2f;
ObjectPool<Bullet> pool;
float nextTime;
void Awake()
{
pool = new ObjectPool<Bullet>(
createFunc: () =>
{
var bullet = Instantiate(bulletPrefab);
bullet.SetPool(pool);
return bullet;
},
actionOnGet: bullet => bullet.gameObject.SetActive(true),
actionOnRelease: bullet => bullet.gameObject.SetActive(false),
actionOnDestroy: bullet => Destroy(bullet.gameObject),
collectionCheck: true,
defaultCapacity: 20,
maxSize: 100);
}
void Update()
{
if (Time.time < nextTime) return;
nextTime = Time.time + interval;
var bullet = pool.Get();
bullet.ResetState(transform.position, transform.rotation);
}
}それぞれBullet.csとBulletSpawner.csとして保存します。
BulletSpawnerを空のゲームオブジェクトに付け、Bulletを付けたPrefabを Bullet Prefab の欄に入れて再生します。
これは単一シーンで生成・移動・時間による返却を学ぶ最小例です。
衝突処理や、Spawnerだけを破棄したときの弾の回収は含みません。
PrefabはProject側のアセットを割り当て、作成用にシーンへ置いたBulletは削除するか無効にしておきます。
この例のBulletはSpawnerから生成されるときにSetPoolを受け取るため、単独で動かすと返却先が未設定です。
まずintervalとlifeTimeを正の値にし、Bullet Prefabが未割り当てでないことを確認してください。
このコードは、スクリプトリファレンスと公式マニュアルの例をもとに書いたもので、Unityでの実行は確かめていません。
返すときに状態を戻す
プールから取り出した物は、前に使ったときの位置や速度などが残っています。
公式マニュアルでは、次に取り出したときに初めて使うときと同じ状態から始まるように、状態を戻すことが大切だと説明されています。
戻す場所は、actionOnGetやactionOnRelease、専用の初期化関数などから選びます。
この例ではGetの後にResetStateを呼ぶため、OnEnableは位置や時間を戻す前に実行されます。
OnEnableで状態を参照する処理を追加する場合は、初期化してからSetActive(true)にする順序へ変更してください。
返すときによく行う後片付けとして、マニュアルでは次のものが挙がっています。
- コルーチンを止める
- イベントの登録を外す
- Rigidbodyの速度などの物理の状態を戻す
- アニメーションを初めの状態に戻す
- パーティクルシステムを止める
- ゲームオブジェクトを無効にして、プールの中でUpdateが呼ばれないようにする
公式マニュアルの弾の例では、返す前にRigidbodyの linearVelocity と angularVelocity を0にしています。
よくある間違い
| 間違い | 起きること・直し方 |
|---|---|
| 返すときにDestroyしている | 使い回せず、毎回作り直しになる。Destroyの代わりにReleaseを呼ぶ |
| 同じ物を2回Releaseしている | collectionCheckがtrueなら、エディタで例外が出る。返した後はその物を使わない |
| 取り出した物の状態を戻していない | 前に使ったときの位置や速度のまま動き出す |
| 返すときに無効にしていない | プールの中でもUpdateが呼ばれ続ける |
プールの状態は、CountActive(使用中の数)、CountInactive(プールで待っている数)、CountAll(合計)で確かめられます。
数が増え続ける場合は、Releaseを呼び忘れている物がないかを見直します。
再利用の確認では、再生中にBulletSpawnerコンポーネントだけを無効にして、新しい発射を止めます。
再生を一時停止せず、出ている弾のlifeTimeが経過するまで待ち、弾がHierarchyから消えず無効状態で残るかを確認します。
続いてBulletSpawnerを有効に戻し、待機していた弾が再び有効になるかを調べてください。
これは掲載コードを確認するための操作案で、今回この手順を実行した結果ではありません。
再利用できても性能向上が保証されるわけではないため、採用時は生成・破棄を繰り返す場合とのCPU時間やメモリーを同条件で比較します。
シーンを切り替えるときの注意
シーンのアンロードでは、そのシーンに属するGameObjectが破棄されます。
ObjectPool自体は通常のC#オブジェクトなので、別のシーンや永続化した管理側に参照を残すと、破棄済みのGameObjectをプールから取り出す問題が起きます。
管理側と弾の寿命をそろえる必要があります。
シーンをまたいでプールを残す場合は、プールを管理するゲームオブジェクトにDontDestroyOnLoadを使い、返すときにSetParentでその下へ戻す方法が案内されています。
使用中の弾は別に追跡し、シーンをアンロードする前に回収するか破棄します。
この例のactionOnReleaseはSetActive(false)を呼ぶため、OnDisableへ無条件のReleaseを追加すると返却処理が再び呼ばれます。
OnDestroyで破棄される弾を返すことも避けてください。
永続化する場合は、使用中の弾も含めて、残す物と破棄する物を明確に管理します。
Clearは待機中の物を取り除き、指定されていればactionOnDestroyを呼びます。
使用中の物は回収しないため、先に発射を止めて使用中の弾を処理し、その後にClearを呼びます。
シーンの読み込み方は、Unityでシーンを切り替える方法で説明しています。
ゲームオブジェクト以外もプールできる
公式マニュアルでは、ListやDictionaryのようなコレクションも、プールして使い回せると説明されています。
UnityEngine.Poolには、List<T>、HashSet<T>、Dictionary<TKey, TValue>のためのプールも用意されています。
毎フレーム new List<T>() をしている処理は、クラスのフィールドに1つ持ってClearで使い回すだけでも、メモリの確保を減らせるとされています。
マニュアルでは、UnityEngine.Poolの機能はスレッドセーフではなく、メインスレッドから呼ぶ必要があると注意書きがあります。
