Unity Object Pooling

Object pooling is pretty simple, and I was put off a lot of existing guides and examples by what seemed like unnecessary complexity. Since then my own solution has probably got at least as complicated, but I’ll explain as I go; your mileage my vary.

This guide was initially aimed at a friend of mine who’s probably a much better coder than me (he just wanted the ‘gotchas’ that others might have found already), but I’ll start from the beginning for those of us who aren’t at his level.

Why Pool?

Instantiate and Destroy are relatively expensive operations. They’ll be fine if you’re occasionally adding or removing something, but once you get down to hundreds-of-projectiles-per-second bullet hell and similar scenarios, you need to make sure you’re not calling them for every object.

What To Look Out For

To be honest, I haven’t found any real hidden problems in making and using this (unless you count an early bug whereby attempting to pre-populate items instead ran a loop of cycling them in and out of the pool, so you’d only get one no matter how many you asked for).

Edit: I’ve now fixed a previous issue whereby too many items were created for the pool. I’ll cover the solution next to the relevant code.

The Basic Approach

You need:

  • To disable objects rather than destroying them.
  • A way of keeping track of these disabled objects (a ‘pool’) to find them again later.
  • To recall a disabled copy and reactivate it rather than instantiating a new one.
    • You’ll need a strategy for resetting your objects to their original state.
    • You still need to instantiate a new one if there isn’t one in the pool.

And that’s almost it: implementing that alone could save a lot of processing. You’ll probably also want:

  • To pre-populate the pool with a number of objects while the level loads (or in some other downtime), so that there will be some ready when needed.
  • To make a reusable or generic solution that doesn’t need editing to support a new object type.

This last point is probably what put me off other people’s examples, but the requirement caught up with me eventually.

If that all makes sense to you, you’re probably ready to roll your own, but I’ll keep writing just in case.

How Much Will I Benefit?

I’ve no idea. It depends how expensive your Instantiate/Destroy calls would be (which is probably a function of how much memory the objects take) and how frequently you were recycling them.

All the more reason to make a generic system that you can easily switch into and out of, so you can profile all your options.

Getting It Done

Set-Up

Here’s the top of my PoolManager class, include declaration and initialisation of the pool’s main storage.

public class PoolManager : Singleton<PoolManager> {
    Dictionary<Transform, List<Transform>> dataPool;

    protected PoolManager() {
        dataPool = new Dictionary<Transform, List<Transform>>();
    }

You’ll probably notice a couple of oddities straight away:

  • I’m using Singleton, from the Unify wiki, to manage the initialisation of the manager so that I can easily refer to parts of it statically. You may want to address that issue another way (because Singletons are Bad); the only relevant facts here are:
    • Singleton inherits from MonoBehaviour, and hence so does PoolManager. I’ll be using that later.
    • Singleton ensures there’s exactly one instance, and provides a static member ‘instance’ for accessing it.
  • I like generics. They’re valuable in a case like this, but I hear that they aren’t supported on all target platforms, so depending on where you want the code to run you may need a less strongly typed version.
    • Edit: I found (then lost) a good source on generics on mobile (perhaps particularly iPhone). It’s not that the generics themselves aren’t available, but AOT can affect their use (see iPhone troubleshooting, about halfway down), and generic versions of some of API functions (such as Instantiate) don’t seem to be available; I don’t know whether there’s some reason you can’t make appropriate helpers.

Addressing the Objects

You might also notice in the code above that I’m storing the objects in lists of Transforms, in a dictionary indexed by Transform. Everything you want to Instantiate (and hence everything you’ll want to pool) will come from a prefab, so we’ll use those as the keys in the dictionary; in particular we’ll use their Transforms, since we can be sure that every prefab will have one, and that makes it as good as anything else.

It means that PoolManager methods will return Transform rather than a specific type, but even as enamoured with type safety as I am, trying to index things by type is a pain that I avoid when I can. It also means that client code takes responsibility for getting the component it needs, but I’ll come back to that.

Incidentally, here’s the simple version of Register, which initialises a list in the dictionary:

    public static void Register(Transform prefab) {
        if (!instance.dataPool.ContainsKey(prefab))
            instance.dataPool[prefab] = new List<Transform>();
    }

A previous version was lacking the check: it just reinitialised the list. I’ll come back to the problems this caused.

Depooling An Object

At runtime, you get objects from the pool before you put them back (like that joke about a mathematician watching people leave/enter a building). Since I’m treating prepopulating as optional, the PoolManager must cope with requests in that order. It’s pretty simple.

public static Transform Get(Transform prefab) {
        return Get(prefab, Vector3.zero, Quaternion.identity);
}
public static Transform Get(Transform prefab, Vector3 position, Quaternion rotation) {
    if (prefab == null) {
        throw new ArgumentNullException("Null prefab in depool request.");
    }
    if (!instance.dataPool.ContainsKey(prefab)) {
        Register(prefab);
    }
    if (instance.dataPool[prefab].Count > 0) {
        Transform clone = instance.dataPool[prefab][0];
        instance.dataPool[prefab].Remove(clone);
        clone.position = position;
        clone.rotation = rotation;
        clone.gameObject.SetActive(true);
        return clone;
    } else {
        Transform clone = Instantiate(prefab, position, rotation) as Transform;
        Poolable p = clone.gameObject.AddComponent<Poolable>();
        p.originPrefab = prefab;
        return clone;
    }
}

Some observations:

