The previous pooling solution is useful for typical classes, but it won't work for special Unity objects, such as GameObject and MonoBehaviour. These objects tend to consume a large chunk of our runtime memory, can cost us a great deal of CPU usage when they're created and destroyed, and tend to risk a large amount of garbage collection at runtime. In other words, the main goal of Prefab pooling is to push the overwhelming majority of object instantiation to Scene initialization, rather than letting them get created at runtime. This can provide some big runtime CPU savings, and avoids a lot of spikes caused by object creation/destruction and garbage collection, at the expense of Scene loading times, and runtime memory consumption. As a result, there are quite a few pooling solutions available on the Asset Store for handling this task, with varying degrees of simplicity, quality, and feature sets.
However, creating a pooling solution is an interesting topic, and building one from scratch is a great way of getting to grips with a lot of important internal Unity Engine behavior. Also, knowing how such a system is built makes it easier to extend if we wish it to meet the needs of our particular game, rather than relying on a prebuilt solution.
The general idea of Prefab pooling is to create a system that contains lists of active and inactive GameObjects that were all instantiated from the same Prefab reference. The following diagram shows how the system might look after several spawns, despawns, and respawns of various objects derived from four different Prefabs (Orc, Troll, Ogre, and Dragon):
In this example, several instances of each Prefab were instantiated (11 Orcs, 8 Trolls, 5 Ogres, and 1 Dragon). Currently only eleven of these objects are active, while the other fourteen have previously been despawned, and are inactive. Note that the despawned objects still exist in memory, although they are not visible and cannot interact with the game world until they have been respawned. Naturally, this costs us a constant amount of heap memory at runtime in order to maintain the inactive objects, but when a new object is instantiated, we can reuse one of the existing inactive objects, rather than allocating more memory in order to satisfy the request. This saves significant runtime CPU costs during object creation and destruction, and avoids garbage collection.
The following diagram shows the chain of events that needs to occur when a new Orc is spawned:
The first object in the Inactive Orc pool (Orc7) is reactivated and moved into the Active pool. We now have 6 active Orcs, and 5 inactive Orcs.
The following figure shows the order of events when an Ogre
object is despawned:
This time the object is deactivated and moved from the Active pool into the Inactive pool, leaving us with 1 active Ogre and 4 inactive Ogres.
Finally, the following diagram shows what happens when a new object is spawned, but there are no inactive objects to satisfy the request:
In this scenario, more memory must be allocated to instantiate the new Dragon
object, since there are no Dragon
objects in its Inactive pool to reuse. Therefore, in order to avoid runtime memory allocations for our GameObjects, it is critical that we know beforehand how many we will need. This will vary depending on the type of object in question, and requires occasional testing and debugging to ensure we have a sensible number of each Prefab instantiated at runtime.
With all of this in mind, let's create a pooling system for Prefabs!
Let's first define an interface for a Poolable Component:
public interface IPoolableComponent { void Spawned(); void Despawned(); }
The approach for IPoolableComponent
will be very different from the approach taken for IPoolableObject
. The objects being created this time are GameObjects, which are a lot trickier to work with than standard objects because of how much of their runtime behavior is already handled through the Unity Engine, and how little access we have to it.
GameObjects do not give us access to an equivalent New()
method that we can invoke any time the object is created, and we cannot derive from the GameObject
class in order to implement one. GameObjects are created either by placing them in a Scene, or by instantiating them at runtime through GameObject.Instantiate()
, and the only inputs we can apply are an initial position and rotation. Of course, their Components have an Awake()
method we can define, which is invoked the first time the Component is brought to life, but this is merely a compositional object—it's not the actual parent object we're spawning and despawning.
So, because we only have control over a GameObject class's Components, it is assumed that the IPoolableComponent
interface is implemented by at least one of the Components that is attached to the GameObject we wish to pool.
The Spawned()
method should be invoked on every implementing Component each time the pooled GameObject is respawned, while the Despawned()
method gets invoked whenever it is despawned. This gives us entry points to control the data variables and behavior during the creation and destruction of the parent GameObject.
The act of despawning a GameObject is trivial; turn its active flag to false
(through SetActive()
). This disables the Collider and Rigidbody for physics calculations, removes it from the list of renderable objects, and essentially takes care of disabling interactions with all built-in Unity Engine subsystems in a single stroke. The only exception is any Coroutines that are currently invoking on the object, since as we learned earlier in Chapter 2, Scripting Strategies, Coroutines are invoked independently of Update()
and GameObject activity. We will therefore need to call StopCoroutine()
, or StopAllCoroutines()
during the despawning of such objects.
In addition, Components typically hook into our own custom gameplay subsystems as well, and so the Despawn()
method gives our Components the opportunity to take care of any custom cleanup before shutting down. For example, we would probably want to use Despawn()
to deregister the Component from the Messaging System we defined back in Chapter 2, Scripting Strategies.
Unfortunately, successfully respawning the GameObject is a lot more complicated. When we respawn an object, there will be many settings that were left behind when the object was previously active, and these must be reset in order to avoid conflicting behaviors. A common problem with this is Rigidbody velocity. If this value is not explicitly reset before the object is reactivated, then the newly respawned object will continue moving with the same velocity the old version had when it was despawned.
This problem becomes further complicated by the fact that built-in Components are sealed
, and therefore cannot be derived from. So, to avoid these issues, we can create a custom Component that resets the attached Rigidbody whenever the object is despawned:
public class ResetPooledRigidbodyComponent : MonoBehaviour, IPoolableComponent { Rigidbody _body; public void Spawned() { } public void Despawned() { if (_body == null) { _body = GetComponent<Rigidbody>(); if (_body == null) { // no Rigidbody! return; } } _body.velocity = Vector3.zero; _body.angularVelocity = Vector3.zero; } }
Note that the best place to perform this task is during despawning, because we cannot be certain in what order the GameObject class's IPoolableComponent
interfaces will have their Spawned()
methods invoked. It is unlikely that another IPoolableComponent
will change the object's velocity during despawning, but it is possible that a different IPoolableComponent
attached to the same object might want to set the Rigidbody's initial velocity to some important value during its own Spawned()
method. Ergo, performing the velocity reset during the ResetPooledRigidbodyComponent
class's Spawned()
method could potentially conflict with other Components and cause some very confusing bugs.
In fact, creating Poolable Components that are not self-contained, and tend to tinker with other Components like this, is one of the biggest dangers of implementing a pooling system. We should minimize such implementations, and routinely verify them when we're trying to debug strange issues in our game.
For the sake of illustration, here is the definition of a simple Poolable Component that replaces the TestMessageListener
class we defined back in Chapter 2, Scripting Strategies. This Component automatically handles some basic tasks every time the object is spawned and despawned:
public class PoolableTestMessageListener : MonoBehaviour, IPoolableComponent { public void Spawned() { MessagingSystem.Instance.AttachListener (typeof(MyCustomMessage), this.HandleMyCustomMessage); } bool HandleMyCustomMessage(BaseMessage msg) { MyCustomMessage castMsg = msg as MyCustomMessage; Debug.Log (string.Format("Got the message! {0}, {1}", castMsg._intValue, castMsg._floatValue)); return true; } public void Despawned() { if (MessagingSystem.IsAlive) { MessagingSystem.Instance.DetachListener(typeof(MyCustomMessage), this.HandleMyCustomMessage); } } }
Hopefully, we now have an understanding of what we need from our pooling system, so all that's left is to implement it. The requirements are as follows:
Spawned()
method should be invoked on all IPoolableComponent
interfaces attached to the GameObjectDespawned()
method on all IPoolableComponent
interfaces attached to the GameObjectThe requirements are fairly straightforward, but the implementation requires some investigation if we wish to make the solution performance-friendly. Firstly, a typical Singleton would be a good choice for the main entry point, since we want this system to be globally accessible from anywhere:
public static class PrefabPoolingSystem { }
The main task for object spawning involves accepting a Prefab reference, and figuring if we have any despawned GameObjects that were originally instantiated from the same reference. To do this, we will essentially want our pooling system to keep track of two different lists for any given Prefab reference: a list of active (spawned) GameObjects, and a list of inactive (despawned) objects that were instantiated from it. This data would be best abstracted into a separate class, which we will name PrefabPool
.
In order to maximize the performance of this system (and hence make the largest gains possible, relative to just allocating and deallocating objects from memory all of the time), we will want to use some fast data structures in order to acquire the corresponding PrefabPool
objects whenever a spawn or despawn request comes in.
Because spawning involves being given a Prefab, we will want one data structure that can quickly map Prefabs to the PrefabPool
that manages them. And because despawning involves being given a GameObject, we will want another data structure that can quickly map spawned GameObjects to the PrefabPool
that originally spawned them. A Dictionary is a good choice for both of these needs.
Let's define these maps in our pooling system:
public static class PrefabPoolingSystem { static Dictionary<GameObject,PrefabPool> _prefabToPoolMap = new Dictionary<GameObject,PrefabPool>(); static Dictionary<GameObject,PrefabPool> _goToPoolMap = new Dictionary<GameObject,PrefabPool>(); }
Next we'll define what happens when we spawn an object:
public static GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation) { if (!_prefabToPoolMap.ContainsKey (prefab)) { _prefabToPoolMap.Add (prefab, new PrefabPool()); } PrefabPool pool = _prefabToPoolMap[prefab]; GameObject go = pool.Spawn(prefab, position, rotation); _goToPoolMap.Add (go, pool); return go; }
The Spawn()
method will be given a Prefab reference, an initial position, and an initial rotation. We need to figure out which PrefabPool
the Prefab belongs to (if any), ask it to spawn a new GameObject using the data provided, and then return the spawned object to the requestor. We first check our "Prefab-to-Pool" map, to see if a pool already exists for this Prefab. If not, we quickly create one. In either case, we then ask the PrefabPool
to spawn us a new object. The PrefabPool
will either end up respawning an object that was despawned earlier, or instantiate a new one (if there aren't any inactive instances left).
Either way, this class doesn't particularly care. It just wants the instance generated by the PrefabPool
class so that it can be entered into the "GameObject-to-Pool" map and returned to the requestor.
For convenience, we can also define an overload which places the object at the world's center (useful for GameObjects that aren't visible, and just need to exist somewhere):
public static GameObject Spawn(GameObject prefab) { return Spawn (prefab, Vector3.zero, Quaternion.identity); }
Despawning involves being given a GameObject, and then figuring out which PrefabPool
is managing it. This could be achieved by iterating through our PrefabPool
classes and checking if they contain the given GameObject
. But if we end up generating a lot of PrefabPools
, then this iterative process can take a while. We will always end up with as many PrefabPool
classes as we have Prefabs (at least so long as we manage all of them through the pooling system). Most projects tend to have dozens, hundreds, if not thousands of different Prefabs.
So, the GameObject-to-Pool map is maintained to ensure that we always have rapid access to the PrefabPool
that originally spawned the object. It can also be used to quickly verify if the given GameObject is even managed by the pooling system to begin with. Here is the method definition for the despawning method, which takes care of these tasks:
public static bool Despawn(GameObject obj) { if (!_goToPoolMap.ContainsKey(obj)) { Debug.LogError (string.Format ("Object {0} not managed by pool system!", obj.name)); return false; } PrefabPool pool = _goToPoolMap[obj]; if (pool.Despawn (obj)) { _goToPoolMap.Remove (obj); return true; } return false; }
As a result, thanks to the two maps we're maintaining, we can quickly access the PrefabPool
that manages the given reference, and this solution will scale for any number of Prefab that the system manages.
Now that we have a system that can handle multiple Prefab pools automatically, the only thing left is to define the behavior of the pools. As mentioned previously, we will want the PrefabPool
class to maintain two data structures: one for active (spawned) objects that have been instantiated from the given Prefab and another for inactive (despawned) objects.
Technically, the PrefabPoolingSystem
class already maintains a map of which Prefab is governed by which PrefabPool
, so we can actually save a little memory by making the PrefabPool
a slave to the PrefabPoolingSystem
class, by not having it keep track of which Prefab it is managing. Consequently, the two data structures are the only member variables the PrefabPool
needs to keep track of.
However, for each spawned GameObject, it must also maintain a list of all of its IPoolableComponent
references in order to invoke the Spawned()
and Despawned()
methods on them. Acquiring these references can be a costly operation to perform at runtime, so it would be best to cache the data in a simple struct:
public struct PoolablePrefabData { public GameObject go; public IPoolableComponent[] poolableComponents; }
This struct will contain a reference to the GameObject
, and the precached list of its IPoolableComponents
.
Now we can define the member data of our PrefabPool
class:
public class PrefabPool { Dictionary<GameObject,PoolablePrefabData> _activeList = new Dictionary<GameObject,PoolablePrefabData>(); Queue<PoolablePrefabData> _inactiveList = new Queue<PoolablePrefabData>(); }
The data structure for the active list should be a dictionary in order to do a quick lookup for the corresponding PoolablePrefabData
from any given GameObject
reference. This will be useful during object despawning.
Meanwhile, the inactive data structure is defined as a Queue, but it will work equally well as a List, a Stack, or really any data structure that needs to regularly expand or contract, and where we only need to pop items from one end of the list, since it does not matter which object it is. It only matters that we retrieve one of them. A Queue is useful in this case because we can both retrieve and remove the object from the data structure in a single call.
Let's define what it means to spawn a GameObject in the context of our pooling system: at some point, PrefabPool
will get a request to spawn a GameObject from a given Prefab, at a particular position and rotation. The first thing we should check is whether or not we have any inactive instances of the Prefab. If so, then we can pop the next available one from the Queue and respawn it. If not, then we need to instantiate a new GameObject
from the Prefab using GameObject.Instantiate()
. At this moment, we should also create a PoolablePrefabData
object to store the GameObject reference, and acquire the list of all IPoolableComponents
that are attached to it.
Either way, we can now activate the GameObject, set its position and rotation, and call the Spawned()
method on all of its IPoolableComponents
. Once the object has been respawned, we can add it to the list of active objects and return it to the requestor.
Here is the definition of the Spawn()
method that defines this behavior:
public GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation) { PoolablePrefabData data; if (_inactiveList.Count > 0) { data = _inactiveList.Dequeue(); } else { // instantiate a new object GameObject newGO = GameObject.Instantiate(prefab, position, rotation) as GameObject; data = new PoolablePrefabData(); data.go = newGO; data.poolableComponents = newGO.GetComponents<IPoolableComponent>(); } data.go.SetActive (true); data.go.transform.position = position; data.go.transform.rotation = rotation; for(int i = 0; i < data.poolableComponents.Length; ++i) { data.poolableComponents[i].Spawned (); } _activeList.Add (data.go, data); return data.go; }
Because we are using GameObject.Instantiate()
whenever the pool has run out of despawned instances, this system does not completely rid us of runtime object instantiation and hence, heap memory allocation. It's important to prespawn the expected number of instances that we will need during the lifetime of the current Scene, so that we don't need to instantiate more during runtime.
It would be wasteful to prespawn 100 explosion particle effects, if the most we will ever expect to see in the Scene at any given time is three or four. Conversely, spawning too few instances will cause excessive runtime memory allocations, and the goal of this system is to push the majority of allocation to the start of a Scene's lifetime. We need to be careful about how many instances we maintain in memory so that we don't waste more memory space than necessary.
Let's define a method in our PrefabPoolingSystem
class that we can use to quickly prespawn a given number of objects from a Prefab. This essentially involves spawning N objects, and then immediately despawning them all:
public static void Prespawn(GameObject prefab, int numToSpawn) { List<GameObject> spawnedObjects = new List<GameObject>(); for(int i = 0; i < numToSpawn; i++) { spawnedObjects.Add (Spawn (prefab)); } for(int i = 0; i < numToSpawn; i++) { Despawn(spawnedObjects[i]); } spawnedObjects.Clear (); }
We would use this method during Scene initialization, to prespawn a collection of objects to use in the level. For example:
public class OrcPreSpawner : MonoBehaviour [SerializeField] GameObject _orcPrefab; [SerializeField] int _numToSpawn = 20; void Start() { PrefabPoolingSystem.Prespawn(_orcPrefab, _numToSpawn); } }
Finally, there is the act of despawning the objects. As mentioned previously, this primarily involves deactivating the object, but we also need to take care of various bookkeeping tasks and invoking Despawned()
on all of its IPoolableComponent
references.
Here is the method definition for the PrefabPool
class's Despawn()
method:
public bool Despawn(GameObject objToDespawn) { if (!_activeList.ContainsKey(objToDespawn)) { Debug.LogError ("This Object is not managed by this object pool!"); return false; } PoolablePrefabData data = _activeList[objToDespawn]; for(int i = 0; i < data.poolableComponents.Length; ++i) { data.poolableComponents[i].Despawned (); } data.go.SetActive (false); _activeList.Remove (objToDespawn); _inactiveList.Enqueue(data); return true; }
First we verify the object is being managed by the pool, and then we grab the corresponding PoolablePrefabData
in order to access the list of IPoolableComponent
references. Once Despawned()
has been invoked on all of them, we deactivate the object, remove it from the active list, and push it into the inactive queue so that it can be respawned later.
The following class definition allows us to perform a simple hands-on test with the PrefabPoolingSystem
class. It will support three Prefabs, and prespawn five instances during application initialization. We can press the 1, 2, or 3 keys to spawn an instance of each type, and then press Q, W, or E to despawn a random instance of each type.
public class PoolTester : MonoBehaviour { [SerializeField] GameObject _prefab1; [SerializeField] GameObject _prefab2; [SerializeField] GameObject _prefab3; List<GameObject> _go1 = new List<GameObject>(); List<GameObject> _go2 = new List<GameObject>(); List<GameObject> _go3 = new List<GameObject>(); void Start() { PrefabPoolSystem_AsSingleton.Prespawn(_prefab1, 5); PrefabPoolSystem_AsSingleton.Prespawn(_prefab2, 5); PrefabPoolSystem_AsSingleton.Prespawn(_prefab3, 5); } void Update () { if (Input.GetKeyDown(KeyCode.Alpha1)) {SpawnObject(_prefab1, _go1);} if (Input.GetKeyDown(KeyCode.Alpha2)) {SpawnObject(_prefab2, _go2);} if (Input.GetKeyDown(KeyCode.Alpha3)) {SpawnObject(_prefab3, _go3);} if (Input.GetKeyDown(KeyCode.Q)) { DespawnRandomObject (_go1); } if (Input.GetKeyDown(KeyCode.W)) { DespawnRandomObject (_go2); } if (Input.GetKeyDown(KeyCode.E)) { DespawnRandomObject (_go3); } } void SpawnObject(GameObject prefab, List<GameObject> list) { GameObject obj = PrefabPoolingSystem.Spawn (prefab, Random.insideUnitSphere * 8f, Quaternion.identity); list.Add (obj); } void DespawnRandomObject(List<GameObject> list) { if (list.Count == 0) { // Nothing to despawn return; } int i = Random.Range (0, list.Count); PrefabPoolingSystem.Despawn(list[i]); list.RemoveAt(i); } }
Once we spawn more than five instances of any of the Prefabs, it will need to instantiate a new one in memory, costing us some memory allocation. But, if we observe the Memory Area in the Profiler, while we only spawn and despawn instances that already exist, then we will notice that absolutely no new allocations take place.
There is one subtle caveat to this system that has not yet been mentioned: the PrefabPoolingSystem
class will outlast Scene lifetime since it is a static class. This means that, when a new Scene is loaded, the pooling system's dictionaries will attempt to maintain references to any pooled instances from the previous Scene, but Unity forcibly destroys these objects regardless of the fact that we are still keeping references to them (unless they were set to DontDestroyOnLoad()
!), and so the dictionaries will be full of null
references. This would cause some serious problems for the next Scene.
We should therefore create a method in PrefabPoolingSystem
that resets the pooling system in preparation for this likely event. The following method should be called before a new Scene is loaded, so that it is ready for any early calls to Prespawn()
in the next Scene:
public static void Reset() { _prefabToPoolMap.Clear (); _goToPoolMap.Clear (); }
Note that, if we also invoke a garbage collection during Scene transitions, there's no need to explicitly empty the PrefabPools
these dictionaries were referencing. Since these were the only references to the PrefabPool
objects, they will be deallocated during the next garbage collection. If we aren't invoking garbage collection between Scenes, then the PrefabPool
and PooledPrefabData
objects will remain in memory until that time.
We have finally solved the problem of runtime memory allocations for GameObjects and Prefabs but, as a quick reminder, we need to be aware of the following caveats:
Spawned()
and Despawned()
methods on IPoolableComponents
Reset()
on PrefabPoolingSystem
before Scene loadingThere are several other features we could implement. These will be left as academic exercises if we wish to extend this system in the future:
IPoolableComponents
added to the GameObject after initialization will not be invoked. We could fix this by changing PrefabPool
to keep acquiring IPoolableComponents
every time Spawned()
and Despawned()
are invoked, at the cost of additional overhead during spawning/despawning.IPoolableComponents
attached to children of the Prefab's root will not be counted. This could be fixed by changing PrefabPool
to use GetComponentsInChildren<T>
, at the cost of additional overhead if we're using Prefabs with deep hierarchies.PrefabPoolingSystem
class of its existence and passes the reference into the corresponding PrefabPool
.IPoolableComponents
to set a priority during acquisition, and directly control the order of execution for their Spawned()
and Despawned()
methods.DontDestroyOnLoad()
. It might be wise to add a Boolean to every Spawn()
call to say whether the object should persist or not, and keep them in a separate data structure that is not cleared out during Reset()
.Spawn()
to accept an argument that allows the requestor to pass custom data to the Spawned()
function of IPoolableObject
for initialization purposes. This could use a system similar to how custom message objects were derived from the BaseMessage
class for our Messaging System back in Chapter 2, Scripting Strategies.