3
\$\begingroup\$

2 scripts: - ObjectPooler - PoolableObject

The ObjectPooler acts as a singleton manager of poolable objects. Any prefab can be instantiated using the pooler via:

    GameObject go = GetPooledObject(prefabReference)

You would use this rather than instantiate the object manually. If an object of that prefab type already exists and is inactive, it is returned instead of instantiating a new one. Currently I am unsure if I should SetActive(true) the gameobject when it is returned from the pool, as that would invoke Start() on that GameObject before its values can be updated.

ObjectPooler.cs:

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

/* This class is used for pooling objects and reusing objects for performance
 * This works best when lots of the same object are spawned multiple times
 * such as projectiles from a gun */
public class ObjectPooler : MonoBehaviour
{
    public static ObjectPooler instance;
    public List<PrePooledObjects> prePooledObjects;
    private Dictionary<string, Queue<GameObject>> dict = null;

    [Serializable]
    public struct PrePooledObjects
    {
        public GameObject gameObject;
        public int count;
    }

    void Awake()
    {
        if (instance == null)
        {
            instance = this;
            dict = new Dictionary<string, Queue<GameObject>>();
        }
        else
        {
            Destroy(this);
        }
    }

    private void Start()
    {
        List<GameObject> pooledObjects = new List<GameObject>();
        foreach (PrePooledObjects prePoolObj in prePooledObjects)
        {
            for (int i = 0; i < prePoolObj.count; i++)
            {
                pooledObjects.Add(GetPooledObject(prePoolObj.gameObject));
            }
        }

        foreach (GameObject go in pooledObjects)
        {
            go.SetActive(false);
        }
    }

    public GameObject GetPooledObject(GameObject go)
    {
        if (!dict.ContainsKey(go.name))
        {
            dict.Add(go.name, new Queue<GameObject>());
        }

        if (dict[go.name].Count > 0)
        {
            return dict[go.name].Dequeue();
        }
        else
        {
            GameObject newGo = Instantiate(go);
            PoolableObject po = newGo.GetComponent<PoolableObject>();
            if (po == null)
            {
                po = newGo.AddComponent<PoolableObject>();
            }
            po.prefab = go;
            return newGo;
        }

    }

    public void ReleasePooledObject(PoolableObject po)
    {
        if (!dict.ContainsKey(po.prefab.name))
        {
            dict.Add(po.prefab.name, new Queue<GameObject>());
        }
        dict[po.prefab.name].Enqueue(po.gameObject);
    }
}

PoolableObject.cs:

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

public class PoolableObject : MonoBehaviour
{
    [SerializeField]
    public GameObject prefab = null;

    private void OnDisable()
    {
        if (prefab != null)
            ObjectPooler.instance.ReleasePooledObject(this);
    }
}
```
\$\endgroup\$
3
\$\begingroup\$

Interesting code! Am about to write something similar for a quite extreme case (decoration elements for procedurally generated terrain) so I could share a few thoughts I had:

That mechanism for prePooledObjects feels a bit too entwined with the pooling system itself. Why not provide a single function like:

PrePoolObjects(GameObject go, int count);

That would directly prepool a number of objects without the need for that extra struct and all that. Your concept is only necessary if you really want to provide the information what needs to be prePooled a single time and also clear the pool regularly for some reason.

Consider adding more than one new object when the queue is empty. For memory access reasons, it tends to be faster to instantiate multiple instances one after another than to instantiate multiple times at separate times.

Together with the above, it might make sense to shrink the number of pooled objects over time. At least if there are rare moments when a ton of a particular object is needed.

A really, really smart pooling system would evaluate the size of the pool constantly in the background (for example once every 5 physics updates) and ensure that there are not too few and not too many objects held "in stock". Important for that is a hysteresis, because of course you do not want to immediately grow the pool whenever a single instance was removed from it and vice versa. Only when it is too full or too empty for a certain period of time, it would start destroying or creating instances. Such a system would nicely adapt to the current need and not load up the memory when a single level progresses. Admittedly this concept might be more necessary for my infinite-terrain game than for a game that's based on levels where you can flush the pool when switching scenes.

Last but not least: Using .name and perform that dictionary search every time could be replaced with custom constants that just hold an int and are the indices in an array/list of pool queues. Then instead of providing the prefab to the GetPooledObject call, you provide that int constant and it takes the relevant queue from the array/list without any lookup operation. The downside of this concept is the slight inconvenience of not being able to use prefabs immediately but first needing to define that constant. Also that might make using drag and drop with the prefabs more complicated or impossible.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thanks for this detailed analysis! I agree, the prePooled functionality may be better suited elsewhere. As for automatic pool expansion/reduction I do implement this in some of my other projects. I felt that in some cases it's not always necessary, so I wanted to avoid that overhead my generic implementation. \$\endgroup\$ – Erik Overflow Feb 11 at 15:53
  • \$\begingroup\$ I'm a bit curious to see some example code on handling custom constants. The GetPooledObject dynamically instantiates objects if the pool is empty, so the gameobject would need to be passed. Otherwise, I would need to have a separate structure that maps all of the ints to their GameObjects and would need to do the lookup in that case. \$\endgroup\$ – Erik Overflow Feb 11 at 15:54
  • \$\begingroup\$ @ErikOverflow True, my bad, you need the queue as well as the GameObject. Still that would only add a second array and jumping to a position in an array or list is quite similar to a pointer indirection. It's not a lookup in a binary tree that requires several comparisons etc. like for the dict lookup. Indeed, those suggestions are completely depending on the situation and usecase of the particular object pool. Best is to tailor one specifically for your project. There are some quite complex looking object pools for Unity in the asset store though. I do wonder how useful those are. \$\endgroup\$ – AlexGeorg Feb 11 at 16:28

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.