When discussing performance optimization of Unity games, be it 2D or 3D, object pooling is a commonly referred to technique to gain significant improvements. But what exactly is an object pool, what’s it’s purpose, and how do you use them in your games?

Purpose of Object Pooling

An object pool is essentially a set of pre-instantiated objects that can be returned and recycled when they are no longer in use, and eventually reused as required. The following diagram demonstrates the basic flow:

There are two core issues that the object pooling pattern is intended to solve:

The first is that instantiating GameObjects can result in a big performance hit when you are frequently instantiating them in the Update loop. During Update, where every millisecond is valuable for keeping your frame rate high, you want to keep your logic as minimal and efficient as possible in order to keep the Update time low. Because of this, an object pool can be a great candidate for situations where you need to frequently instantiate a new object on the Update loop. The second issue is that the garbage collector can wreak havoc on your game’s frame rate when it runs, which is increasingly more likely the more allocations you create. By using an object pool, you can pre-allocate all (or at least a good chunk) of the instances of a particular object that you’ll need and simply reuse them when you need another instance.

A common example of a situation that warrants an object pool would be any game where you rapidly instantiate GameObjects, such as when your players can shoot bullets, or a platformer that instantiates platforms for your player to interact with.

Using the shooter example, let’s say you have a Gun class that instantiates instances of a Bullet prefab whenever the user is pressing the spacebar. Your logic could look something like this:

public class Gun : MonoBehaviour { GameObject bulletPrefab; void Update() { if (Input.GetKeyDown("space")) { GameObject bullet = (GameObject) Instantiate(bulletPrefab); // Initialize the bullet as required... } } }

This works fine and can even be good enough for performance depending on your target platform/device and what else is going on in your game, but if you have a lot of bullets being instantiated you can quickly start to see memory usage creeping up and frame rates starting to dip.

This is a prime example of where an object pool can come into play.

Implementation

The following demonstrates a generic ObjectPool class that can be reused for any prefab, and below I’ll explain how it works and demonstrate how to use it. After explaining how it works, we’ll look at how we can refactor the shooter example from above to use an object pool.

First of all, here’s the ObjectPool class:

using UnityEngine; using System.Collections.Generic; public class ObjectPool { private GameObject prefab; private List<GameObject> pool; public ObjectPool(GameObject prefab, int initialSize) { this.prefab = prefab; this.pool = new List<GameObject>(); for (int i = 0; i < initialSize; i++) { AllocateInstance(); } } public GameObject GetInstance() { if (pool.Count == 0) { AllocateInstance(); } int lastIndex = pool.Count - 1; GameObject instance = pool[lastIndex]; pool.RemoveAt(lastIndex); instance.SetActive(true); return instance; } public void ReturnInstance(GameObject instance) { instance.SetActive(false); pool.Add(instance); } protected virtual GameObject AllocateInstance() { GameObject instance = (GameObject) GameObject.Instantiate(prefab); instance.SetActive(false); pool.Add(instance); return instance; } }

Update: Originally in the GetInstance method I was removing the first element in the list (instances[0]), however Reddit user SilentSin26 pointed out that this wastefully requires the list to copy each remaining element down an index. I’ve updated the code above to remove from the end of the list to fix this.

First up, the ObjectPool class is constructed by providing the prefab you want to pool, and the initial size of the pool. If for example you pass 10 as the initial size, the pool will immediately allocate 10 instances of your prefab and add them to the pool.

Next we can see the GetInstance function, which returns an instance of the prefab. This is what you would be calling in the Update loop of the shooter example above to get a bullet. The first thing this function does is check if a new bullet needs to be allocated because the pool is empty - this is something you want to be hit as infrequently as reasonably possible, so it’s helpful to add some counters or logging here and see how many misses your pool is getting while developing. Next it retrieves the first object from the pool, removes it from the pool, activates it, and returns it to the caller.

ReturnInstance is where you return an unused instance of the prefab to be recycled. For instance, in the shooter example above, when your bullet hits a wall and is no longer useful, rather than destroying the bullet you would return it to the pool. This function deactivates the instance and adds it back into the pool.

Finally, AllocateInstance is where the actual instance of the prefab is instantiated. The instance is immediately made inactive so that pooled GameObjects aren’t being updated or displayed in the scene, and is then added to the pool. This function is virtual so that it can be subclassed by a more specific pool, such as a BulletPool in the shooter example, to perform more specific initialization if necessary. For example, if you wanted your instances to be allocated with a random scale, you could do something like this:

public class CustomPool : Object Pool { protected virtual GameObject AllocateInstance() { GameObject instance = base.AllocateInstance(); // Perform custom initialization as required instance.transform.localScale = GetRandomScale(); } }

The idea is to make the ObjectPool generic enough to be used in most cases, but flexible enough to be customized as needed.

Refactoring

Given the shooter example from above, we can refactor the Gun class to use an object pool. We’ll instantiate the pool with an initial size of 20, just as an example, but the initial size of the pool should be enough that you are infrequently having calls to AllocateInstance in the ObjectPool:

public class Gun : MonoBehaviour { GameObject bulletPrefab; ObjectPool bulletPool; void Start() { bulletPool = new ObjectPool(bulletPrefab, 20); } void Update() { if (Input.GetKeyDown("space")) { GameObject bullet = bulletPool.GetInstance(); } } // When to call this depends heavily on your game, but whenever you are // done with the GameObject you should return it to the pool. void ReleaseBullet(GameObject bullet) { bulletPool.ReleaseInstance(bullet); } }

Now whenever we fire we’re reusing an existing instance of the bullet prefab, rather than instantiating a new one. This avoids the excessive instantiations, and prevents unnecessary memory allocations. Finally, whenever we decide the bullet is no longer of use, we can return it by calling ReleaseBullet (or whatever is applicable to your game). When to release your objects depends heavily on the game you’re making and your use of the object pool, but it’s essentially the point at which you would normally destroy or deactivate the object.

If your game has a number of Gun instances, and all of them use the same bullet prefab with the same (or similar) properties, you could even share a single ObjectPool instance among all the Gun instances.

Wrapping Up

The shooter example here is just that, an example. It’s important when adding object pools to your game to investigate and uncover the cases where a pool may be appropriate. The examples above and the ObjectPool class demonstrate only usage of GameObjects and prefabs, but it could really be instances of any object that you want to pool.

It’s also important to note that you will likely want to keep the initial size of your pool reasonable. For example, if you initialize a pool of 100 bullets but only ever have 10 on the screen at a time, you have 90 allocated GameObjects that are sitting idle. In a perfect world you’ll want to tune the initial size to a point where you can hit AllocateInstance as little as possible after the initial allocations, but never have a large pool of idle objects.