Object Pooling - For Unity3D

 

PlatformerGuy

 

This is another post brought over from my website: www.IndieDevSpot.com you can find the alternate version of this article here: https://indiedevspot.azurewebsites.net/2014/07/08/object-pooling-great-way-to-increase-performance/

So you want to have constant explosions, hundreds of rockets, bullets flying all over the place and general insanity in your game.  Great, so do I!  But this insanity comes at a cost if you don’t manage your resources properly.  When you instantiate and destroy an object, you have to allocate memory, it sits there for a while and eventually the garbage collector comes by and releases it, IF it meets all of the requirements.  Well that’s the sliced down version anyways.  If you are interested in more reading on the whys, here are some links to additional reading.

This article discusses one technique for helping manage those resources for objects that might quickly go through that cycle.  The article is three sections, what is object pooling, what to pool and how to implement object pooling.

So what is this object pooling business?

Object pooling is actually a fairly simple strategy which is designed to reduce the number of times you allocate and de-allocate memory to boost performance.  The central idea is that you have a list of objects that are pre-instantiated.  When you need an object, you ask for it, and are returned an object whose properties you then re-set to what you need.  When it is time to destroy or de-allocate that object, instead of calling Destroy(object); or similar, you simply set it to inactive, which automatically returns it to the pool of objects you can use. If your character can sit and shoot 100 bullets per second but at any time only 10 of them are ever on screen and can impact the game, you will probably have a pool of only 11 bullets and have the same affect of 100 bullets having been instantiated and destroyed every second without that overhead.

So what types of objects should we be pooling?

I have chosen to break down objects I pool into a few simple categories

  • Gets created and destroyed a lot (examples: bullets, enemies, clouds)
  • Objects I always need, but change context a lot and shift in quantity (examples: Targeters, Measurers, Spotlights, Imitation Shadows).

When deciding if you should pool an object, also think about how often that object is active.  If most of the time the object is inactive and it doesn’t churn a lot.  Even a power up in which 100 rockets burst from your character and are then almost immediately de-allocated may not necessarily need to be pooled.  You can potentially instantiate those objects when you need them, and then ditch them.  If your game has 50 types of power-ups that you can ever only use one at a time, it probably doesn’t make sense to create pools of each one on startup.

So how do I implement Object Pooling

The Object Pool

