Menu
CoddyTech

Unity's ObjectPool

Part of the Game Architecture in Unity section of Coddy's Unity journey. Lesson 39 of 50.

Unity ships a pool, ObjectPool<T> in UnityEngine.Pool. You tell it what to do at each step with four lambdas, and it keeps the spares and the counts. This chimney puffs smoke every 0.2 seconds, and each puff goes back after a second:

using UnityEngine;
using UnityEngine.Pool;

public class Chimney : MonoBehaviour
{
    public Puff puffPrefab;
    private ObjectPool<Puff> pool;
    private float timer;

    void Awake()
    {
        pool = new ObjectPool<Puff>(
            createFunc: () => Instantiate(puffPrefab),
            actionOnGet: puff => puff.gameObject.SetActive(true),
            actionOnRelease: puff => puff.gameObject.SetActive(false),
            actionOnDestroy: puff => Destroy(puff.gameObject),
            collectionCheck: true,
            defaultCapacity: 10,
            maxSize: 20);
    }

    void Update()
    {
        timer += Time.deltaTime;
        if (timer >= 0.2f)
        {
            timer = 0f;
            Puff puff = pool.Get();
            puff.transform.position = transform.position + Vector3.up;
            puff.Launch(pool);
        }
    }
}
// Puff.cs
private IObjectPool<Puff> pool;
private float age;

public void Launch(IObjectPool<Puff> owner)
{
    pool = owner;
    age = 0f;
}

void Update()
{
    transform.Translate(0f, Time.deltaTime, 0f);
    age += Time.deltaTime;
    if (age >= 1f) pool.Release(this);
}

createFunc runs when Get finds the pool empty; actionOnGet runs on every Get and actionOnRelease on every Release. When a puff is released while maxSize spares already wait, the pool does not keep it: actionOnDestroy destroys it. defaultCapacity is only the starting size of the pool's list, so nothing is made up front. CountAll counts the puffs the pool has made and still owns, CountActive those out in the game and CountInactive the spares. Get has switched the puff on before the chimney places it, which is fine here because Puff has no OnEnable that reads its position. The puff keeps the pool as an IObjectPool<Puff>, the interface every Unity pool implements, and Launch resets its age on every use.

With collectionCheck on, releasing a puff that is already in the pool throws an InvalidOperationException: Trying to release an object that has already been released to the pool. The check runs only in the Editor, because it searches the spares on every Release; in a build the puff would sit in the pool twice and two Gets would hand out the same object. A common cause is an object that keeps running after its release and releases itself again, which is why actionOnRelease switches it off.

challenge icon

Challenge

Medium

Fireworks. Each press of Space bursts 8 sparks out of the launcher, and each Spark calls the launcher's Return(this) when its 1 second of life is over (Spark is ready). Today the launcher Instantiates and Destroys every spark. Use Unity's pool:

  • In Launcher.cs, make an ObjectPool<Spark> in Awake. It creates a spark by instantiating sparkPrefab, switches a spark on when it is taken out and off when it comes back, and destroys the spark's object when it lets one go.
  • The pool keeps at most 5 spare sparks, so after a burst of 8, 5 wait and 3 are destroyed.
  • Space takes its 8 sparks from the pool instead of instantiating them, and Return releases the spark to the pool instead of destroying it.

Press Play, fire a few bursts, then Check.

Try it yourself

using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Pool;

public class Launcher : MonoBehaviour
{
    public Spark sparkPrefab;
    public int sparks = 8;

    void Awake()
    {
        // Make the pool: create, get, release and destroy, with at most 5 spare sparks
    }

    void Update()
    {
        if (Keyboard.current.spaceKey.wasPressedThisFrame)
        {
            for (int i = 0; i < sparks; i++)
            {
                Spark spark = Instantiate(sparkPrefab);
                spark.transform.position = transform.position + Vector3.up;
                spark.Launch(Direction(i), this);
            }
        }
    }

    // A spark whose life is over
    public void Return(Spark spark)
    {
        Destroy(spark.gameObject);
    }

    // Out and up, spread evenly round the launcher
    Vector3 Direction(int i)
    {
        return Quaternion.Euler(0f, i * 360f / sparks, 0f) * new Vector3(0f, 1f, 3f);
    }
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Game Architecture in Unity