Unity Term Book
Advanced Techniques

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

Get()
Pool→ dequeue inactive object → SetActive(true) → returnGame Object
Release()
Game Object→ Reset state → SetActive(false) → enqueue back to poolPool
Pool full?
Release()pool is full (countAll == maxSize) → Destroy(object) or expand the pool🗑 Destroy

Hands-on steps

1

Create an IPoolable interface

OnGet() (reset state when taken from the pool) and OnRelease() (cleanup before returning).

2

Create an ObjectPool

new ObjectPool<T>(onCreate, onGet, onRelease, onDestroy, collectionCheck, defaultCapacity, maxSize) — built-in from 2021+.

3

Use pool.Get() / pool.Release()

Replace Instantiate with pool.Get(), Destroy with pool.Release(). Reset position/velocity/state in onGet.

4

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.

5

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

Basic

A 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

Advanced

UnityEngine.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