Well the first thing you need is an object pool.  Below is a sample object pool I have written and am currently using in code named “Project Spritzer”.  Please note that lines 65-70 require you to have a Game State Manager that issues shrink events.  If you do not have a Game State Manager that issues Shrink events, you can simply comment these lines out.  Also note this is not thread safe. The comments in this sample should be fairly good at explaining what is going on, but here is a high level description. The object pool class is the management structure for individual object pools.  This includes creation of the pool, retrieving objects, increasing the pool size and also shrinking the pool.  If you have multiple object pools, they should be accessed through the object pool manager and not the individual pools.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 /*  * @Author: David Crook  *  * Use the object pools to help reduce object instantiation time and performance  * with objects that are frequently created and used.  *  *  */ using UnityEngine; using System.Collections.Generic; using System; using System.Linq;   /// <summary> /// The object pool is a list of already instantiated game objects of the same type. /// </summary> public class ObjectPool {     //the list of objects.     private List<GameObject> pooledObjects;       //sample of the actual object to store.     //used if we need to grow the list.     private GameObject pooledObj;       //maximum number of objects to have in the list.     private int maxPoolSize;       //initial and default number of objects to have in the list.     private int initialPoolSize;       /// <summary>     /// Constructor for creating a new Object Pool.     /// </summary>     /// <param name="obj">Game Object for this pool</param>     /// <param name="initialPoolSize">Initial and default size of the pool.</param>     /// <param name="maxPoolSize">Maximum number of objects this pool can contain.</param>     /// <param name="shouldShrink">Should this pool shrink back to the initial size.</param>     public ObjectPool(GameObject obj, int initialPoolSize, int maxPoolSize, bool shouldShrink)     {         //instantiate a new list of game objects to store our pooled objects in.         pooledObjects = new List<GameObject>();           //create and add an object based on initial size.         for (int i = 0; i < initialPoolSize; i++)         {             //instantiate and create a game object with useless attributes.             //these should be reset anyways.             GameObject nObj = GameObject.Instantiate(obj, Vector3.zero, Quaternion.identity) as GameObject;               //make sure the object isn't active.             nObj.SetActive(false);               //add the object too our list.             pooledObjects.Add(nObj);               //Don't destroy on load, so             //we can manage centrally.             GameObject.DontDestroyOnLoad(nObj);         }           //store our other variables that are useful.         this.maxPoolSize = maxPoolSize;         this.pooledObj = obj;         this.initialPoolSize = initialPoolSize;           //are we supposed to shrink?         if(shouldShrink)         {             //listen to the game state manager's event for all pools should shrink             //back to their initial size.             GameStateManager.Instance.ShrinkPools += new GameStateManager.GameEvent(this.Shrink);         }     }       /// <summary>     /// Returns an active object from the object pool without resetting any of its values.     /// You will need to set its values and set it inactive again when you are done with it.     /// </summary>     /// <returns>Game Object of requested type if it is available, otherwise null.</returns>     public GameObject GetObject()     {         //iterate through all pooled objects.         for (int i = 0; i < pooledObjects.Count; i++)         {             //look for the first one that is inactive.             if (pooledObjects[i].activeSelf == false)             {                 //set the object to active.                 pooledObjects[i].SetActive(true);                 //return the object we found.                 return pooledObjects[i];             }         }         //if we make it this far, we obviously didn't find an inactive object.         //so we need to see if we can grow beyond our current count.         if (this.maxPoolSize > this.pooledObjects.Count)         {             //Instantiate a new object.             GameObject nObj = GameObject.Instantiate(pooledObj, Vector3.zero, Quaternion.identity) as GameObject;             //set it to active since we are about to use it.             nObj.SetActive(true);             //add it to the pool of objects             pooledObjects.Add(nObj);             //return the object to the requestor.             return nObj;         }         //if we made it this far obviously we didn't have any inactive objects         //we also were unable to grow, so return null as we can't return an object.         return null;     }       /// <summary>     /// Iterate through the pool and releases as many objects as     /// possible until the pool size is back to the initial default size.     /// </summary>     /// <param name="sender">Who initiated this event?</param>     /// <param name="eventArgs">The arguments for this event.</param>     public void Shrink(object sender, GameEventArgs eventArgs)     {         //how many objects are we trying to remove here?         int objectsToRemoveCount = pooledObjects.Count - initialPoolSize;         //Are there any objects we need to remove?         if (objectsToRemoveCount <= 0)         {             //cool lets get out of here.             return;         }           //iterate through our list and remove some objects         //we do reverse iteration so as we remove objects from         //the list the shifting of objects does not affect our index         //Also notice the offset of 1 to account for zero indexing         //and i >= 0 to ensure we reach the first object in the list.         for (int i = pooledObjects.Count - 1; i >= 0; i--)         {             //Is this object active?             if (!pooledObjects[i].activeSelf)             {                 //Guess not, lets grab it.                 GameObject obj = pooledObjects[i];                 //and kill it from the list.                 pooledObjects.Remove(obj);             }         }     }   }

The Object Pool Manager

