using System.Collections.Generic; using Core; using UnityEngine; namespace Pooling { /// /// Object pool that supports multiple prefab types with usage tracking and intelligent trimming. /// /// The type of component to pool public abstract class MultiPrefabPool : MonoBehaviour where T : Component { [Tooltip("Whether to pre-instantiate objects during initialization or create them on demand")] public bool preInstantiatePrefabs = false; [Tooltip("Initial number of objects to pre-instantiate per prefab (if preInstantiatePrefabs is true)")] public int initialObjectsPerPrefab = 2; [Tooltip("Maximum number of objects to keep in the pool across all prefab types")] public int totalMaxPoolSize = 50; [Tooltip("Maximum number of inactive instances to keep per prefab type")] public int maxPerPrefabPoolSize = 5; protected Dictionary> pooledObjects = new Dictionary>(); protected Dictionary prefabUsageCount = new Dictionary(); protected List prefabs; protected int totalPooledCount = 0; /// /// Initialize the pool with the specified prefabs /// /// The list of prefabs to use for this pool public virtual void Initialize(List prefabsToPool) { prefabs = prefabsToPool; // Initialize usage tracking for (int i = 0; i < prefabs.Count; i++) { prefabUsageCount[i] = 0; pooledObjects[i] = new Stack(); } // Pre-instantiate objects only if enabled if (preInstantiatePrefabs) { // Calculate how many to pre-instantiate based on available pool size int totalToCreate = Mathf.Min(totalMaxPoolSize, prefabs.Count * initialObjectsPerPrefab); int perPrefab = Mathf.Max(1, totalToCreate / prefabs.Count); for (int i = 0; i < prefabs.Count; i++) { for (int j = 0; j < perPrefab; j++) { if (totalPooledCount >= totalMaxPoolSize) break; CreateNew(i); } } } Logging.Debug($"[{GetType().Name}] Initialized with {prefabs.Count} prefab types"); } /// /// Creates a new instance and adds it to the pool /// protected virtual T CreateNew(int prefabIndex) { if (prefabs == null || prefabIndex >= prefabs.Count) { Debug.LogError($"[{GetType().Name}] Invalid prefab index or prefabs list is null!"); return null; } T prefab = prefabs[prefabIndex]; T obj = Instantiate(prefab, transform); obj.gameObject.SetActive(false); // Initialize IPoolable components if present IPoolable poolable = obj.GetComponent(); if (poolable != null) { poolable.OnDespawn(); } if (!pooledObjects.ContainsKey(prefabIndex)) { pooledObjects[prefabIndex] = new Stack(); } pooledObjects[prefabIndex].Push(obj); totalPooledCount++; return obj; } /// /// Gets an object from the pool for the specified prefab index, or creates a new one if the pool is empty /// /// The index of the prefab to get from the pool /// An object ready to use public virtual T Get(int prefabIndex) { T obj = null; // Track usage frequency if (prefabUsageCount.ContainsKey(prefabIndex)) { prefabUsageCount[prefabIndex]++; } else { prefabUsageCount[prefabIndex] = 1; } // Try to get a valid object from the pool, cleaning up any destroyed objects if (pooledObjects.ContainsKey(prefabIndex) && pooledObjects[prefabIndex].Count > 0) { Logging.Debug($"[{GetType().Name}] Found {pooledObjects[prefabIndex].Count} objects in pool for prefab index {prefabIndex}"); // Keep trying until we find a valid object or the pool is empty while (pooledObjects[prefabIndex].Count > 0) { obj = pooledObjects[prefabIndex].Pop(); totalPooledCount--; // Check if the object is still valid (not destroyed) if (obj != null && obj.gameObject != null) { Logging.Debug($"[{GetType().Name}] Retrieved valid object {obj.name} from pool, current active state: {obj.gameObject.activeInHierarchy}"); break; // Found a valid object } else { // Object was destroyed, continue looking Logging.Warning($"[{GetType().Name}] Found destroyed object in pool, removing it"); obj = null; } } } else { Logging.Debug($"[{GetType().Name}] No objects in pool for prefab index {prefabIndex}, creating new one"); } // If we couldn't find a valid object in the pool, create a new one if (obj == null) { T prefab = prefabs[prefabIndex]; obj = Instantiate(prefab, transform); Logging.Debug($"[{GetType().Name}] Created new object {obj.name} from prefab, active state: {obj.gameObject.activeInHierarchy}"); } // Ensure the object is valid before proceeding if (obj == null || obj.gameObject == null) { Debug.LogError($"[{GetType().Name}] Failed to create valid object for prefab index {prefabIndex}"); return null; } // CRITICAL FIX: Reset position to safe location BEFORE activation // This prevents off-screen checks from triggering during spawn process Vector3 originalPosition = obj.transform.position; obj.transform.position = new Vector3(0f, -1000f, 0f); Logging.Debug($"[{GetType().Name}] Moved object {obj.name} from {originalPosition} to safe position before activation"); Logging.Debug($"[{GetType().Name}] About to activate object {obj.name}, current state: {obj.gameObject.activeInHierarchy}"); obj.gameObject.SetActive(true); Logging.Debug($"[{GetType().Name}] After SetActive(true), object {obj.name} state: {obj.gameObject.activeInHierarchy}"); // Call OnSpawn for IPoolable components IPoolable poolable = obj.GetComponent(); if (poolable != null) { Logging.Debug($"[{GetType().Name}] Calling OnSpawn for object {obj.name}"); poolable.OnSpawn(); Logging.Debug($"[{GetType().Name}] After OnSpawn, object {obj.name} state: {obj.gameObject.activeInHierarchy}"); } Logging.Debug($"[{GetType().Name}] Returning object {obj.name} with final state: {obj.gameObject.activeInHierarchy}"); return obj; } /// /// Returns an object to the pool /// /// The object to return /// The index of the prefab this object was created from public virtual void Return(T obj, int prefabIndex) { if (obj == null) return; // Call OnDespawn for IPoolable components IPoolable poolable = obj.GetComponent(); if (poolable != null) { poolable.OnDespawn(); } // Always deactivate and parent the object obj.gameObject.SetActive(false); obj.transform.SetParent(transform); // Initialize stack if it doesn't exist if (!pooledObjects.ContainsKey(prefabIndex)) { pooledObjects[prefabIndex] = new Stack(); } // Check if we need to trim this specific prefab type's pool if (pooledObjects[prefabIndex].Count >= maxPerPrefabPoolSize) { // Remove the oldest object from this prefab's pool to make room if (pooledObjects[prefabIndex].Count > 0) { T oldestObj = pooledObjects[prefabIndex].Pop(); if (oldestObj != null && oldestObj.gameObject != null) { Destroy(oldestObj.gameObject); totalPooledCount--; } } } // Check global pool limit if (totalPooledCount >= totalMaxPoolSize) { // Find the prefab type with the most pooled objects and remove one int maxCount = 0; int prefabToTrim = -1; foreach (var kvp in pooledObjects) { if (kvp.Value.Count > maxCount) { maxCount = kvp.Value.Count; prefabToTrim = kvp.Key; } } if (prefabToTrim >= 0 && pooledObjects[prefabToTrim].Count > 0) { T oldestObj = pooledObjects[prefabToTrim].Pop(); if (oldestObj != null && oldestObj.gameObject != null) { Destroy(oldestObj.gameObject); totalPooledCount--; } } } // Now add the current object to the pool pooledObjects[prefabIndex].Push(obj); totalPooledCount++; } /// /// Trims the pool to remove excess objects /// Can be called periodically or when memory pressure is high /// public virtual void TrimExcess() { // If we're under the limit, no need to trim if (totalPooledCount <= totalMaxPoolSize) return; // Calculate how many to remove int excessCount = totalPooledCount - totalMaxPoolSize; // Get prefab indices sorted by usage (least used first) List> sortedUsage = new List>(prefabUsageCount); sortedUsage.Sort((a, b) => a.Value.CompareTo(b.Value)); // Remove objects from least used prefabs first foreach (var usage in sortedUsage) { int prefabIndex = usage.Key; if (!pooledObjects.ContainsKey(prefabIndex) || pooledObjects[prefabIndex].Count == 0) continue; // How many to remove from this prefab type int toRemove = Mathf.Min(pooledObjects[prefabIndex].Count, excessCount); for (int i = 0; i < toRemove; i++) { if (pooledObjects[prefabIndex].Count == 0) break; T obj = pooledObjects[prefabIndex].Pop(); Destroy(obj.gameObject); totalPooledCount--; excessCount--; if (excessCount <= 0) return; } } } /// /// Logs pool statistics to the console /// public virtual void LogPoolStats() { Logging.Debug($"[{GetType().Name}] Total pooled objects: {totalPooledCount}/{totalMaxPoolSize}"); string prefabDetails = ""; int index = 0; foreach (var entry in pooledObjects) { int prefabIndex = entry.Key; int count = entry.Value.Count; int usageCount = prefabUsageCount.ContainsKey(prefabIndex) ? prefabUsageCount[prefabIndex] : 0; string prefabName = prefabIndex < prefabs.Count ? prefabs[prefabIndex].name : "Unknown"; prefabDetails += $"\n - {prefabName}: {count} pooled, {usageCount} usages"; // Limit the output to avoid too much text if (++index >= 10 && pooledObjects.Count > 10) { prefabDetails += $"\n - ...and {pooledObjects.Count - 10} more prefab types"; break; } } Logging.Debug($"[{GetType().Name}] Pool details:{prefabDetails}"); } #if UNITY_EDITOR private float _lastLogTime = 0f; protected virtual void Update() { // Log pool stats every 5 seconds if in the editor if (Time.time - _lastLogTime > 5f) { LogPoolStats(); _lastLogTime = Time.time; } } #endif } }