← All patterns
Optimization

Object Pooling

Recycle a fixed set of objects instead of creating and destroying them, so spawning never stalls the frame.

What Is It?

Every Instantiate allocates managed and native memory, runs Awake/OnEnable, and every Destroy leaves garbage behind. Do that for twenty bullets a second and the garbage collector eventually wakes up mid-fight and freezes the frame — the dreaded GC spike.

An object pool pre-creates (or lazily creates) instances and keeps the inactive ones on a shelf. "Spawning" becomes pool.Get() — reactivate, reposition, go. "Destroying" becomes pool.Release(obj) — deactivate and put it back. After warm-up, the steady state allocates nothing.

Since Unity 2021 the engine ships UnityEngine.Pool.ObjectPool<T>, which takes four callbacks (create, on-get, on-release, on-destroy) plus a default capacity and max size. A turret, a particle burst, floating damage numbers or enemy waves can all be pooled with a dozen lines.

When Is It Used?

Use it for anything spawned and despawned frequently: projectiles, hit effects, pickups, audio one-shots, UI list items, enemies in wave shooters.

It matters most on mobile and consoles, where GC pauses and instantiate cost are most visible, and for objects with expensive setup (many components, large meshes).

Do not pool objects that are created once per level, or whose state is hard to reset cleanly. A pool that hands out half-reset objects causes bugs that are worse than the allocation it saved.

Interactive Demo

Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.

Code

Assets / Scripts / Optimization/ ObjectPooling ›Turret.cs
using UnityEngine;

namespace Patterns.Optimization.ObjectPooling
{
    /// <summary>
    /// Fires bullets on a timer. It asks the pool for bullets instead of
    /// calling Instantiate, so firing allocates nothing after warm-up.
    /// </summary>
    public class Turret : MonoBehaviour
    {
        [SerializeField] private BulletPool pool;
        [SerializeField] private Transform muzzle;
        [SerializeField] private float fireRate = 8f;
        [SerializeField] private float bulletSpeed = 25f;

        private float cooldown;

        private void Update()
        {
            cooldown -= Time.deltaTime;
            if (cooldown > 0f || !Input.GetButton("Fire1"))
                return;

            cooldown = 1f / fireRate;
            Fire();
        }

        private void Fire()
        {
            Bullet bullet = pool.Get();
            bullet.transform.SetPositionAndRotation(muzzle.position, muzzle.rotation);
            bullet.Launch(muzzle.forward * bulletSpeed);
        }
    }
}
3 files · namespace Patterns.Optimization.ObjectPoolingC# · UTF-8 · LF

Advantages & Disadvantages

+ Advantages

  • Eliminates per-spawn allocation, so the GC stays quiet during gameplay.
  • Spawning is just SetActive(true) — much cheaper than Instantiate.
  • Pre-warming moves the creation cost to a loading screen where nobody notices.

− Disadvantages

  • Pooled objects hold memory even when unused; an oversized pool wastes RAM.
  • Every object must fully reset its state on reuse — velocity, health, trails, coroutines.
  • Releasing the same object twice, or using it after release, causes confusing bugs.

Tips

  1. 01Prefer the built-in ObjectPool<T> with collectionCheck: true in development; it throws on double release.
  2. 02Reset state in the on-get callback (or an OnEnable), not in the object's constructor-like Awake, since Awake only runs once.
  3. 03Clear TrailRenderers with Clear() and stop particle systems when releasing, or ghosts appear on the next spawn.
  4. 04Give the pooled object a reference back to its pool (or an Action<T> release callback) so it can return itself on hit or timeout.
  5. 05Profile with the Memory Profiler and the GC Alloc column in the CPU Profiler to confirm the pool actually removed allocations.