Game Development

How to Implement A Generic Modular Object Pooling System In Unity

What is Object Pooling?

Object pooling is a software design pattern that creates a pool of ready-to-use objects, rather than creating and destroying objects as needed. The idea behind object pooling is to improve application performance by reusing already instantiated objects instead of creating new ones. This is especially useful in situations where object creation and destruction are expensive, such as games. When working with a large number of objects or when creating objects is expensive.

In essence, you create a “pool” a sort where you create the objects at one point in your application and store them in that pool. When you need an instance of a pooled object you would ask the pool to “borrow” one to use. I said borrow because the basis of this pattern is that you return the object when you are finished using the object you give it back to the pool to be used later again either by you or any other process.

How to Implement an Object Pooling in Unity?

In essence, there is no difference between implementing object pooling in Unity or Unreal, or any other game engine. In fact, you can even use object pooling for any other software but that is a topic for another post.

The first thing we need to do before start writing code is to analyze our needs and requirements. Making this a habit will save you a ton of headaches in the long run. All we need to do is find our needs and constraint ourselves to fulfill those needs.

So, what would we expect an object pooling system to do for us? Well, the first thing is that it must be able to create some number of copies of an object and store it in memory and keep track of them. Secondly, we should be able to ask for an object and return it when we are done. With these two requirements, we pretty much fulfill all the necessary constraints to create an object pooling system. However, to create a more generic and modular pooling system we need to have a bit more requirements but we will add them later in the post. Below is a really simple implementation of object pooling in Unity.

using System;
using System.Collections.Generic;
using UnityEngine;

namespace ObjectPoolingExample
{
    [Serializable]
    public struct PoolData
    {
        public GameObject Object;
        public int Count;
    }
    public class ObjectPooler : MonoBehaviour
    {
        [SerializeField] private List<PoolData> ObjectPool = new List<PoolData>();

        private Dictionary<int, Queue<GameObject>> _pool = new Dictionary<int, Queue<GameObject>>();

        public static ObjectPooler Instance;

        void Awake()
        {
            Instance = this;
        }

        void Start()
        {
            FillList();
        }
        private void FillList()
        {
            foreach (var item in ObjectPool)
            {

                int count = item.Count;
                var clonedObjectList = new Queue<GameObject>();

                for (int i = 0; i < count; i++)
                {
                    GameObject obj = Instantiate(item.Object, Vector3.zero, Quaternion.identity);
                    obj.SetActive(false);

                    clonedObjectList.Enqueue(obj);
                }
                
                var key = item.Object.GetInstanceID();
                
                if (_pool.TryGetValue(key, out var dictionaryObjectList))
                {
                    foreach (var go in clonedObjectList)
                    {
                        dictionaryObjectList.Enqueue(go);
                    }
                }
                else
                {
                    _pool.Add(key, clonedObjectList);
                }

            }
        }
        public GameObject Spawn(GameObject objectToClone)
        {
            if (_pool.TryGetValue(objectToClone.GetInstanceID(), out var queue))
            {
                return queue.Dequeue();
            }
            return null;
        }
    }

}

This is the most basic and simple implementation of object pooling in unity. It has a lot of flaws and shortcomings but this will do the job. Now, let’s break down this code block by block to understand what each block does and why we did it the way we did.

  [Serializable]
    public struct PoolData
    {
        public GameObject Object;
        public int Count;
    }

We first created a struct and mark it as Serializable so that we can expose it in the Inspector.

  [SerializeField] private List<PoolData> ObjectPool = new List<PoolData>();

        private Dictionary<int, Queue<GameObject>> _pool = new Dictionary<int, Queue<GameObject>>();

        public static ObjectPooler Instance;