The next thing I use is an Object Pool Manager, as I tend to have multiple pools of multiple types of objects.  You probably will too.  The object pool manager is a singleton class, which is where I centrally ask for objects, it finds the correct pool and returns an object from that pool.  The pool manager simply keeps tabs on the various object pools and allows a central location for creation and access.  The code for my object pooling manager is below.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 /*  * @Author: David Crook  *  * Use this singleton Object Pooling Manager Class to manage a series of object pools.  * Typical uses are for particle effects, bullets, enemies etc.  *  *  */ using UnityEngine; using System.Collections; using System.Collections.Generic; using System;   public class ObjectPoolingManager {       //the variable is declared to be volatile to ensure that     //assignment to the instance variable completes before the     //instance variable can be accessed.     private static volatile ObjectPoolingManager instance;       //look up list of various object pools.     private Dictionary<String, ObjectPool> objectPools;       //object for locking     private static object syncRoot = new System.Object();       /// <summary>     /// Constructor for the class.     /// </summary>     private ObjectPoolingManager()     {         //Ensure object pools exists.         this.objectPools = new Dictionary<String, ObjectPool>();     }       /// <summary>     /// Property for retreiving the singleton.  See msdn documentation.     /// </summary>     public static ObjectPoolingManager Instance     {         get         {             //check to see if it doesnt exist             if (instance == null)             {                 //lock access, if it is already locked, wait.                 lock (syncRoot)                 {                     //the instance could have been made between                     //checking and waiting for a lock to release.                     if (instance == null)                     {                         //create a new instance                         instance = new ObjectPoolingManager();                     }                 }             }             //return either the new instance or the already built one.             return instance;         }     }       /// <summary>     /// Create a new object pool of the objects you wish to pool     /// </summary>     /// <param name="objToPool">The object you wish to pool.  The name property of the object MUST be unique.</param>     /// <param name="initialPoolSize">Number of objects you wish to instantiate initially for the pool.</param>     /// <param name="maxPoolSize">Maximum number of objects allowed to exist in this pool.</param>     /// <param name="shouldShrink">Should this pool shrink back down to the initial size when it receives a shrink event.</param>     /// <returns></returns>     public bool CreatePool(GameObject objToPool, int initialPoolSize, int maxPoolSize, bool shouldShrink)     {         //Check to see if the pool already exists.         if (ObjectPoolingManager.Instance.objectPools.ContainsKey(objToPool.name))         {             //let the caller know it already exists, just use the pool out there.             return false;         }         else         {             //create a new pool using the properties             ObjectPool nPool = new ObjectPool(objToPool, initialPoolSize, maxPoolSize, shouldShrink);             //Add the pool to the dictionary of pools to manage             //using the object name as the key and the pool as the value.             ObjectPoolingManager.Instance.objectPools.Add(objToPool.name, nPool);             //We created a new pool!             return true;         }     }       /// <summary>     /// Get an object from the pool.     /// </summary>     /// <param name="objName">String name of the object you wish to have access to.</param>     /// <returns>A GameObject if one is available, else returns null if all are currently active and max size is reached.</returns>     public GameObject GetObject(string objName)     {         //Find the right pool and ask it for an object.         return ObjectPoolingManager.Instance.objectPools[objName].GetObject();     } }

Ok, so that’s good, but how do I actually use these classes now?  Below is some sample code for creating an object pool for circle light, retrieving a circle light for my game “Project Spritzer” as well as returning it to the pool.

Creating the Pool.

Here we create a brand new object pool of type RangeCookieLight, which is a prefab directional light that all of my players share.  The initial size is 1, with a max of 3 and I want this to shrink down if necessary.  I chose to pool this object because it is active 95% of the time, it just simply changes context based on who is using it, and there is only ever a maximum of 3 active at one time, though that is rare.

1 2 3 4 private void CreateObjectPools() {     ObjectPoolingManager.Instance.CreatePool(this.RangeCookieLight, 1, 3, true); }

Getting the object.

Notice when we retrieve the object, we are referencing it by the GameObject.name attribute.  If you look at the source code for the object pool manager, that is what it uses as the key for the dictionary.  After we have it, we need to reset all properties that might impact the game and situation.  I know the object is returned Active, but I set it active just in case.

1 2 3 4 5 6 7 8 9 10 public void StartTurn() {    this.circleLight = ObjectPoolingManager.Instance.GetObject("CircleLight");    this.circleLight.transform.position = this.transform.position;    this.circleLight.transform.rotation = Quaternion.Euler(90, 0, 0);    this.circleLight.GetComponent<Light>().cookieSize = this.movementRadius;    this.circleLight.GetComponent<Light>().color = Color.white;    this.circleLight.SetActive(true);    this.isMyTurn = true; }

Returning the object to the pool.

Since this is a turn based game, I no longer need the light at the end of the turn.  Therefor the circle should be set to inactive and the next player can now use this same object by simply resetting the properties you can see above.

1 2 3 4 5 6 private void EndTurn() {     this.isMyTurn = false;     this.circleLight.SetActive(false);     GameStateManager.Instance.EndCurrentPlayersTurn(); }

Summary

So in this article we covered why we would use object pooling, what object pooling is as well as how to implement it along with source code that you can copy/paste to begin using. I hope this was informative and helpful for everybody. Again feel free to comment if you find issues with the article or use the contact me section of the website to request new articles on things that are giving you difficulty.