340 lines
14 KiB
C#
340 lines
14 KiB
C#
using System.Collections.Generic;
|
|
using Core;
|
|
using UnityEngine;
|
|
|
|
namespace Pooling
|
|
{
|
|
/// <summary>
|
|
/// Object pool that supports multiple prefab types with usage tracking and intelligent trimming.
|
|
/// </summary>
|
|
/// <typeparam name="T">The type of component to pool</typeparam>
|
|
public abstract class MultiPrefabPool<T> : 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<int, Stack<T>> pooledObjects = new Dictionary<int, Stack<T>>();
|
|
protected Dictionary<int, int> prefabUsageCount = new Dictionary<int, int>();
|
|
protected List<T> prefabs;
|
|
protected int totalPooledCount = 0;
|
|
|
|
/// <summary>
|
|
/// Initialize the pool with the specified prefabs
|
|
/// </summary>
|
|
/// <param name="prefabsToPool">The list of prefabs to use for this pool</param>
|
|
public virtual void Initialize(List<T> prefabsToPool)
|
|
{
|
|
prefabs = prefabsToPool;
|
|
|
|
// Initialize usage tracking
|
|
for (int i = 0; i < prefabs.Count; i++)
|
|
{
|
|
prefabUsageCount[i] = 0;
|
|
pooledObjects[i] = new Stack<T>();
|
|
}
|
|
|
|
// 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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new instance and adds it to the pool
|
|
/// </summary>
|
|
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<IPoolable>();
|
|
if (poolable != null)
|
|
{
|
|
poolable.OnDespawn();
|
|
}
|
|
|
|
if (!pooledObjects.ContainsKey(prefabIndex))
|
|
{
|
|
pooledObjects[prefabIndex] = new Stack<T>();
|
|
}
|
|
|
|
pooledObjects[prefabIndex].Push(obj);
|
|
totalPooledCount++;
|
|
return obj;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets an object from the pool for the specified prefab index, or creates a new one if the pool is empty
|
|
/// </summary>
|
|
/// <param name="prefabIndex">The index of the prefab to get from the pool</param>
|
|
/// <returns>An object ready to use</returns>
|
|
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<IPoolable>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns an object to the pool
|
|
/// </summary>
|
|
/// <param name="obj">The object to return</param>
|
|
/// <param name="prefabIndex">The index of the prefab this object was created from</param>
|
|
public virtual void Return(T obj, int prefabIndex)
|
|
{
|
|
if (obj == null) return;
|
|
|
|
// Call OnDespawn for IPoolable components
|
|
IPoolable poolable = obj.GetComponent<IPoolable>();
|
|
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<T>();
|
|
}
|
|
|
|
// 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++;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Trims the pool to remove excess objects
|
|
/// Can be called periodically or when memory pressure is high
|
|
/// </summary>
|
|
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<KeyValuePair<int, int>> sortedUsage = new List<KeyValuePair<int, int>>(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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logs pool statistics to the console
|
|
/// </summary>
|
|
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
|
|
}
|
|
}
|