Here we created a List of PoolData so that we can assign the objects we want to pool, and a dictionary to keep track of the objects. The Dictionary takes an int and a Queue that takes game objects. We will look into why soon.

 private void FillList()
        {
            foreach (var item in ObjectPool)
            {

                int count = item.Count;
                var clonedObjectList = new Queue<GameObject>();

                for (int i = 0; i < count; i++)
                {
                    GameObject obj = Instantiate(item.Object, Vector3.zero, Quaternion.identity);
                    obj.SetActive(false);

                    clonedObjectList.Enqueue(obj);
                }
                
                var key = item.Object.GetInstanceID();
                
                if (_pool.TryGetValue(key, out var dictionaryObjectList))
                {
                    foreach (var go in clonedObjectList)
                    {
                        dictionaryObjectList.Enqueue(go);
                    }
                }
                else
                {
                    _pool.Add(key, clonedObjectList);
                }

            }
        }

This function here is where the magic happens. Firstly, we iterate over our PoolData List to get all the objects that we need to instantiate and how many. Next, we use the count property to instantiate and add each object to the queue. Finally, we use the GetInstanceID() on the game object that we created clones of to get its InstanceID. InstanceID is unique for each object in Unity for every run. That means an object will have a different and unique InstanceId every time we run it and keep that id till we terminate the game. Therefore, it is okay to use this property to use as a key for our Dictionary since we know that it will be unique. Since we create the Dictionary on tun time we don’t care if the id changes as long as it is the same during the lifetime of the game.

    public GameObject Spawn(GameObject objectToClone)
        {
            if (_pool.TryGetValue(objectToClone.GetInstanceID(), out var queue))
            {
                var obj = queue.Dequeue();
                queue.Enqueue(obj);
                return obj;
            }
            return null;
        }

With this method, we request a clone of a GameObject. Since we used the InstanceID of the game object to store the queue of its clones. We simply ask the Dictionary if it has our GameObjects clone, if so return the next one in the queue, and before we return it add it back to our queue to be used later again. If we do not have any clones of the GameOBject we return null.

Can We Make it Better?

Yes, we can. First of all, let’s see what is wrong with this implementation. First of all, we have no way of stopping any other object to get the object that we are currently using. We could have just fired a bullet with object pooling, However, even before our bullet reached its target it could be taken by someone else to be used. Consequently, we have no way of giving the object back to the pool after we finished with it. We also cannot create more of the object if it is critical to have it even if it cost some performance. I don’t even go into the singleton that we use to reference the script, which is a time bomb waiting to go off as a project grows.

Let’s look at our object pooling class. The main difference is that instead of inheriting from MonoBehaviour, we inherit from ScriptableObject. The reason for this is to mainly remove the need for having an instance of our object pooler so that we can reference it directly from anywhere without worrying about creating an instance of it.

We also created another struct for holding the cloned object data so that we can have a finer grain of control over the objects. The “InstantiateOnDemand” variable allows us to instantiate the object on the runtime if need it. The “CanBeUsedBeforeReleased” variable is used to determine how tight of ownership an object has over the particular cloned object.

Since we no longer inherit from MonoBehaviour we don’t have methods like Awake or Start to initialize or list. Therefore, we use the “Init” method to initialize our list form somewhere else. The “Destroy” method is for returning a cloned object back to the pool when we are finished. Now, let’s look at a concrete implementation of our abstract classes

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

   namespace ObjectPoolingExample.ScriptableObj
{
    /// <summary>
    /// ScriptableObject class that represents an object pooler for GameObjects.
    /// </summary>

    [CreateAssetMenu(menuName = "ObjectPooling", fileName = "Object_Pool_Container")]
    public class ObjectPoolerSO : ScriptableObject
    {
        /// <summary>
        /// Struct that represents data for a single object in the object pool.
        /// </summary>
        [Serializable]
        protected struct PoolData
        {
            /// <summary>
            /// The object to be pooled.
            /// </summary>
            public GameObject Object;
            /// <summary>
            /// The number of instances of the object to be spawned when the object pool is initialized.
            /// </summary>
            public int AmountToSpawn;
            /// <summary>
            /// Indicates whether new instances of the object should be instantiated on demand when the object pool is empty.
            /// </summary>
            public bool InstantiateOnDemand;
            /// <summary>
            /// Indicates whether the object can be used before it is released back to the object pool.
            /// </summary>
            public bool CanBeUsedBeforeReleased;
        }

