Object Pooling
Object Pooling reuses instances instead of Instantiate/Destroy on every spawn — it kills GC spikes and stuttering when firing bullets, FX, or waves of enemies.
Imagine...
Instead of buying a new coffee cup every time → using it → tossing it (Instantiate → use → Destroy), a restaurant uses a dish pool: 20 cups ready, take one, return it to the rack. Never buy extra, never throw away — zero Garbage Collection. In Unity, GC.Collect() is the janitor bursting into the kitchen at rush hour — a dropped frame. Pooling stops that.
The concept in detail
The Instantiate/Destroy problem: each
Instantiate()
allocates a new GameObject, copies component data, and registers it with the scene.
Destroy()
marks memory for GC. A shooter can spawn 30–50 bullets per second — frequent GC, classic
stutter (“sudden lag” every few seconds).
Object Pool pattern: Pre-spawn a fixed count at load. When you need one, take it from the pool (it was deactivated) instead of creating it. When done, disable and return it. Memory stays flat; GC is not triggered.
From Unity 2021+,
UnityEngine.Pool
ships
ObjectPool<T>,
LinkedPool<T>,
GenericPool<T>
— built-in, no hand-rolled queue. Callbacks: onCreate, onGet,
onRelease, onDestroy (when the pool is full).
Typical uses: bullets, hit/explosion FX, floating damage numbers, enemy/NPC spawns, footstep audio sources, UI toasts, particle instances. Skip pooling for objects that spawn <1/minute — management overhead is not worth it.
How a pool works
Hands-on steps
Create an IPoolable interface
OnGet() (reset state when taken from the pool) and OnRelease() (cleanup before returning).
Create an ObjectPool
new ObjectPool<T>(onCreate, onGet, onRelease, onDestroy, collectionCheck, defaultCapacity, maxSize) — built-in from 2021+.
Use pool.Get() / pool.Release()
Replace Instantiate with pool.Get(), Destroy with pool.Release(). Reset position/velocity/state in onGet.
Pre-warm the pool at load
Spawn and immediately release N objects at game start so the first frame does not spike on an empty pool.
Monitor pool.CountAll / pool.CountActive
Log stats to see if the pool is large enough. High countActive/countAll → raise defaultCapacity.
Interactive simulator
Compare Instantiate/Destroy vs Object Pool — press Fire to see the GC difference.
Pool: 5/12
Allocations
0
GC Events
0
Active Objects
0
Code example
BasicA simple bullet pool with Queue — no Unity package required.
using UnityEngine;
using System.Collections.Generic;
public class BulletPool : MonoBehaviour
{
public GameObject bulletPrefab;
public int poolSize = 30;
readonly Queue<GameObject> pool = new Queue<GameObject>();
void Awake()
{
// Pre-warm: create and disable
for (int i = 0; i < poolSize; i++)
{
var go = Instantiate(bulletPrefab);
go.SetActive(false);
go.transform.SetParent(transform);
pool.Enqueue(go);
}
}
public GameObject Get(Vector3 pos, Quaternion rot)
{
var go = pool.Count > 0
? pool.Dequeue()
: Instantiate(bulletPrefab); // Expand if empty
go.transform.SetPositionAndRotation(pos, rot);
go.SetActive(true);
return go;
}
public void Release(GameObject go)
{
go.SetActive(false);
pool.Enqueue(go);
}
}Code example
AdvancedUnityEngine.Pool.ObjectPool<T> (Unity 2021+) — built-in auto-expand and callbacks.
using UnityEngine;
using UnityEngine.Pool;
public class AdvancedBulletPool : MonoBehaviour
{
public Bullet bulletPrefab;
ObjectPool<Bullet> pool;
void Awake()
{
pool = new ObjectPool<Bullet>(
createFunc: () => Instantiate(bulletPrefab, transform),
actionOnGet: b => { b.gameObject.SetActive(true); b.OnGet(); },
actionOnRelease: b => { b.gameObject.SetActive(false); b.OnRelease(); },
actionOnDestroy: b => Destroy(b.gameObject), // When the pool is full
collectionCheck: false, // Skip duplicate checks (production)
defaultCapacity: 20,
maxSize: 50
);
}
public Bullet Spawn(Vector3 pos, Vector3 dir)
{
var bullet = pool.Get();
bullet.transform.position = pos;
bullet.Init(dir, pool); // Bullet calls pool.Release(this) when done
return bullet;
}
public void LogStats()
{
Debug.Log($"Pool: {pool.CountActive} active / {pool.CountAll} total");
}
}📌 Quick recap
- ▸Pool = reuse objects, avoid GC allocation
- ▸Pre-warm at load to avoid a first-frame spike
- ▸UnityEngine.Pool.ObjectPool<T> — built-in from 2021+
- ▸Reset state in onGet, not in Update
- ▸collectionCheck=true only in the Editor for debug
⚠️ Common mistakes
❌ Releasing an object that is already active (double release)
Released twice → pool corrupt → flicker or crash
✅ collectionCheck=true in dev; null-guard before Release()
❌ Not resetting state on Get
The object keeps old velocity, color, health from last use
✅ Fully reset in the actionOnGet callback