-
Notifications
You must be signed in to change notification settings - Fork 15
Object Pooling
antfarmar edited this page Sep 26, 2021
·
31 revisions
Asteroids, Bullets, Explosion Particle Systems, etc
- Object pooling is a common practice in games.
- Objects are created once and recycled, not destroyed.
- Reusing GameObjects instead of destroying & instantiating them over and over again saves precious CPU cycles.
- Pooling is a necessary runtime memory optimization for mobile games and large, memory-intensive games.
- Memory allocation & deallocation(gc calls) is costly, especially on the heap during runtime.
- Zero-tolerance for Runtime Memory Allocation is a best practice in game development.
-
Unity Live Training Video: Object Pooling
- See Research below for source & analysis of this implementation, as well as an improved solution.
- Architecting Games in Unity. Link to the [sample code]
- Catlike Coding Tutorial
- ObjectPool implementations can also get quite complex & robust!
- Using pools drastically changes workflow in Unity.
- GameObjects are simply activated/enabled & deactivated/disabled instead of instantiated/destroyed.
- Objects are no longer disposable one-time-uses. You have to carefully watch/maintain their states.
- Remember that
Awake&Startare only called once in the lifetime of aComponent! - They can't be used for implicit subsequent initializations. (Can be called explicitly in code, though.)
- Remember that
- You have to be more careful about what code you place in
OnEnable/OnDisableevents.- These functions will get called on every activation/deactivation of the object!
-
OnEnableis a good place for runtime initializations and/or state changes/updates.
- The solution currently used in this project.
- Creating a
Poolablecomponent allowsGameObjectscontaining them to be pooled by aPooler. - The
GameObjectcan be referenced and hence be controlled via itsPoolablecomponent. -
Poolablecomponents are added to objects at runtime during pool creation and object instantiation by the Pooler. - This affects when references to the component are available to other scripts.
- It can't be on
Awake!- Though in Unity you can edit script execution order.
- Could be in
Startdepending on where/when we create the pools. - It's currently during every
OnEnablefor safety.
- It can't be on
- We could also instead add the
PoolableComponent manually in the editor or in code using the RequireComponent(typeof (Poolable)) attribute. Issues Link
Sample Code:
using UnityEngine;
using System.Collections;
// Extends MonoBehaviour, so it's a Component. Makes an owner poolable.
public class Poolable : MonoBehaviour
{
public string key; // For dictionary. Maps to its Pool.
public bool isPooled; // Flag for state checks.
}- A lot of free scripts and tutorials on the subject.
- Unity has provided one in a Live Training session.
- A great introduction, not really production ready.
- Improvable. See implementation below.
Analysis of Unity's Live Training On Object Pooling (Source)
Unity Live Training: Object Pooling Video
- A script which recycles a bullet after a period of time.
- A script which rapidly fires bullets (which are reused from the object pool).
- A script which manages the object pool itself.
using UnityEngine;
using System.Collections;
public class BulletDestroyScript : MonoBehaviour
{
void OnEnable ()
{
Invoke("Destroy", 2f);
}
void Destroy ()
{
gameObject.SetActive(false);
}
void OnDisable ()
{
CancelInvoke("Destroy");
}
}using UnityEngine;
using System.Collections;
public class BulletFireScript : MonoBehaviour
{
public float fireTime = 0.05f;
void Start ()
{
InvokeRepeating("Fire", fireTime, fireTime);
}
void Fire ()
{
GameObject obj = ObjectPoolerScript.current.GetPooledObject();
if (obj == null)
return;
// Position the bullet
obj.SetActive(true);
}
}using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class ObjectPoolerScript : MonoBehaviour
{
public static ObjectPoolerScript current;
public GameObject pooledObject;
public int pooledAmount = 20;
public bool willGrow = true;
List<GameObject> pooledObjects;
void Awake ()
{
current = this;
}
void Start ()
{
pooledObjects = new List<GameObject>();
for (int i = 0; i < pooledAmount; ++i)
{
GameObject obj = (GameObject)Instantiate(pooledObject);
obj.SetActive(false);
pooledObjects.Add(obj);
}
}
public GameObject GetPooledObject ()
{
for (int i = 0; i < pooledObjects.Count; ++i)
{
if (!pooledObjects[i].activeInHierarchy)
{
return pooledObjects[i];
}
}
if (willGrow)
{
GameObject obj = (GameObject)Instantiate(pooledObject);
pooledObjects.Add(obj);
return obj;
}
return null;
}
}- The bullet firing and recycling scripts are examples of how to use the pooling system.
- The
ObjectPoolerScriptholds the important code. - There are several areas for improvement...
- The script itself was separated into its own
Componentin order to be reusable. - It is really only reusable between different projects.
- It is not reusable within the same project:
- The script only holds a reference to a single prefab.
- If you wanted to pool different types of bullets, powerups, or enemies, etc, you would need to use a new script for each one.
- Note that you couldn’t simply add this
Componentmultiple times and assign different prefabs:- The class somewhat implements the
Singletondesign pattern. - Whichever script was the last one to
Awakewould have the static class reference called “current”. - The other instances would not have a good way to be found or differentiated from each other.
- The class somewhat implements the
- Needs upper limits on growth.
- Good: The system can initially populate the object pool.
- Bad: It can grow whenever necessary, but that growth is unbounded.
- Exponentially, since
List<T>doubles in size after capacity met.
- Exponentially, since
- An object is considered “pooled” or “not pooled” based on whether or not its
GameObjectis active in the scene hierarchy. - This is problematic for multiple reasons:
- The object is not made active before providing it to a consumer.
- There is no way to know when a consumer will activate its object.
- Hence, the pool might erroneously provide the same object to multiple consumers.
- The object is not made active before providing it to a consumer.
- Because the pool is checking
activeInHierarchy, any parent object which is disabled will cause the pooled object to become marked as reusable.- May have an unintended and unexpected consequence.
- The entire ancestral hierarchy has to be checked to determine whether or not an object is available for use
- Much slower than having a boolean flag somewhere.
- Never checks the validity of its own pooled items.
- e.g. Parent\Child: If you have a pool of objects, a consumer takes the object and parents it to another object, and then that parent object is destroyed, you wont be able to save the pooled object from destruction.
- The pool manager will then crash the next time it checks the index of the destroyed object.
- In a system where you pull objects out of a pool and add them back in, the pool could choose not to add objects which are null, and therefore add a degree of safety.
- It uses a generic
List<>. - Better to use a generic
Queue<>: O(1) operations.- A Queue’s
EnqueueandDequeuemethods are both O(1) operations regardless of the size of the system. -
List<>would be somewhere between O(1) and O(n) depending on how quickly the object found a valid reusable object.- Though
RemoveAtfrom the end of a list in O(1) time as well (emulates aStack)
- Though
- A Queue’s
- Minor Issue: Hot-reloading (recompile during playmode)
- Hot-reloading requires you to not use any types which can't be serialized (such as Dictionaries).
- Currently uses a Dictionary and Queues.
using UnityEngine;
using System.Collections;
// Extends MonoBehaviour, so it's a Component. Makes an owner poolable.
public class Poolable : MonoBehaviour
{
public string key; // For dictionary. Maps to a Pool.
public bool isPooled; // Flag for checks.
}using UnityEngine;
using System.Collections;
using System.Collections.Generic;
// A pool that stores objects made from a prefab.
public class Pool
{
public GameObject prefab;
public int maxCount;
public Queue<Poolable> queue;
}
// Manages pools for clients. Singleton Pattern (lazy instantiation).
public class ObjectPooler : MonoBehaviour
{
#region Fields / Properties
static ObjectPooler Instance
{
get
{
if (instance == null)
CreateSharedInstance();
return instance;
}
}
// The ObjectPooler shared instance.
static ObjectPooler instance;
// The table of pools available for use. Each accessible by a key string.
static Dictionary<string, Pool> pools = new Dictionary<string, Pool>();
#endregion
#region MonoBehaviour
// Enforce Singleton pattern on awake.
void Awake()
{
if (instance != null && instance != this)
Destroy(this);
else
instance = this;
}
#endregion
#region Public
// Set an upper-limit on pool growth.
public static void SetMaxCount(string key, int maxCount)
{
if (!pools.ContainsKey(key))
return;
Pool pool = pools[key];
pool.maxCount = maxCount;
}
// Creates a new pool for use by a prefab type & fills it with startCount new prefab instances.
public static bool CreatePool(string key, GameObject prefab, int startCount, int maxCount)
{
if (pools.ContainsKey(key))
return false;
Pool pool = new Pool();
pool.prefab = prefab;
pool.maxCount = maxCount;
pool.queue = new Queue<Poolable>(startCount);
pools.Add(key, pool);
for (int i = 0; i < startCount; ++i)
Enqueue(CreatePoolableObject(key, prefab));
return true;
}
// Removes an entire pool from use and destroys all of its objects.
public static void DestroyPool(string key)
{
if (!pools.ContainsKey(key))
return;
Pool pool = pools[key];
while (pool.queue.Count > 0)
{
Poolable obj = pool.queue.Dequeue();
GameObject.Destroy(obj.gameObject);
}
pools.Remove(key);
}
// Pool and deactivate an item for reuse, or destroy it if pool is full.
public static void Enqueue(Poolable item)
{
if (item == null || item.isPooled || !pools.ContainsKey(item.key))
return;
Pool pool = pools[item.key];
// Pool is full! Destroy the object.
if (pool.queue.Count >= pool.maxCount)
{
GameObject.Destroy(item.gameObject);
return;
}
// Pool the object for reuse.
pool.queue.Enqueue(item);
item.isPooled = true;
item.transform.SetParent(Instance.transform);
item.gameObject.SetActive(false);
}
// Always returns an inactive prefab instance for the client.
public static Poolable Dequeue(string key)
{
if (!pools.ContainsKey(key))
return null;
Pool pool = pools[key];
// Pool is spent! Create a new object for client.
if (pool.queue.Count == 0)
{
return CreatePoolableObject(key, pool.prefab);
}
// Dequeue an existing object for client.
Poolable obj = pool.queue.Dequeue();
obj.isPooled = false;
return obj;
}
#endregion
#region Private
// Create the statically shared, persistent ObjectPooler.
static void CreateSharedInstance()
{
GameObject objectPooler = new GameObject("ObjectPooler");
DontDestroyOnLoad(objectPooler);
instance = objectPooler.AddComponent<ObjectPooler>();
}
// Instantiate prefab, make it poolable, set key for dictionary.
static Poolable CreatePoolableObject(string key, GameObject prefab)
{
GameObject go = Instantiate(prefab);
Poolable p = go.AddComponent<Poolable>();
p.key = key;
return p;
}
#endregion
}