  • For ease of switching objects in an out of pooling I’m accepting the same arguments as Instantiate, including an overload without position and rotation.
  • The depool request checks if the prefab is registered, and silently registers if not. I um’d and ah’d about this, wondering whether to require explicit registration, but assuming you ever want to allow pooling without pre-population I figure you may as well save yourself the extra line.
  • I’ve got an explicit NullArgumentException for one of the arguments, but not the others. I don’t know why I did that; it’s not like my newly thrown one has any useful extra information in it. I’m not very good with exceptions.

The main operative code is pretty self-explanatory (or so I tell myself, as I gradually slip back into the habit of being a solo coder who forgets to comment things):

  • If there’s a suitable object in the pool, get it, remove it from the list, move it to the right place (saving a few characters because we’re referring to it by Transform), activate it and return it.
  • If there isn’t, Instantiate a new one, add a Poolable component and store the prefab on it (see below) and return it.

Poolable looks like this:

using UnityEngine;

public class Poolable : PauseableMonoBehaviour {
    [HideInInspector]
    public Transform originPrefab;
}

Nothing more than defining a variable that I’ll need later (and exposing it to code but not the inspector).

Telling the Object it’s Active

Your version of this code might include telling the poolable object that it’s being activated (and you may not want to do so in OnEnable, in case you deactivate objects for any other reason). Apparently the asset store solutions to this problem often implement an OnSpawn message for this.

In my current project (and others I can foresee) my objects are typically quite heavily parameterised, in order to reduce the number of mostly-duplicated prefabs. For example, I might wake up a pooled bullet only for it to have different damage parameters to last time, or a different colour trail. This means that my initialisation call has a signature particular to the object being activated, so I made the client code responsible for calling that, straight after depooling.

If you do decide that you want PoolManager to call a common initialiser, I recommend that you consider giving your poolable items an interface or a shared base class, rather than using SendMessage (which uses reflection, matches things by string comparison, and is slower than a direct call). You could put the poolable prefab reference into the base class and save the extra component.

Repooling an Object

Repooling is mostly the reverse, as you’d expect.

    public static void Repool(Transform obj) {
        Poolable p = obj.GetComponent<Poolable>();
        instance.dataPool[p.originPrefab].Add(obj);
        obj.gameObject.SetActive(false);
        obj.transform.SetParent(instance.transform, false);
    }

Here you can see why I needed the Poolable component: I’m indexing by object instance rather than type, so without a reference to the original prefab I can’t tell which list to put the object back into.

Besides the obvious bits (add the object to its list, deactivate it) I’m also reparenting it to the PoolManager’s gameobject (you’ll recall that PoolManager is a Component, via Singleton<PoolManager>). This is just to keep the scene tidy. Client code could move objects to wherever they’re needed, although mine tends to leave the new ones in the root and the pooled ones in the pool, so I can easily watch how many new ones are being spawned.

I’m including the optional extra false in my SetParent calls so that local position/scale isn’t updated to preserve world position/scale. I do this because I find that when moving UI objects in an out of their canvases (since instantiated ones will always start outside of their canvas) you need to preserve their local scale or they’ll ignore their CanvasScaler, which is very bad news.

Pre-populating

    public static void Register(Transform prefab, int stockLevel) {
        Register(prefab);
        while (instance.dataPool[prefab].Count < stockLevel) {
            Transform clone = Instantiate(prefab) as Transform;
            Poolable p = clone.gameObject.AddComponent<Poolable>();
            p.originPrefab = prefab;
            instance.dataPool[prefab].Add(clone);
            clone.gameObject.SetActive(false);
            clone.transform.SetParent(instance.transform, false);
        }
    }

This is the other Register. While stock count is less than that specified, Instantiate a new one and immediately repool it. Looking at it again now, a lot of these lines are similar to ones in Repool, so perhaps there’s scope for making it simpler and more maintainable than this.

You might instead have a pre-populater that took the number of instances to create, but I figured that I wanted other entities to set the minimum pool size, rather than individually reserving specific instances (or thereabouts). Your mileage…

Here’s where the problem lies with the previous version of Register(), which failed to check for an existing list before it installed a new one. In the implicit use during Get() it’s fine, because it’s wrapped in the same check (and perhaps I should take out the outer check), but the prepopulation Register(int) is intended to be called multiple times (e.g. each ship registers its own explosion, even though many share prefabs). In this case each call overwrote the benefit of the last one, demanding a fresh list and pre-populating it with its own stock; the calls become more expensive (because I was creating far more objects than required) for no extra benefit, and worse the orphaned clones are never Destroy()ed, generating redundant inactive objects and a memory leak.

Fun times.

Using the PoolManager

Here’s an example of the client code:

Transform clone = PoolManager.Get(item.projPrefab,
    firingPositionTransform.position + direction * Vector3.forward * owner.config.bulletSpace,
    firingPositionTransform.rotation);
clone.gameObject.GetComponent<Bullet>().Init(this, direction);

As I mentioned before, this code is responsible for reinitialising the object, including finding the relevant component(s). If you don’t need initialisation parameterised (or you want to build a hierarchy of parameter stores) then you could move initialisation into the pooler, but I’m getting on OK like this.

Repooling is simpler:

PoolManager.Repool(gameObject.transform);

The only thing to remember is that Repool accepts a Transform rather than a GameObject, although if you were really worried about that it would be easy to adapt it to the same signature as Destroy.

Conclusion

So there you go, a versatile pooling solution that seems to work.

I’ve having a hard time with my code plugin so I won’t post the whole files; let me know if you’d like a copy. All the code in this page can be reused on a CC0 no-rights-reserved basis, as is, without warranty.