3
\$\begingroup\$

I've made 2 object pooling classes -

MonoObjectPooler : MonoBehaviour - can hold a single bucket of pooled objects of 1 type.

PolyObjectPooler : MonoBehaviour - can hold multiple buckets of pooled objects of different types.

The object to pool information is store in a class PooledObject

/// <summary>
/// Information holder for a pooled object
/// </summary>
[Serializable]
public class PooledObject
{
    [Tooltip(@"Name is used to differ the objects from one another")]
    public string Name;

    [Tooltip(@"What object should be created ?")]
    public GameObject Object;

    [Range(1, 10000)] [Tooltip(@"How much objects should be created ?")]
    public int Amount;

    [Tooltip(@"Can new objects be created in case there are none left ?")]
    public bool CanGrow;

    [Tooltip(@"False - objects must be created manually using Populate method
True - objects will be created automatically on awake")]
    public bool CreateOnAwake;
}

Which is later used in the 2 object pooling classes.

MonoObjectPooler : MonoBehaviour

/// <summary>
/// Object pooler class which can hold a single type of objects.
/// </summary>
public class MonoObjectPooler : MonoBehaviour
{
    /// <summary>
    /// Object to be pooled.
    /// </summary>
    public PooledObject PooledObject;

    /// <summary>
    /// List to store all the objects that will be pooled.
    /// </summary>
    private readonly List<GameObject> pooledObjects = new List<GameObject>();

    private void Awake()
    {
        //Create the pooled object if the CreateOnAwake variable is set to true.
        if (PooledObject.CreateOnAwake)
        {
            Populate();
        }
    }

    /// <summary>
    /// Populates the list of pooled objects with PooledObjects.
    /// </summary>
    public void Populate()
    {
        //Clear the previous items in the list.
        pooledObjects.Clear();

        //Load the items again
        for (int i = 0; i < PooledObject.Amount; i++)
        {
            GameObject obj = Instantiate(PooledObject.Object);
            obj.SetActive(false);
            pooledObjects.Add(obj);
        }
    }

    /// <summary>
    /// Returns a PooledObject.
    /// </summary>
    public GameObject GetPooledObject()
    {
        for (int i = 0; i < pooledObjects.Count; i++)
        {
            if (!pooledObjects[i].activeInHierarchy)
            {
                //we have an available object
                return pooledObjects[i];
            }
        }
        if (PooledObject.CanGrow)
        {
            //we ran out of objects but we can create more
            GameObject obj = Instantiate(PooledObject.Object);
            pooledObjects.Add(obj);
            return obj;
        }
        //We ran out of objects and we cant create more
        return null;
    }
}

PolyObjectPooler : MonoBehaviour

/// <summary>
/// Object pooler class which can hold many different types of objects at one place.
/// </summary>
public class PolyObjectPooler : MonoBehaviour
{
    /// <summary>
    /// Array to store all the objects that will be pooled.
    /// </summary>
    public PooledObject[] ObjectsToPool;

    /// <summary>
    /// Dictionary which holds all the created objects i.e the one you get objects from.
    /// </summary>
    private readonly Dictionary<string, List<GameObject>> pooledObjects =
        new Dictionary<string, List<GameObject>>();

    /// <summary>
    /// Dictionary to hold information about each separate pooled object.
    /// </summary>
    private readonly Dictionary<string, PooledObject> pooledObjectsContainer =
        new Dictionary<string, PooledObject>();

    private void Awake()
    {
        //Load all the required info into the dictionary.
        foreach (PooledObject objectToPool in ObjectsToPool)
        {
            pooledObjects.Add(objectToPool.Name, new List<GameObject>());
            pooledObjectsContainer.Add(objectToPool.Name, objectToPool);

            //Create any pooled object with the CreateOnAwake variable set to true.
            if (objectToPool.CreateOnAwake)
            {
                Populate(objectToPool.Name);
            }
        }
    }

    /// <summary>
    /// Creates objects by a specified pooled object name.
    /// </summary>
    /// <param name="objectName"> The name of the pooled object. </param>
    public void Populate(string objectName)
    {
        //Clear the previous items in the list.
        pooledObjects[objectName].Clear();

        //Load the items again
        for (int i = 0; i < pooledObjectsContainer[objectName].Amount; i++)
        {
            GameObject obj = Instantiate(pooledObjectsContainer[objectName].Object);
            obj.SetActive(false);
            pooledObjects[objectName].Add(obj);
        }
    }

    /// <summary>
    /// Returns a GameObject by a specified pooled object name.
    /// </summary>
    /// <param name="objectName">The name of the pooled object.</param>
    public GameObject GetPooledObject(string objectName)
    {
        for (int i = 0; i < pooledObjects[objectName].Count; i++)
        {
            if (!pooledObjects[objectName][i].activeInHierarchy)
            {
                //we have an available object
                return pooledObjects[objectName][i];
            }
        }
        if (pooledObjectsContainer[objectName].CanGrow)
        {
            //we ran out of objects but we can create more
            GameObject obj = Instantiate(pooledObjectsContainer[objectName].Object);
            pooledObjects[objectName].Add(obj);
            return obj;
        }
        //We ran out of objects and we cant create more
        return null;
    }
}

The code is relatively short and I putted a lot of comments for each method so it should be easy to understand and follow the logic, any performance and code style improvements are welcome.

\$\endgroup\$
4
\$\begingroup\$

I am not too familiar with Unity, so it's hard for me to comment on design. But I do have some experience with pools, so I want to point out, that depending on pool size this linear O(n) search:

for (int i = 0; i < pooledObjects.Count; i++)
{
    if (!pooledObjects[i].activeInHierarchy)
    {
        //we have an available object
        return pooledObjects[i];
    }
}

can quickly become a serious performance bottleneck, that will negate any performance gains normally associated with pools. This problem is usually solved by actually removing "in-use" objects from the pool. And explicitly adding them back once pooled object is "released". This guaranties that any object that is present in the pool at any given time is not "in-use" so you can just "pop" the first one on every request (no search is needed). However, I do not know whether or not such design is applicable to Unity.

Also, both classes look like they can use a common ancestor and/or separate ObjectPool<T> component to represent generic re-usable pool (SRP). As it stands, instantiation logic, population logic, search logic, etc. are basically copy-pasted.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.