        /// <summary>
        /// Struct that represents data for a single pooled object.
        /// </summary>
        protected struct PooledObjectData
        {
            /// <summary>
            /// Queue of objects available for use in the object pool.
            /// </summary>
            public Queue<GameObject> ObjectQueue;
            /// <summary>
            /// Indicates whether new instances of the object should be instantiated on demand when the object pool is empty.
            /// </summary>
            public bool InstantiateOnDemand;
            /// <summary>
            /// Indicates whether the object can be used before it is released back to the object pool.
            /// </summary>
            public bool CanBeUsedBeforeReleased;
        }

        /// <summary>
        /// List of data for the objects in the object pool.
        /// </summary>
        [SerializeField] private List<PoolData> Pool = new List<PoolData>();
        /// <summary>
        /// Dictionary of data for the pooled objects, keyed by the object's instance ID.
        /// </summary>
        private Dictionary<int, PooledObjectData> _pool = new Dictionary<int, PooledObjectData>();

        /// <summary>
        /// Initializes the object pool.
        /// </summary>
        public void Init()
        {
            // Iterate through the list of objects to be pooled
            foreach (var item in Pool)
            {
                // Get the number of instances to spawn and the prefab to use for spawning
                int count = item.AmountToSpawn;
                int key = item.Object.GetInstanceID();

                GameObject prefab = item.Object;

                // Create a queue of GameObjects for the pooled object
                Queue<GameObject> gameObjectQueue = new Queue<GameObject>();

                // Instantiate the specified number of objects and add them to the queue
                for (int i = 0; i < count; i++)
                {
                    GameObject obj = Instantiate(prefab, Vector3.zero, Quaternion.identity);
                    obj.AddComponent<PooledObject>();

                    PooledObject pooledObject = obj.GetComponent<PooledObject>();
                    pooledObject.ID = key;
                    // Set the object to inactive
                    obj.SetActive(false);
                    // Add the object to the queue
                    gameObjectQueue.Enqueue(obj);
                }

                // Create a PooledObjectData struct for the pooled object
                PooledObjectData pooledData = new PooledObjectData
                {
                    ObjectQueue = gameObjectQueue,
                    InstantiateOnDemand = item.InstantiateOnDemand,
                    CanBeUsedBeforeReleased = item.CanBeUsedBeforeReleased
                };

                // Add the pooled object data to the dictionary, using the object's instance ID as the key

                _pool.Add(key, pooledData);
            }
        }
        /// <summary>
        /// Spawns an instance of the given object from the object pool.
        /// </summary>
        /// <param name="objectToClone">The object to spawn.</param>
        /// <param name="spawnPosition">The position at which to spawn the object.</param>
        /// <param name="spawnRotation">The rotation of the spawned object.</param>
        /// <param name="isActive">Indicates whether the spawned object should be active or inactive.</param>
        /// <returns>The spawned object.</returns>
        public GameObject Spawn(GameObject objectToClone, Vector3 spawnPosition, Quaternion spawnRotation, bool isActive = true)
        {
            // Try to get the pooled object data for the object to be spawned    
            int key = objectToClone.GetInstanceID();
            if (_pool.TryGetValue(key, out PooledObjectData pooledData))
            {
                // Get the queue of available objects for the pooled object
                var queue = pooledData.ObjectQueue;
                // If the queue is empty
                if (queue.Count < 1)
                {
                    // If new objects should not be instantiated on demand, return null
                    if (!pooledData.InstantiateOnDemand)
                    {

                        return null;
                    }
                    // If new objects should be instantiated on demand, instantiate it and add it to the queue so that queue size increased for feature needs
                    // Then return the cloned object
                    GameObject clonedObject = Instantiate(objectToClone, spawnPosition, spawnRotation);
                    clonedObject.AddComponent<PooledObject>();

                    PooledObject pooledObject = clonedObject.GetComponent<PooledObject>();
                    pooledObject.ID = key;

                    clonedObject.SetActive(isActive);

                    if (pooledData.CanBeUsedBeforeReleased)
                    {
                        queue.Enqueue(clonedObject);
                    }

                    return clonedObject;
                }
                // Dequeue an available object from the queue
                GameObject obj = queue.Dequeue();

                // Set the object's position and rotation
                obj.transform.localPosition = spawnPosition;
                obj.transform.localRotation = spawnRotation;
                // Set the object to active or inactive as specified
                obj.SetActive(isActive);
                // If the object can be used before it is released back to the object pool, add it back to the queue
                if (pooledData.CanBeUsedBeforeReleased)
                {
                    queue.Enqueue(obj);
                }

                // Return the object
                return obj;
            }

            // If the object to be spawned was not found in the object pool, return null
            return null;
        }

