Utilities: Object Pool

0

This seems to become a running theme on here, but I decided I would again at least pretend to have some content on this blog. Over the next couple of days or weeks I will post some of the Utility-Classes I wrote and use in my projects and write a bit about each. But first a couple of disclaimers: These classes are not perfect. They are prone to change without notice. The code I post here will be as it is at the time of writing, but you can get the latest versions from my Bitbucket repository.

The first utility is my generic Object Pool. I already posted it more or less in its entirety on the Unity3D forums, but here it is again. I wrote it for my current project and the motivation was to move all checks into the pool itself, so instead of returning a GameObject, where one has to then find the component of interest, it will directly return that component. Pools can be created by themselves or managed by a central static class to keep track of the pools. Each pool has an initial number of objects and a maximum size (which can be modified after creation), but it will still always return a valid object, even if the maximum size has been reached. Surplus-objects will not be destroyed instead of released back into the pool. That means it will (hopefully) never break any functionality, but when the maximum is reached and a new object is requested, the overhead from the pool will be added to the inherent processing required to instantiate/destroy a GameObject, so the maximum size should be chosen sensibly.

Firstly, here is the abstract class which defines the objects that can be managed by a pool. It mainly serves as a reference to the pool owning the object, so the object can release itself, if necessary. While this reference is publicly writeable, it should not be necessary to ever do so, and it might break stuff, so use with caution, but I did not find a sensible way of making it private.

public abstract class PoolObject : MonoBehaviour {
    private IObjectPool _pool;

    /// <summary>
    /// Set the pool this object belongs to; should not be necessary to ever set manually; use with caution
    /// </summary>
    /// <param name="pool"></param>
    public void SetPool(IObjectPool pool) {
         _pool = pool;
    }

    /// <summary>
    /// Release the object back into the pool. If the pool is invalid, destroy the object
    /// </summary>
    public void Release() {
        ObjectPool<PoolObject> pool = _pool as ObjectPool<PoolObject>;
        if (pool != null) {
            pool.ReleaseItem(this);
        } else {
            Destroy(gameObject);
        }
    }
}

Here is the pool class itself. The interface is mainly needed for the reference in the PoolObject, due to the generic nature of the pool class. It is not written with threads in mind, so it would need some guards, if it was to be used by multiple threads (mainly getItem and Release()),

public interface IObjectPool {

    /// <summary>
    /// Returns true if an object is available
    /// </summary>
    bool objectAvailable { get; }

    /// <summary>
    /// Number of available objects
    /// </summary>
    int remainingObjects { get; }

    /// <summary>
    /// Set the maximum number of objects in the pool. Will prune excess objects, if the new size is smaller than the current number of objects
    /// </summary>
    /// <param name="num"></param>
    void SetMaxNum(int num);
}

public class ObjectPool<T> : IObjectPool where T : PoolObject {

    private readonly GameObject _sourceObject;
    private readonly Stack<GameObject> _objects;
    private int _maxNum;
    private int _currentNum;
    private int _excessNum;

    private ObjectPool(GameObject sourceObject, int startNum, int maxNum) {
        _sourceObject = sourceObject;
        _maxNum = maxNum;
        _currentNum = startNum;

        _objects = new Stack<GameObject>(startNum);

        for (int i = 0; i < startNum; ++i) {
            GameObject newObj = GameObject.Instantiate(_sourceObject);
            newObj.GetComponent<T>().SetPool(this);
            newObj.SetActive(false);
            GameObject.DontDestroyOnLoad(newObj);
            _objects.Push(newObj);
        }
    }

    /// <summary>
    /// Creates and returns a pool for the template of the desired size
    /// </summary>
    /// <param name="sourceObject"></param>
    /// <param name="initialNumber"></param>
    /// <param name="maxNumber"></param>
    /// <returns></returns>
    public static ObjectPool<T> Create(GameObject sourceObject, int initialNumber, int maxNumber) {
        if (sourceObject.GetComponent<T>() == null) return null;

        return new ObjectPool<T>(sourceObject, initialNumber, maxNumber);
    }

    /// <summary>
    /// Get an object from the pool. If none is available, a new one will be created. New ones that exceed the maximum number of objects will be destroyed uppon release
    /// </summary>
    public T getObject {
        get {
            if (_objects.Count > 0) {
                GameObject obj = _objects.Pop();
                obj.SetActive(true);
                return obj.GetComponent<T>();
            }

            GameObject newObj = GameObject.Instantiate(_sourceObject);
            newObj.GetComponent<T>().SetPool(this);
            GameObject.DontDestroyOnLoad(newObj);
            _currentNum++;

            if (_currentNum > _maxNum) {
                _excessNum = _currentNum - _maxNum;
            }

            return newObj.GetComponent<T>();
        }
    }

    /// <summary>
    /// Release an object back into the pool. Excess objects will be destroyed. Use with caution to not release objects into the wrong pool (or use the Release() function on the object itself)
    /// </summary>
    /// <param name="item"></param>
    public void ReleaseItem(T item) {
        item.gameObject.SetActive(false);
        if (_excessNum > 0) {
            GameObject.Destroy(item.gameObject);
            _excessNum--;
            _currentNum--;
        }
        else _objects.Push(item.gameObject);
    }

    public bool objectAvailable {
        get { return _objects.Count > 0 || _currentNum < _maxNum; }
    }

    public int remainingObjects {
        get { return _objects.Count; }
    }

    public void SetMaxNum(int num) {
        _maxNum = num;
        if (_currentNum > _maxNum) {
            _excessNum = _currentNum - _maxNum;

            while (_excessNum > 0 && _objects.Count > 0) {
                GameObject.Destroy(_objects.Pop());
                _excessNum--;
            }
        }
    }
}

Lastly, here is the central manager class. It contains a dictionary of ObjectPools, identified by their respective template object. If no pool exists for a requested item, one will be created automatically, but alternatively a pool can be created manually, with a specified size.

public static class PoolSupervisor {
    private static readonly Dictionary<GameObject, ObjectPool<PoolObject>> pools = new Dictionary<GameObject, ObjectPool<PoolObject>>();
    private const int DEFAULT_SIZE = 5;
    private const int DEFAULT_MAX_SIZE = 15;

    /// <summary>
    /// Returns an object of type T. Will create a pool for the template with default size, if none exists in the central pool-repository
    /// </summary>
    /// <typeparam name="T">PoolObject-derived Component</typeparam>
    /// <param name="template">template object that populates the pool</param>
    /// <returns>a single object from the pool</returns>
    public static T Get<T>(GameObject template) where T : PoolObject {
        if (!pools.ContainsKey(template)) {
            CreatePool<T>(template, DEFAULT_SIZE, DEFAULT_MAX_SIZE);
        }

        return pools[template].getObject as T;
    }

    /// <summary>
    /// Create a pool for the template of the desired size and sstore it in the central pool-repository
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="template"></param>
    /// <param name="initialSize"></param>
    /// <param name="maxSize"></param>
    public static void CreatePool<T>(GameObject template, int initialSize, int maxSize) {
        if (pools.ContainsKey(template)) {
            pools[template].SetMaxNum(maxSize);
            return;
        }

        pools[template] = ObjectPool<PoolObject>.Create(template, initialSize, maxSize);
    }

}

Comments

Your email address will not be published. Required fields are marked *