弾を連射すると、撃つたびに一瞬カクつく。敵のラッシュやエフェクトの多い場面でフレームレートが落ちる。Profiler を見るとInstantiateとGC.Collectが上位に——。
Instantiate()(生成)とDestroy()(破棄)は、メモリの確保・解放を伴う重い処理です。しかも破棄されたオブジェクトはガベージになり、GCスパイク まで誘発します。1発ずつなら問題なくても、毎秒何十回と繰り返せば確実にカクつきます。
解決策が オブジェクトプーリング(Object Pooling)——「一度作ったものは捨てずに使い回す」テクニックです。
この記事でわかること
Instantiate/Destroyがなぜ重いのか- プーリングの3ステップ——事前生成・取得・返却
- 自前のシンプル実装(Queue版)で仕組みを理解する
- Unity公 式の
ObjectPool<T>(UnityEngine.Pool・Unity 2021+)の使い方- 実践:ヒットエフェクトを自動返却つきのプールで回す
オブジェクトプーリングとは
考え方はレンタルショップと同じです。商品(オブジェクト)は毎回製造(Instantiate)して廃棄(Destroy)するのではなく、棚に置いておいて貸し出し、使い終わったら返してもらう——このサイクルを回します。

- 事前生成: ゲーム開始時など負荷が問題にならないタイミングで、必要数(例: 弾30個)を
Instantiateし、全部SetActive(false)で眠らせて「プール」に保管する。 - 取得: 弾が必要になったら、
Instantiateする代わりにプールから1つ取り出し、SetActive(true)にして位置・向きをセットし直して使う。 - 返却: 弾が当たった・画面外に出たら、
Destroyする代わりにSetActive(false)でプールに戻す。
これでゲームプレイ中のInstantiate/Destroyはゼロになります。負荷の面で比べると、違いは一目瞭然です。

生成破棄を繰り返す左側は、CPUスパイクに加えてガベージを撒き散らし、あとから GCスパイク という時限爆弾も仕掛けます。プーリングの右側は、最初にまとめて払うコスト以外はほぼフラットです。
自前で実装して仕組みを理解する
まず、仕組みがそのまま見える最小限の実装です。Queueで非アクティブなオブジェクトを管理します。
// ObjectPool.cs
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
public GameObject objectToPool; // プールする対象のPrefab(弾など)
public int amountToPool = 30; // 最初に生成しておく数
private Queue<GameObject> pooledObjects;
void Start()
{
pooledObjects = new Queue<GameObject>();
for (int i = 0; i < amountToPool; i++)
{
GameObject obj = Instantiate(objectToPool);
obj.SetActive(false);
pooledObjects.Enqueue(obj);
}
}
public GameObject GetPooledObject()
{
if (pooledObjects.Count > 0)
{
GameObject obj = pooledObjects.Dequeue();
obj.SetActive(true);
return obj;
}
// 棚が空なら追加生成してプールを拡張する(負荷はかかるが弾切れよりまし)
return Instantiate(objectToPool);
}
public void ReturnObjectToPool(GameObject obj)
{
obj.SetActive(false);
pooledObjects.Enqueue(obj);
}
}
使う側は、Instantiateと書いていた場所をGetPooledObject()に、Destroyと書いていた場所をReturnObjectToPool()に置き換えるだけです。
// PlayerShooter.cs(抜粋)
void Fire()
{
GameObject bullet = bulletPool.GetPooledObject();
bullet.transform.SetPositionAndRotation(firePoint.position, firePoint.rotation);
bullet.GetComponent<Bullet>().SetPool(bulletPool); // 弾が自分で帰れるように
}
// Bullet.cs(抜粋): 当たったら自分でプールに帰る
void OnCollisionEnter(Collision collision)
{
myPool.ReturnObjectToPool(gameObject);
}
公式のObjectPool<T>を使う
Unity 2021以降には、公式のプーリングAPI UnityEngine.Pool が標準搭載されています。自前実装で書いた処理に加えて、上限管理・二重返却チェック・破棄時の後始末まで揃っているため、実戦ではこちらが第一候補です。
コンストラクタの引数が多く見えますが、役割を整理するとレンタルショップの運営マニュアルそのものです。

// BulletSpawner.cs
using UnityEngine;
using UnityEngine.Pool;
public class BulletSpawner : MonoBehaviour
{
public Bullet bulletPrefab;
private ObjectPool<Bullet> pool;
void Awake()
{
pool = new ObjectPool<Bullet>(
createFunc: () => Instantiate(bulletPrefab), // 棚が空のときの生成方法
actionOnGet: bullet => bullet.gameObject.SetActive(true), // 貸し出し時
actionOnRelease: bullet => bullet.gameObject.SetActive(false), // 返却時
actionOnDestroy: bullet => Destroy(bullet.gameObject), // 上限超過分の処分
defaultCapacity: 30,
maxSize: 100 // これ以上は返却されてもDestroyされる
);
}
public void Fire(Vector3 position, Quaternion rotation)
{
Bullet bullet = pool.Get(); // 取得
bullet.transform.SetPositionAndRotation(position, rotation);
bullet.Init(returnAction: () => pool.Release(bullet)); // 返却手段を渡す
}
}
弾側は、寿命が尽きたら渡された返却手段を呼ぶだけです。
// Bullet.cs
using System;
using UnityEngine;
public class Bullet : MonoBehaviour
{
private Action returnToPool;
public void Init(Action returnAction)
{
returnToPool = returnAction;
}
void OnCollisionEnter(Collision collision)
{
returnToPool?.Invoke(); // Destroyの代わりにプールへ帰る
}
}
maxSizeを超えた分は返却時に破棄されるため、「一瞬だけ大量に必要だった」あとにプールが膨れ上がったまま、という事態を防げます。
導入の判断基準は1行です。 「同じPrefabのInstantiateが毎秒何度も走るなら、プールにする」。弾・敵・ヒットエフェクト・ダメージ数字がその代表です。
実践:ヒットエフェクトをプールで回す
弾の次にプール化の効果が大きいのが ヒットエフェクト です。剣戟アクションの斬撃ヒット、弾幕シューティングの着弾の火花、ローグライトで敵を巻き込む範囲攻撃——ジャンルを問わず、エフェクトは「戦闘中ずっと、大量に、短命で」生成されるプーリングの本命です。
ただし弾と違って、エフェクトには「当たったら返却」のようなわかりやすい返却タイミングがありません。そこで 「再生が終わったら自分で棚に帰る」 仕組みを組み込みます。鍵になるのはParticleSystemの Stop Action です。
