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 } }