Tuesday, May 2, 2017

What Is Object Pooling And Why Do I Need It?

Why Do I Need Object Pooling?

I think we'll work backwards and talk about why we need it, then talk about what it is...

Let's think of a scenario where you have a shooting game.  Each bullet renders on the screen as a GameObject and the player is pretty trigger happy, letting loose four bullets a second for a duration of a three minutes.

Over this time, the player has instantiated 720 game objects!

Now I know what you may be thinking... Surely you destroy the bullet when it has reached it's lifetime so that won't be a problem - there won't be 720 bullets in the scene.
You're right, of course you would do something about the stray bullets - however when it comes to memory management cleanup (see ref 1) and the Garbage Collector, there is going to be some performance cost which can introduce the dreaded micro-stutter!

I have performed a very basic test to prove the theory by replicating the above scenario over 20 seconds of shooting 4 projectiles a second.  The scene was very basic and the projectiles were nothing more than a standard Unity cube. Let's take a look at the asset memory consumption gathered from the profiler shall we!

Without Object Pooling: 402KB
With Object Pooling: 146KB

"Whoa... Steady on cowboy, I'm pretty sure my system can handle a spare 256KB of memory" I hear you say.
True... However, keep in mind the situation... 
  • There is nothing in this scene except basic cube geometry
  • The tests were run for only 20 seconds
This is a 36% increase in memory efficiency!

This is exactly why we need object pooling - performance!
So what is it?

What is Object Pooling

In it's simplest definition, object pooling can be thought of as recycling.

Rather than instantiating a whole bunch of clone GameObjects only to destroy them shortly after, Object Pooling simply disables the object and tucks it away for reuse later on when a new object of that type is needed.

There are likely a stupendous amount of examples for how to create an Object Pooling system - so let us add to the list and I'll demonstrate my implementation below ;) 

Creating An Object Pooling System

Let's build an Object Pooling System!
What do we need? Well let's see, we need to...
  • Set a starting pool size
  • Know what object to pool
  • Handle dishing out the objects in the pool
  • Handle what happens if the pool is empty
  • Know when to recycle the object (normally when it would be destroyed)
Cool - so let's do ourselves a favour and make sure we can easily listen out for the recycle request - we should use an event! I've opted to create a MonoBehaviour class which will be added to any recyclable object...

// IPoolable.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class Recyclable : MonoBehaviour
{
    public event Action RecycleEvent;

    private void OnRecycleEvent(GameObject obj)
    {
        Action handler = RecycleEvent;
        if (handler != null)
        {
            handler(obj);
        }
    }
    
    public void Recycle()
    {        
        OnRecycleEvent(gameObject);
    }
}

This class can be added to any object now via an Object Pool Manager.  This manager's responsibilities will be to add this component and listen out for the event when it has called to be recycled.

Below is the entire pool class which comments really speak for themselves, but I'll point out some important notes below...

// ProjectilePool.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class ObjectPool
{
    private GameObject objectPrefab;
    private List activePool = new List();
    private List readyPool = new List();
    private const int POOL_SIZE = 5;

    private Transform objectParent;

    public ObjectPool(GameObject poolableObject, Transform parent)
    {
        BuildPool(poolableObject, parent);
    }

    public void BuildPool(GameObject poolableObject, Transform parent)
    {
        objectParent = parent;
        objectPrefab = poolableObject;
        for (int i = 0; i < POOL_SIZE; i++)
        {
            // Instansiate a bunch of objects ready to go
            GameObject obj = GameObject.Instantiate(objectPrefab, objectParent);
            obj.SetActive(false);
            // Check if Recyclable component has been added
            if (obj.GetComponent() == null)
                obj.AddComponent();
            readyPool.Add(obj);
        }
    }

    public GameObject GetFromPool()
    {
        // If there are no ready objects, spawn a new one and add to the list
        if (readyPool.Count == 0)
        {
            // Run out of objects - spawn new one            
            GameObject obj = GameObject.Instantiate(
                objectPrefab, objectParent);
            obj.SetActive(false);
            obj.AddComponent();
            // Subscribe to Recycle Request event    
            obj.SetActive(true);
            obj.GetComponent().RecycleEvent += Recycle;            
            return obj;
        }
        else
        {
            // Pull out first objects from ready list and move into active list
            GameObject obj = readyPool[0];
            readyPool.Remove(obj);
            activePool.Add(obj);
            obj.SetActive(true);

            // Subscribe to Recycle Request event            
            obj.GetComponent().RecycleEvent += Recycle;
            return obj;
        }
    }

    private void Recycle(GameObject obj)
    {
        // unsubscribe from Recycle Request event        
        obj.GetComponent().RecycleEvent -= Recycle;

        // Disable GO ready to be used again, move to ready list
        obj.SetActive(false);
        activePool.Remove(obj);
        readyPool.Add(obj);
    }

    public void PurgePool()
    {
        if (activePool.Count > 0)
            for (int i = 0; i < activePool.Count; i++)
            {
                PurgeGO(activePool[i]);
            }

        if (readyPool.Count > 0)
            for (int i = 0; i < readyPool.Count; i++)
            {
                PurgeGO(readyPool[i]);
            }
    }

    private void PurgeGO(GameObject go)
    {
        activePool.Remove(go);
        GameObject.Destroy(go);
    }

    public bool CheckPrefabMatch(GameObject match)
    {
        return (match == objectPrefab) ? true : false;
    }
}
What a mouth full ;)
Let me explain what is happening here.

