Example-Code / ObjectPooler / ObjectPooler.cs
ObjectPooler.cs
Raw
using System;
using System.Collections.Generic;
using System.Linq;
using CCG.Shared.DependencyInjection.Unity;
using Cysharp.Threading.Tasks;
using Microsoft.Extensions.Logging;
using UnityEngine;
using static CCG.Bigfoot.StateMachine.ScreenStateMachine;
namespace CCG.Bigfoot.Utility
{
    /// <summary>
    /// Provides object pooling for GameObjects.
    /// </summary>
    /// <remarks>
    /// Authors: CS
    /// Created: 2024-01-09
    /// </remarks> 
    [HelpURL("https://ccglab.atlassian.net/wiki/x/AgC1B")]
    [DisallowMultipleComponent]
    public sealed class ObjectPooler : SingletonUnityBehaviour<ObjectPooler>
    {
        public enum PooledPrefabName
        {
            //--Add an enum for the object you wish to pool.
            CollectionCard,
            DeckEditCard,
            BattleCard,
            AudioSourceSFX
        }

        [System.Serializable]
        public class ObjectPoolItem
        {
            [HideInInspector]
            public Transform objectPoolName;    //--Set automatically depending on the asset name, must be public

            [Header("Select the name of the object you're pooling.")]
            [Tooltip("Go into this script and make a name that matches your item if it's not here.")]
            public PooledPrefabName prefabName; //--user drops into inspector

            [Header("How many copies to make of this prefab?")]
            public int amountToPool;            //--user drops into inspector

            [Header("Drop the prefab you want to clone here.")]
            public GameObject objectToPool;     //--user drops into inspector

            [Header("Advanced options:")]

            [Tooltip("Can the list of clones expand? Or is there a hard cap?")]
            public bool canExpand = true;

            [Tooltip("Set true to mark individul clones of this item as eligible to be marked as in use, even if they are inactive.")]
            public bool CanClonesBeInUseEvenIfInactive;

            [Tooltip("Set true to enable the auto-cleanup feature. It will Destroy() clones and remove the ObjectPoolItem if a new scene activates that isn't in the IncludedScenes list, if the list is populated.")]
            public bool IsEnableAutoCleanupClones;

            [Tooltip("Add all scenes where these clones are needed. ObjectPooler will Destroy() all clones if a scene not included in this list activates. Valid only if IsEnableAutoCleanupClones is true.")]
            public List<EScreenState> IncludedScenes;

            [Header("Parent object options:")]

            [Tooltip("Set the parent object - that holds these clones - position.")]
            public Vector3 parentPosition;

            [Tooltip("Set the parent object - that holds these clones - rotation.")]
            public Quaternion parentRotation;

            [Tooltip("Set the parent of the parent object that holds these clones. One will be created if you don't need to supply this.")]
            public Transform parent;

            /// <summary>
            /// Gets or sets a value indicating whether this item needs to remake its clones upon entering a scene not in the IncludedScenes list.
            /// Valid only if IsEnableAutoCleanupClones is true.
            /// </summary>
            public bool IsNeedsRefill{get; set;}

            /// <summary>
            /// Gets or sets a value indicating whether items of this type have ever existed. Should only be set when an item of this type is instantiated.
            /// </summary>
            public bool HasExisted { get; set; }

        }

        [Header("Begin by clicking the plus sign in Items To Pool to add a new item.")]
        [SerializeField] private List<ObjectPoolItem> _itemsToPool;

        [Header("Utility Options")]
        [Tooltip("A persistant list of prefabs you can reference at runtime with GetPrefab(). Useful for adding items to a pool at runtime, or a central storage site.")]
        [SerializeField] private List<Transform> _prefabList; //--If only we could serialize a dict, so this could be constant time lookup...

#if UNITY_EDITOR
        //--Users who care about viewing private values in the Inspector can go into Debug mode in the Inspector.
        /// <summary>
        /// All of the pooled objects in one list
        /// </summary>
        private List<Transform> _pooledObjects;

        /// <summary>
        /// The parent containing clones of the same name
        /// </summary>
        private List<Transform> _pooledObjectsParents;
#endif
        /// <summary>
        /// Stores items that were auto-cleaned up with the IsEnableAutoCleanupClones setting. Only exists if that feature is utilized.
        /// </summary>
        private List<ObjectPoolItem> _autoCleanedUpItems;

private const string _parentLiteral = "Parent";