        /// <summary>
        /// Releases the given object back to the object pool.
        /// </summary>
        /// <param name="obj">The object to release.</param>
        public void Destroy(GameObject obj)
        {
            // Set the object to inactive
            obj.SetActive(false);

            var pooledObject = obj.GetComponent<PooledObject>();
            // Get the object's instance ID
            int key = pooledObject.ID;
            // Add the object to the queue for its respective pooled object
            _pool[key].ObjectQueue.Enqueue(obj);
        }
    }
}

Now, let’s dissect our class one last time.

  [CreateAssetMenu(menuName = "ObjectPooling", fileName = "Object_Pool_Container")]

With this attribute, we create a “CreateMenu” entry so that we can create a new ScriptableObject

 public void Init()
        {
            // Iterate through the list of objects to be pooled
            foreach (var item in Pool)
            {
                // Get the number of instances to spawn and the prefab to use for spawning
                int count = item.AmountToSpawn;
                int key = item.Object.GetInstanceID();

                GameObject prefab = item.Object;

                // Create a queue of GameObjects for the pooled object
                Queue<GameObject> gameObjectQueue = new Queue<GameObject>();

                // Instantiate the specified number of objects and add them to the queue
                for (int i = 0; i < count; i++)
                {
                    GameObject obj = Instantiate(prefab, Vector3.zero, Quaternion.identity);
                    obj.AddComponent<PooledObject>();

                    PooledObject pooledObject = obj.GetComponent<PooledObject>();
                    pooledObject.ID = key;
                    // Set the object to inactive
                    obj.SetActive(false);
                    // Add the object to the queue
                    gameObjectQueue.Enqueue(obj);
                }

                // Create a PooledObjectData struct for the pooled object
                PooledObjectData pooledData = new PooledObjectData
                {
                    ObjectQueue = gameObjectQueue,
                    InstantiateOnDemand = item.InstantiateOnDemand,
                    CanBeUsedBeforeReleased = item.CanBeUsedBeforeReleased
                };

                // Add the pooled object data to the dictionary, using the object's instance ID as the key

                _pool.Add(key, pooledData);
            }
        }

We iterate over our list again. This time we check if the object we are currently iterating already exists in our Dictionary so that we don’t create it again. The rest pretty much is the same. We create a number of clones of our object, disable them, add them to a Queue and add that Queue to the Dictionary with the object InstanceId as the key. We also, add a component called PooledObject that holds the instance id of the object we cloned.

Here is the PooledObject component.

using UnityEngine;