The Constructor

The constructor is taking in two parameters, a prefab and a parent location. Pretty straight forward, the constructor loops through a bunch of times and fills up a list of GameObjects ready to be used, also making sure they are set to be disabled.

GetFromPool

This function returns a GameObject from... You guessed it, the object pool!

First it checks if there are any free from the ready list, if not - it will instantiate a new one, growing the list dynamically as required.

Importantly - there is something noteworthy happening here...
It is subscribing the method Recycle to the RecycleEvent (see ref 2 for more info on events).
Also, note that if there isn't any objects available, the pool simply expands by one :)

Recycle

As it states, this nifty little method does the object recycling. It will unsubscribe from the object's recycle event, set it inactive and move it from the active pool into the inactive pool.
Nice and simple.

How To Implement It

This is nice and simple! For this example I created an Object Pool Manager as a singleton to look after all recyclable objects in the scene..

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


public class ObjectPoolManager : MonoBehaviour 
{    
    public static ObjectPoolManager instance;
    public List projectilePool = new List();

    private void Awake()
    {
        if (instance == null)
            instance = this;
        else
            Destroy(this);
    }

    public void BuildPool(GameObject prefab, PoolType type)
    {
        // Check if pool exists
        if(!CheckPoolExists(prefab, type))
        {
            List poolList = GetPool(type);
            ObjectPool newPool = new ObjectPool(prefab, transform);
            poolList.Add(newPool);
        } // else never mind because this projectile already exists
    }

    private bool CheckPoolExists(GameObject prefab, PoolType type)
    {
        bool result = false;

        List poolList = GetPool(type);
        

        for (int i = 0; i < poolList.Count; i++)
        {
            if(poolList[i].CheckPrefabMatch(prefab))
            {
                // match found
                return true;
            }
        }        
        return result;
    }

    public GameObject GetObject(GameObject objectPrefab, PoolType type)
    {
        // find which pool
        List poolList = GetPool(type);
        
        for(int i = 0; i < poolList.Count; i++)
        {
            if (poolList[i].CheckPrefabMatch(objectPrefab))
            {
                return poolList[i].GetFromPool();
            }
        }
        Debug.LogError("Object not found in pool");
        return null;
    }

    private List GetPool(PoolType type)
    {
        switch (type)
        {
            case PoolType.projectile:
                return projectilePool;
            case PoolType.tileset:
                return tilesetPool;
            default:
                Debug.LogError("Pool not found!");
                return null;
        }
    }
}
This script does most of the heavy lifting :)
The Object Pools can be referenced from other classes, such as a weapon system and pull a copy from the pool. But wait... Is that a LIST of pools :o
Yes! So you can see how powerful this manager can be.

This manager checks for a matching prefab type, if one is found then it returns the next available object from it's respective pool!

Conclusion

As mentioned, there are numerous ways to handle Object Pooling - this is just one. You could also build on this - for example my weapon had two modes of fire, so all I had to do was request the projectile prefab, and bam! The next available one was returned for duty.

If anything, you should now have a solid concept of what an Object Pool is and why you should use them!

References

0 comments:

Post a Comment