Skip to content
antfarmar edited this page Sep 26, 2021 · 31 revisions

Object Pooling

Asteroids, Bullets, Explosion Particle Systems, etc

General

  • 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.

Quick Sample Implementations

Workflow Changes

  • 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 & Start are only called once in the lifetime of a Component!
      • They can't be used for implicit subsequent initializations. (Can be called explicitly in code, though.)
    • You have to be more careful about what code you place in OnEnable/OnDisable events.
      • These functions will get called on every activation/deactivation of the object!
      • OnEnable is a good place for runtime initializations and/or state changes/updates.

Pooling with a Poolable Component

  • The solution currently used in this project.
  • Creating a Poolable component allows GameObjects containing them to be pooled by a Pooler.
  • The GameObject can be referenced and hence be controlled via its Poolable component.
  • Poolable components 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 Start depending on where/when we create the pools.
    • It's currently during every OnEnable for safety.
  • We could also instead add the Poolable Component 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.
}

Research

  • 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

3 basic scripts in their implementation:

  • 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 ObjectPoolerScript holds the important code.
  • There are several areas for improvement...

What Could Be Improved

Reusability

  • The script itself was separated into its own Component in 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 Component multiple times and assign different prefabs:
      • The class somewhat implements the Singleton design pattern.
      • Whichever script was the last one to Awake would have the static class reference called “current”.
      • The other instances would not have a good way to be found or differentiated from each other.

Size & Growth

  • 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.

Pooled Flags

  • An object is considered “pooled” or “not pooled” based on whether or not its GameObject is 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.
  • 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.

Collections Used

  • It uses a generic List<>.
  • Better to use a generic Queue<>: O(1) operations.
    • A Queue’s Enqueue and Dequeue methods 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 RemoveAt from the end of a list in O(1) time as well (emulates a Stack)

An Improved Sample Implementaion

Pool Queues + Dictionary + Poolable Component

  • 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
}

Clone this wiki locally