using System.Collections.Generic; using UnityEngine; using UnityEngine.Pool; /// /// Centralized manager for GameObject pooling using Unity's built-in ObjectPool system. /// Handles per-prefab pools for projectiles, AOE effects, and other frequently instantiated objects. /// /// USAGE: /// - Replace Object.Instantiate(prefab, pos, rot) with GameObjectPoolManager.Instance.Get(prefab, pos, rot) /// - Replace Destroy(gameObject) with GameObjectPoolManager.Instance.Release(gameObject) /// /// TO ADD NEW PREFAB TYPES: /// 1. Add prefab to poolConfigs list in inspector or call RegisterPrefab() at runtime /// 2. That's it! The system automatically creates pools for any prefab you Get() /// public class GameObjectPoolManager : MonoBehaviour { [System.Serializable] public class PoolConfig { [Tooltip("The prefab to pool")] public GameObject prefab; [Tooltip("Initial number of objects to create in pool")] public int defaultCapacity = 50; [Tooltip("Maximum objects in pool before oldest gets destroyed")] public int maxSize = 100; [Tooltip("Check objects when returned to pool")] public bool collectionCheck = true; } #region Singleton private static GameObjectPoolManager _instance; public static GameObjectPoolManager Instance { get { if (_instance == null) { _instance = FindFirstObjectByType(); if (_instance == null) { GameObject go = new GameObject("GameObjectPoolManager"); _instance = go.AddComponent(); DontDestroyOnLoad(go); } } return _instance; } } private void Awake() { if (_instance == null) { _instance = this; DontDestroyOnLoad(gameObject); InitializePools(); } else if (_instance != this) { Destroy(gameObject); } } #endregion [Header("Pool Configuration")] [SerializeField] private List poolConfigs = new List(); [ContextMenu("Reset All Pool Configs")] private void ResetPoolConfigs() { foreach (var config in poolConfigs) { if (config.prefab != null) { config.defaultCapacity = 50; config.maxSize = 100; config.collectionCheck = true; Debug.Log($"Reset config for {config.prefab.name}"); } } Debug.Log("All pool configs reset to default values"); } [Header("Debug Info")] [SerializeField] private bool showDebugLogs = true; // Temporarily enabled for debugging [SerializeField] private bool showPoolStats = false; // Runtime pool storage - maps prefab to its ObjectPool private Dictionary> pools = new Dictionary>(); // Track which pool each active object belongs to for release private Dictionary activeObjectToPrefab = new Dictionary(); #region Public API /// /// Get a pooled GameObject instance. Creates pool if it doesn't exist. /// /// Prefab to instantiate /// World position /// World rotation /// Pooled GameObject instance public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation) { if (prefab == null) { Debug.LogError("GameObjectPoolManager: Cannot get null prefab"); return null; } // Ensure pool exists if (!pools.ContainsKey(prefab)) { CreatePoolForPrefab(prefab); } // Get object from pool GameObject obj = pools[prefab].Get(); // Set position and rotation obj.transform.position = position; obj.transform.rotation = rotation; // Track this object activeObjectToPrefab[obj] = prefab; if (showDebugLogs) Debug.Log($"GameObjectPoolManager: Got {prefab.name} from pool"); return obj; } /// /// Return a GameObject to its pool. /// /// Object to return to pool public void Release(GameObject obj) { if (obj == null) { Debug.LogError("GameObjectPoolManager: Cannot release null object"); return; } // Find which prefab this object belongs to if (!activeObjectToPrefab.TryGetValue(obj, out GameObject prefab)) { Debug.LogWarning($"GameObjectPoolManager: Object {obj.name} not tracked. This might be a non-pooled object. Destroying instead."); Destroy(obj); return; } // Remove from tracking activeObjectToPrefab.Remove(obj); // Return to pool pools[prefab].Release(obj); if (showDebugLogs) Debug.Log($"GameObjectPoolManager: Released {obj.name} to {prefab.name} pool"); } /// /// Register a new prefab for pooling at runtime. /// /// Prefab to register /// Initial pool size /// Maximum pool size public void RegisterPrefab(GameObject prefab, int defaultCapacity = 50, int maxSize = 100) { if (prefab == null) return; if (pools.ContainsKey(prefab)) { Debug.LogWarning($"GameObjectPoolManager: Prefab {prefab.name} already registered"); return; } PoolConfig config = new PoolConfig { prefab = prefab, defaultCapacity = defaultCapacity, maxSize = maxSize, collectionCheck = true }; poolConfigs.Add(config); CreatePool(config); Debug.Log($"GameObjectPoolManager: Registered and created pool for {prefab.name}"); } #endregion #region Pool Management private void InitializePools() { foreach (var config in poolConfigs) { if (config.prefab != null && ValidatePoolConfig(config)) { CreatePool(config); } } Debug.Log($"GameObjectPoolManager: Initialized {pools.Count} pools"); } private bool ValidatePoolConfig(PoolConfig config) { if (config.defaultCapacity <= 0) { Debug.LogError($"GameObjectPoolManager: Invalid defaultCapacity ({config.defaultCapacity}) for {config.prefab.name}. Must be > 0. Using default value 50."); config.defaultCapacity = 50; } if (config.maxSize <= 0) { Debug.LogError($"GameObjectPoolManager: Invalid maxSize ({config.maxSize}) for {config.prefab.name}. Must be > 0. Using default value 100."); config.maxSize = 100; } if (config.maxSize < config.defaultCapacity) { Debug.LogWarning($"GameObjectPoolManager: maxSize ({config.maxSize}) is less than defaultCapacity ({config.defaultCapacity}) for {config.prefab.name}. Setting maxSize to {config.defaultCapacity}."); config.maxSize = config.defaultCapacity; } return true; } private void CreatePoolForPrefab(GameObject prefab) { // Check if we have a config for this prefab PoolConfig config = poolConfigs.Find(c => c.prefab == prefab); if (config == null) { // Create default config config = new PoolConfig { prefab = prefab, defaultCapacity = 50, maxSize = 100, collectionCheck = true }; Debug.Log($"GameObjectPoolManager: Auto-creating pool for {prefab.name} with default settings"); } // Always validate config before creating pool ValidatePoolConfig(config); CreatePool(config); } private void CreatePool(PoolConfig config) { if (pools.ContainsKey(config.prefab)) { Debug.LogWarning($"GameObjectPoolManager: Pool for {config.prefab.name} already exists"); return; } Debug.Log($"GameObjectPoolManager: Creating pool for {config.prefab.name} with defaultCapacity={config.defaultCapacity}, maxSize={config.maxSize}"); var pool = new ObjectPool( createFunc: () => CreatePooledObject(config.prefab), actionOnGet: (obj) => OnGetFromPool(obj), actionOnRelease: (obj) => OnReleaseToPool(obj), actionOnDestroy: (obj) => OnDestroyPooledObject(obj), collectionCheck: config.collectionCheck, defaultCapacity: config.defaultCapacity, maxSize: config.maxSize ); pools[config.prefab] = pool; Debug.Log($"GameObjectPoolManager: Successfully created pool for {config.prefab.name}"); } #endregion #region Pool Callbacks private GameObject CreatePooledObject(GameObject prefab) { GameObject obj = Instantiate(prefab); obj.name = prefab.name + "(Pooled)"; // Add PooledObject component to track pool membership var pooledComponent = obj.GetComponent(); if (pooledComponent == null) { pooledComponent = obj.AddComponent(); } pooledComponent.Initialize(prefab); if (showDebugLogs) Debug.Log($"GameObjectPoolManager: Created new pooled object {obj.name}"); return obj; } private void OnGetFromPool(GameObject obj) { // Reset any IPoolable components FIRST var poolables = obj.GetComponents(); for (int i = 0; i < poolables.Length; i++) { poolables[i].OnGetFromPool(); } // Activate object AFTER reset obj.SetActive(true); } private void OnReleaseToPool(GameObject obj) { // Reset any IPoolable components var poolables = obj.GetComponents(); for (int i = 0; i < poolables.Length; i++) { poolables[i].OnReleaseToPool(); } obj.SetActive(false); } private void OnDestroyPooledObject(GameObject obj) { if (obj != null) { Destroy(obj); } } #endregion #region Debug and Stats private void OnGUI() { if (!showPoolStats) return; GUILayout.BeginArea(new Rect(10, 10, 300, 200)); GUILayout.Label("Pool Stats", GUI.skin.box); foreach (var kvp in pools) { string prefabName = kvp.Key.name; int activeCount = activeObjectToPrefab.Values.Count; GUILayout.Label($"{prefabName}: {activeCount} active"); } GUILayout.EndArea(); } #endregion } /// /// Interface for objects that need custom reset behavior when pooled/released. /// Implement this on components that need to reset state when returned to pool. /// public interface IPoolable { void OnGetFromPool(); void OnReleaseToPool(); } /// /// Component automatically added to pooled objects to track their source prefab. /// public class PooledObject : MonoBehaviour { [SerializeField] private GameObject sourcePrefab; public GameObject SourcePrefab => sourcePrefab; public void Initialize(GameObject prefab) { sourcePrefab = prefab; } }