        #region Callbacks
        private void Start()
        {
            EScreenState currentScene = AppManager.Instance.CurrentState;

#if UNITY_EDITOR
            _pooledObjectsParents = new List<Transform>();
            _pooledObjects = new List<Transform>();
#endif

            foreach (var item in _itemsToPool)
            {
                if (item.IsEnableAutoCleanupClones && item.IncludedScenes != null && item.IncludedScenes.Count > 0)
                {
                    if (item.IncludedScenes.Contains(currentScene))
                    {
                        AutoRefill(item);
                    }
                    else
                    {
                        item.IsNeedsRefill = true;
                    }
                }
                else
                {
                    AutoRefill(item);
                }
            }

            GameObject.DontDestroyOnLoad(gameObject);
            AppManager.Instance.OnSceneLoadBeginEvent += OnSceneLoadBegin;

            //--Uncomment to see an example
            //-- RunExample().Forget();
        }

        private void OnSceneLoadBegin(EScreenState sceneType)
        {
            if (_itemsToPool != null)
            {
                for (int i = _itemsToPool.Count - 1; i >= 0; i--)
                {
                    ObjectPoolItem currentItem = _itemsToPool[i];

                    if (currentItem.IsEnableAutoCleanupClones && currentItem.IncludedScenes != null && currentItem.IncludedScenes.Count > 0)
                    {
                        using var scope = Logger.BeginScope("OnSceneEnabled");

                        if (!currentItem.IncludedScenes.Contains(sceneType))
                        {
                            if (currentItem.parent != null)
                            {
                                Logger.LogInformation("{ObjectNames}'s clones have been destroyed. Due to enabling the scene: {Scene}", currentItem.parent.name, sceneType);

                                currentItem.IsNeedsRefill = true;
                                Destroy(currentItem.parent.gameObject);
                            }
                            else if(currentItem.HasExisted) //--You may have done something during runtime which dereferenced the parent. Usually it's fine if you see this.
                            {
                                if (!currentItem.IsNeedsRefill)
                                {
                                    Logger.LogWarning("Couldn't auto-cleanup because a parent is missing for clones of type: {ObjectName}. Due to enabling the scene: {Scene}", currentItem.prefabName, sceneType);
                                    continue;
                                }
                                //--Else we just auto-cleaned up after these and we shouldn't throw a warning.
                            }
                        }
                        else if(currentItem.IsNeedsRefill)
                        {
                            Logger.LogInformation("{ObjectName} was auto-refilled. Due to enabling the scene: {Scene}", currentItem.prefabName, sceneType);

                            AutoRefill(currentItem);
                            currentItem.IsNeedsRefill = false;
                        }
                    }
                }
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Add a new item to the object pool at runtime.
        /// </summary>
        /// <param name="ObjectPoolItem">Data used to create a new item.</param>
        public void AddItemToPool(ObjectPoolItem newItem)
        {
            Transform parent = CreateObjectPoolParent(newItem);

            _itemsToPool.Add(newItem);
            PopulatePool(newItem, parent);
        }

        public Transform GetPrefab(PooledPrefabName prefabType)
        {
            int prefabIndex = (int)prefabType;

            if (prefabIndex >= 0 && prefabIndex < _prefabList.Count)
            {
                return _prefabList[prefabIndex];
            }
            else
            {
                using var scope = Logger.BeginScope("GetPrefab");
                Logger.LogError("Prefab not found for type: {type}. Ensure prefab is dropped into Prefab List.", prefabType);

                return null;
            }
        }

        /// <summary>
        /// Get an inactive clone by enum. Consider if clones of this type can implement IPoolable for better performance.
        /// </summary>
        /// <param name="poolPrefabName"></param>
        /// <returns></returns>
        public GameObject Get(PooledPrefabName poolPrefabName)
        {
            return GetObjectByCondition(item => item.prefabName == poolPrefabName);
        }

        /// <summary>
        /// Get an inactive clone by tag. Consider if clones of this type can implement IPoolable for better performance.
        /// </summary>
        /// <param name="tag"></param>
        /// <returns></returns>
        public GameObject Get(string tag)
        {
            return GetObjectByCondition(item => item.objectToPool.CompareTag(tag));
        }

        /// <summary>
        /// Get an IPoolable by enum. Passing a parent will set the returned object's parent.
        /// </summary>
        /// <param name="poolPrefabName"></param> test
        /// <returns></returns>
        public IPoolable GetPoolable(PooledPrefabName poolPrefabName, Transform parent = null)
        {
            GameObject pooledObject = GetObjectByCondition(item => item.prefabName == poolPrefabName);

            if (pooledObject != null)
            {
                if (parent != null)
                {
                    pooledObject.transform.SetParent(parent);
                }
                return pooledObject.GetComponentInChildren<IPoolable>();
            }
            return null;
        }

        /// <summary>
        /// Get an IPoolable by tag.
        /// </summary>
        /// <param name="tag"></param>
        /// <returns></returns>
        public IPoolable GetPoolable(string tag)
        {
            GameObject pooledObject = GetObjectByCondition(item => item.objectToPool.CompareTag(tag));

            if (pooledObject != null) return pooledObject.GetComponentInChildren<IPoolable>();
            return null;
        }

        /// <summary>
        /// Returns an object to its corresponding pool.
        /// </summary>
        /// <param name="obj"></param>
        public void ReturnToPool(Transform obj)
        {
            if (obj.TryGetComponent(out IPoolable poolable))
            {
                ReturnToPoolByEnum(obj, poolable);
            }
            else
            {
                ReturnToPoolByTag(obj);
            }
        }

        private void ReturnToPoolByEnum(Transform obj, IPoolable poolable)//--ReturnToPoolByEnum only supports IPoolables currently.
        {
            using var scope = Logger.BeginScope("ReturnToPoolByEnum");

            ObjectPoolItem item = _itemsToPool.FirstOrDefault(i => i.prefabName == poolable?.Identifier);

            if (item != null && poolable != null)
            {
                if (item.CanClonesBeInUseEvenIfInactive && poolable.IsInUseEvenIfInactive)
                {
                    Logger.LogError("A {ID} clone can't be recycled back into the pool, even though it's inactive because its IsInUseEvenIfInactive is set to true.", poolable.Identifier);
                    return;
                }

                ReparentObjectToPool(obj, item.objectPoolName);
                return;
            }

            Logger.LogError("Failed to find the corresponding pool for the object {ObjectName}", obj.name);
        }

        private void ReturnToPoolByTag(Transform obj)
        {
            using var scope = Logger.BeginScope("ReturnToPoolByTag");

            obj.gameObject.SetActive(false);

            obj.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);

            ObjectPoolItem item = _itemsToPool.FirstOrDefault(i => i.objectPoolName.name == (obj.tag + _parentLiteral));

            if (item != null)
            {
                ReparentObjectToPool(obj, item.objectPoolName);
            }
            else
            {
                Logger.LogError("Failed to find the corresponding pool for the object: {ObjectName}", obj.name);
            }
        }

        private void PopulatePool(ObjectPoolItem item, Transform parent)
        {
            for (int j = 0; j < item.amountToPool; j++)
            {
                GameObject obj = InstantiatePooledObject(item.objectToPool, parent);
                if (obj.TryGetComponent(out IPoolable poolableComponent))
                {
                    poolableComponent.Identifier = item.prefabName;
                }
#if UNITY_EDITOR
                _pooledObjects.Add(obj.transform);
#endif
            }

            item.HasExisted = true;
        }

        private void AutoRefill(ObjectPoolItem item)
        {
            Transform parent = CreateObjectPoolParent(item);
            PopulatePool(item, parent);
        }

        private void ReparentObjectToPool(Transform obj, Transform poolTransform)
        {
            obj.SetParent(poolTransform);
        }

        private GameObject GetObjectByCondition(Func<ObjectPoolItem, bool> condition)
        {
            foreach (ObjectPoolItem item in _itemsToPool)
            {
                if (condition(item))
                {
                    GameObject obj = GetPooledObject(item);
                    if (obj != null) return obj;
                }
            }
            return null;
        }

        private GameObject GetPooledObject(ObjectPoolItem item)
        {
            using var scope = Logger.BeginScope("GetPooledObject");

            foreach (Transform t in item.objectPoolName)
            {
                IPoolable poolable;

                if (!t.gameObject.activeInHierarchy) //--If the clone is inactive...
                {
                    if (item.CanClonesBeInUseEvenIfInactive) //--Check if clones of this item can be marked as in use, even if they are inactive.
                    {
                        poolable = t.GetComponentInChildren<IPoolable>();

                        if (poolable != null && poolable.IsInUseEvenIfInactive)
                        {
                            //--Then we can't return an inactive item here.
                            Logger.LogWarning("{Name} can't be pulled from the pool, even though it's inactive because its IsInUseEvenIfInactive is set to true.", t.name);
                        }
                        else
                        {
                            return t.gameObject;
                        }
                    }

                    else return t.gameObject;
                }
            }

            Logger.LogWarning("Failed to get clone. There aren't any clones not in use. Attempting to instantiate...");

            if (item.canExpand)
            {
                GameObject obj = InstantiatePooledObject(item.objectToPool, item.objectPoolName);
#if UNITY_EDITOR
                _pooledObjects.Add(obj.transform);
#endif
                return obj;
            }
            else
            {
                Logger.LogError("Failed to instantiate a new clone. The pool can't dynamically expand. Set CanExpand on this item in ObjectPooler if you want it to expand past the limit set.");
            }
            return null;
        }

        private Transform CreateObjectPoolParent(ObjectPoolItem item)
        {
            GameObject parent = new();
            parent.transform.SetParent(transform);
            parent.name = item.objectToPool.name + _parentLiteral;
            parent.transform.SetPositionAndRotation(item.parentPosition, item.parentRotation);

            if (item.parent != null) parent.transform.SetParent(item.parent);
            else item.parent = parent.transform;

            item.objectPoolName = parent.transform;

#if UNITY_EDITOR
            _pooledObjectsParents.Add(parent.transform);
#endif
            return parent.transform;
        }

        private GameObject InstantiatePooledObject(GameObject original, Transform parent)
        {
            GameObject clone = Instantiate(original, parent); //--Awake and OnEnable will still callback once this executes.
            clone.SetActive(false);
            clone.name = original.name;
            return clone;
        }

        #region How to use this object pooler
        async UniTask RunExample()
        {
            await UniTask.Delay(2000);

            //-----------How to use if your prefab doesn't extend IPoolable---------------
            //--1.) Tag your prefab at the root level. Its tag needs to match the name of the prefab.
            /*
            //--Get a clone by type
            GameObject clone1 = ObjectPooler.Instance!.Get(PooledPrefabName.ExamplePrefab);
            clone1.SetActive(true);
            clone1.transform.SetParent(GameObject.Find("OtherParent").transform);

            //--Get another of that same clone by what you've tagged it as.
            GameObject clone2 = ObjectPooler.Instance!.Get("ExamplePrefab");
            clone2.SetActive(true);
            clone2.transform.SetParent(GameObject.Find("OtherParent").transform);

            //--Using...
            await UniTask.Delay(5000);

            //--Must call this when you're done using a clone.
            this.ReturnToPool(clone1.transform);
            this.ReturnToPool(clone2.transform);
            */

            //-----------How to use if your prefab extends IPoolable----------------
            //--Setup:
            //--1.) Put your IPoolable interface at root level of the object you're pooling.
            //--2.) Implement the interface functions. (example below)


            //--Get an IPoolable clone by type
            IPoolable ipoolClone1 = ObjectPooler.Instance!.GetPoolable(PooledPrefabName.CollectionCard);
            ipoolClone1.Activate();

            //--IPoolables need to implement below similar logic in their Activate():
            //ipoolClone1.SetActive(true);
            //ipoolClone1.transform.SetParent(GameObject.Find("OtherParent").transform);

            //--Get another IPoolable by type
            IPoolable ipoolClone2 = ObjectPooler.Instance!.GetPoolable(PooledPrefabName.CollectionCard);
            ipoolClone2.Activate();

            //--Using...
            await UniTask.Delay(5000);

            //--Call when done using.
            ipoolClone1.Deactivate(true);

            //--IPoolables need to implement below similar logic in their Deactivate():
            //ObjectPooler.Instance!.ReturnToPool(clone2);

            //--Call when done using.
            ipoolClone2.Deactivate(true);
        }
        #endregion

        #endregion
    }
}