public class PooledObject : MonoBehaviour
{
    public int ID;
}
 /// <summary>
        /// Spawns an instance of the given object from the object pool.
        /// </summary>
        /// <param name="objectToClone">The object to spawn.</param>
        /// <param name="spawnPosition">The position at which to spawn the object.</param>
        /// <param name="spawnRotation">The rotation of the spawned object.</param>
        /// <param name="isActive">Indicates whether the spawned object should be active or inactive.</param>
        /// <returns>The spawned object.</returns>
        public GameObject Spawn(GameObject objectToClone, Vector3 spawnPosition, Quaternion spawnRotation, bool isActive = true)
        {
            // Try to get the pooled object data for the object to be spawned    
            int key = objectToClone.GetInstanceID();
            if (_pool.TryGetValue(key, out PooledObjectData pooledData))
            {
                // Get the queue of available objects for the pooled object
                var queue = pooledData.ObjectQueue;
                // If the queue is empty
                if (queue.Count < 1)
                {
                    // If new objects should not be instantiated on demand, return null
                    if (!pooledData.InstantiateOnDemand)
                    {

                        return null;
                    }
                    // If new objects should be instantiated on demand, instantiate it and add it to the queue so that queue size increased for feature needs
                    // Then return the cloned object
                    GameObject clonedObject = Instantiate(objectToClone, spawnPosition, spawnRotation);
                    clonedObject.AddComponent<PooledObject>();

                    PooledObject pooledObject = clonedObject.GetComponent<PooledObject>();
                    pooledObject.ID = key;

                    clonedObject.SetActive(isActive);

                    if (pooledData.CanBeUsedBeforeReleased)
                    {
                        queue.Enqueue(clonedObject);
                    }

                    return clonedObject;
                }
                // Dequeue an available object from the queue
                GameObject obj = queue.Dequeue();

                // Set the object's position and rotation
                obj.transform.localPosition = spawnPosition;
                obj.transform.localRotation = spawnRotation;
                // Set the object to active or inactive as specified
                obj.SetActive(isActive);
                // If the object can be used before it is released back to the object pool, add it back to the queue
                if (pooledData.CanBeUsedBeforeReleased)
                {
                    queue.Enqueue(obj);
                }

                // Return the object
                return obj;
            }

            // If the object to be spawned was not found in the object pool, return null
            return null;
        }

In our Spawn method, we take the object to spawn, where to spawn it in the world, its rotation when we spawn it and whether we want our object to be activated when we get it. Then we check if our Queue is empty and whether we should instantiate the object.

If the set the “InstantiateOnDemand” property to true for this particular object then we create it add it to the Queue, give it a PooledObject component, set its id, and return it. Therefore, we dynamically increase our queue size so that next time it will less likely to run out of objects in the pool.

If we already have the object in the pool we assign the passed values and before returning it decide, if we want our object to be available in the pool or wait for it to be returned to us by checking the “CanBeUsedBeforeReleased” property.

  /// <summary>
        /// Releases the given object back to the object pool.
        /// </summary>
        /// <param name="obj">The object to release.</param>
        public void Destroy(GameObject obj)
        {
            // Set the object to inactive
            obj.SetActive(false);

            var pooledObject = obj.GetComponent<PooledObject>();
            // Get the object's instance ID
            int key = pooledObject.ID;
            // Add the object to the queue for its respective pooled object
            _pool[key].ObjectQueue.Enqueue(obj);
        }

Lastly, we use Destroy method to return a borrowed object back to the pool.

Now, we can reference our Object Pooling Scriptable Object in any script we need to use it without worrying about having an instance of it in the scene. One thing to remember is that we need to call the “Init()” method at some point in the life cycle of our game before using the object pooling system. I usually create a manager class just to initialize it at the beginning of the game. Aside from that with this object pooling system we break our dependency to an instance and have more granular control over each object’s usage throughout our game.

You can get the final version of the object pooling system from the below code.

Generic Modular Object Pooling

1 thought on “How to Implement A Generic Modular Object Pooling System In Unity”

Leave a Reply

Your email address will not be published. Required fields are marked *