using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; using UnityEngine; namespace AlicizaX.ObjectPool { internal sealed partial class ObjectPoolService { private sealed class ObjectPool : ObjectPoolBase, IObjectPool where T : ObjectBase { private struct ObjectSlot { public T Obj; public int SpawnCount; public float LastUseTime; public int PrevByName; public int NextByName; public int PrevAvailableByName; public int NextAvailableByName; public int PrevUnused; public int NextUnused; public byte Flags; [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool IsAlive() => (Flags & 1) != 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetAlive(bool alive) { if (alive) Flags |= 1; else Flags = 0; } } private ObjectSlot[] m_Slots; private int m_SlotCount; private int m_SlotCapacity; private int[] m_FreeStack; private int m_FreeTop; private IntOpenHashMap m_TargetMap; private StringOpenHashMap m_AllNameHeadMap; private StringOpenHashMap m_AvailableNameHeadMap; private StringOpenHashMap m_AvailableNameTailMap; private readonly bool m_AllowMultiSpawn; private float m_AutoReleaseInterval; private int m_Capacity; private float m_ExpireTime; private int m_Priority; private ReleaseStrategy m_ReleaseStrategy; private float m_AutoReleaseTime; private int m_PendingReleaseCount; private int m_ReleasePerFrameBudget; private int m_UnusedHead; private int m_UnusedTail; private int m_LastBudgetScanStart; private bool m_IsShuttingDown; private int m_ShrinkCounter; private const int ShrinkCheckInterval = 60; private const int DefaultReleasePerFrame = 8; private const int InitSlotCapacity = 16; public ObjectPool(string name, bool allowMultiSpawn, float autoReleaseInterval, int capacity, float expireTime, int priority, ReleaseStrategy releaseStrategy) : base(name) { int initCap = Math.Min(Math.Max(capacity, 1), InitSlotCapacity); m_SlotCapacity = initCap; m_Slots = SlotArrayPool.Rent(initCap); m_FreeStack = SlotArrayPool.Rent(initCap); m_TargetMap = new IntOpenHashMap(initCap); m_AllNameHeadMap = new StringOpenHashMap(initCap); m_AvailableNameHeadMap = new StringOpenHashMap(initCap); m_AvailableNameTailMap = new StringOpenHashMap(initCap); m_AllowMultiSpawn = allowMultiSpawn; m_AutoReleaseInterval = autoReleaseInterval; m_Capacity = capacity; m_ExpireTime = expireTime; m_Priority = priority; m_ReleaseStrategy = releaseStrategy; m_AutoReleaseTime = 0f; m_PendingReleaseCount = 0; m_ReleasePerFrameBudget = DefaultReleasePerFrame; m_UnusedHead = -1; m_UnusedTail = -1; m_LastBudgetScanStart = -1; m_IsShuttingDown = false; m_ShrinkCounter = 0; } public override Type ObjectType => typeof(T); public override int Count => m_TargetMap.Count; public override bool AllowMultiSpawn => m_AllowMultiSpawn; public override float AutoReleaseInterval { get => m_AutoReleaseInterval; set { if (value < 0f) { #if UNITY_EDITOR UnityEngine.Debug.LogError("AutoReleaseInterval is invalid."); #endif return; } m_AutoReleaseInterval = value; } } public override int Capacity { get => m_Capacity; set { if (value < 0) { #if UNITY_EDITOR UnityEngine.Debug.LogError("Capacity is invalid."); #endif return; } m_Capacity = value; if (Count > m_Capacity) MarkRelease(Count - m_Capacity); } } public override float ExpireTime { get => m_ExpireTime; set { if (value < 0f) { #if UNITY_EDITOR UnityEngine.Debug.LogError("ExpireTime is invalid."); #endif return; } m_ExpireTime = value; } } public override int Priority { get => m_Priority; set => m_Priority = value; } public override int ReleasePerFrameBudget { get => m_ReleasePerFrameBudget; set => m_ReleasePerFrameBudget = Math.Max(1, value); } public void Register(T obj, bool spawned) { if (obj == null) return; if (obj.Target == null) return; int targetHash = obj.Target.GetHashCode(); if (m_TargetMap.TryGetValue(targetHash, out int existingIdx) && m_Slots[existingIdx].IsAlive()) { #if UNITY_EDITOR UnityEngine.Debug.LogError($"Target '{obj.Target.GetType().FullName}' is already registered in pool '{FullName}'."); #endif return; } int idx = AllocSlot(); ref var slot = ref m_Slots[idx]; slot.Obj = obj; slot.SpawnCount = spawned ? 1 : 0; slot.LastUseTime = Time.realtimeSinceStartup; slot.PrevByName = -1; slot.NextByName = -1; slot.PrevAvailableByName = -1; slot.NextAvailableByName = -1; slot.PrevUnused = -1; slot.NextUnused = -1; slot.SetAlive(true); m_TargetMap.AddOrUpdate(targetHash, idx); string objectName = obj.Name ?? string.Empty; if (m_AllNameHeadMap.TryGetValue(objectName, out int existingHead)) { m_Slots[existingHead].PrevByName = idx; slot.NextByName = existingHead; } m_AllNameHeadMap.AddOrUpdate(objectName, idx); obj.LastUseTime = slot.LastUseTime; if (spawned) obj.OnSpawn(); else MarkSlotAvailable(idx); if (Count > m_Capacity) MarkRelease(Count - m_Capacity); ValidateState(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Spawn() => Spawn(string.Empty); public T Spawn(string name) { if (name == null) return null; if (m_AllowMultiSpawn) return SpawnAny(name); if (!m_AvailableNameHeadMap.TryGetValue(name, out int head)) return null; ref var slot = ref m_Slots[head]; if (!slot.IsAlive() || slot.SpawnCount != 0 || !string.Equals(slot.Obj.Name, name, StringComparison.Ordinal)) { #if UNITY_EDITOR UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name head is inconsistent."); #endif return null; } float now = Time.realtimeSinceStartup; SpawnSlot(head, now); ValidateState(); return slot.Obj; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CanSpawn() => CanSpawn(string.Empty); public bool CanSpawn(string name) { if (name == null) return false; if (m_AllowMultiSpawn) return m_AllNameHeadMap.ContainsKey(name); return m_AvailableNameHeadMap.ContainsKey(name); } public void Unspawn(T obj) { if (obj == null) return; Unspawn(obj.Target); } public void Unspawn(object target) { if (target == null) return; int targetHash = target.GetHashCode(); if (!m_TargetMap.TryGetValue(targetHash, out int idx)) { if (m_IsShuttingDown) return; #if UNITY_EDITOR UnityEngine.Debug.LogError($"Cannot find target in pool '{Name}', type='{target.GetType().FullName}'"); #endif return; } UnspawnSlot(idx); ValidateState(); } public override void Release() { MarkRelease(Count - m_Capacity); } public override void Release(int toReleaseCount) { MarkRelease(toReleaseCount); } public override void ReleaseAllUnused() { int released = 0; int current = m_UnusedHead; while (current >= 0) { int next = m_Slots[current].NextUnused; ref var slot = ref m_Slots[current]; if (CanReleaseSlot(ref slot)) { ReleaseSlot(current); released++; } current = next; } if (released > 0) { m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released); ShrinkStorageIfEmpty(); ValidateState(); } } internal override void Update(float elapseSeconds, float realElapseSeconds) { m_AutoReleaseTime += realElapseSeconds; if (m_AutoReleaseTime >= m_AutoReleaseInterval) { m_AutoReleaseTime = 0f; MarkRelease(Count - m_Capacity); } bool checkExpire = m_ExpireTime < float.MaxValue; if (m_PendingReleaseCount <= 0 && !checkExpire) { TryProgressiveShrink(); return; } float now = Time.realtimeSinceStartup; float expireThreshold = checkExpire ? now - m_ExpireTime : float.MinValue; if (m_PendingReleaseCount > 0) { int releaseBudget = Math.Min(m_ReleasePerFrameBudget, m_PendingReleaseCount); int releasedByBudget = ReleaseUnused(releaseBudget, false, float.MinValue); m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - releasedByBudget); } else if (checkExpire) { ReleaseExpired(m_ReleasePerFrameBudget, expireThreshold); } TryProgressiveShrink(); } private void TryProgressiveShrink() { m_ShrinkCounter++; if (m_ShrinkCounter < ShrinkCheckInterval) return; m_ShrinkCounter = 0; if (m_TargetMap.Count == 0 || m_SlotCapacity <= InitSlotCapacity) return; float usageRatio = (float)m_TargetMap.Count / m_SlotCapacity; if (usageRatio < 0.25f) { int targetCapacity = Math.Max(m_SlotCapacity / 2, InitSlotCapacity); if (targetCapacity < m_SlotCapacity) { var newSlots = SlotArrayPool.Rent(targetCapacity); var newFreeStack = SlotArrayPool.Rent(targetCapacity); Array.Copy(m_Slots, 0, newSlots, 0, Math.Min(m_SlotCount, targetCapacity)); Array.Copy(m_FreeStack, 0, newFreeStack, 0, Math.Min(m_FreeTop, targetCapacity)); SlotArrayPool.Return(m_Slots, true); SlotArrayPool.Return(m_FreeStack, true); m_Slots = newSlots; m_FreeStack = newFreeStack; m_SlotCapacity = targetCapacity; } } } internal override void Shutdown() { m_IsShuttingDown = true; for (int i = 0; i < m_SlotCount; i++) { ref var slot = ref m_Slots[i]; if (!slot.IsAlive()) continue; slot.Obj.Release(true); MemoryPool.Release(slot.Obj); slot.Obj = null; slot.SetAlive(false); } m_TargetMap.Clear(); m_AllNameHeadMap.Clear(); m_AvailableNameHeadMap.Clear(); m_AvailableNameTailMap.Clear(); SlotArrayPool.Return(m_Slots, true); SlotArrayPool.Return(m_FreeStack, true); m_Slots = null; m_FreeStack = null; m_SlotCount = 0; m_SlotCapacity = 0; m_FreeTop = 0; m_PendingReleaseCount = 0; m_UnusedHead = -1; m_UnusedTail = -1; m_LastBudgetScanStart = -1; m_IsShuttingDown = false; ValidateState(); } public override int GetAllObjectInfos(ObjectInfo[] results) { if (results == null) { #if UNITY_EDITOR UnityEngine.Debug.LogError("Results is invalid."); #endif return 0; } int write = 0; int capacity = results.Length; for (int i = 0; i < m_SlotCount; i++) { ref var slot = ref m_Slots[i]; if (!slot.IsAlive()) continue; if (write < capacity) { results[write] = new ObjectInfo(slot.Obj.Name, slot.Obj.Locked, slot.Obj.CustomCanReleaseFlag, slot.Obj.LastUseTime, slot.SpawnCount); } write++; } return write; } internal override void OnLowMemory() { ReleaseAllUnused(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SpawnSlot(int idx, float now) { ref var slot = ref m_Slots[idx]; if (slot.SpawnCount == 0) MarkSlotUnavailable(idx); slot.SpawnCount++; slot.LastUseTime = now; slot.Obj.LastUseTime = now; slot.Obj.OnSpawn(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void UnspawnSlot(int idx) { ref var slot = ref m_Slots[idx]; float now = Time.realtimeSinceStartup; slot.LastUseTime = now; slot.Obj.LastUseTime = now; slot.Obj.OnUnspawn(); slot.SpawnCount--; if (slot.SpawnCount < 0) { #if UNITY_EDITOR UnityEngine.Debug.LogError($"Object '{slot.Obj.Name}' spawn count < 0."); #endif slot.SpawnCount = 0; } if (slot.SpawnCount == 0) MarkSlotAvailable(idx); if (Count > m_Capacity && slot.SpawnCount == 0) MarkRelease(Count - m_Capacity); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void MarkRelease(int count) { if (count > 0) m_PendingReleaseCount = Math.Max(m_PendingReleaseCount, count); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private int AllocSlot() { if (m_FreeTop > 0) return m_FreeStack[--m_FreeTop]; if (m_SlotCount >= m_Slots.Length) GrowSlots(); return m_SlotCount++; } private void GrowSlots() { int newCap = Math.Max(m_SlotCapacity * 2, InitSlotCapacity); var newSlots = SlotArrayPool.Rent(newCap); var newFreeStack = SlotArrayPool.Rent(newCap); Array.Copy(m_Slots, 0, newSlots, 0, m_SlotCount); Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop); SlotArrayPool.Return(m_Slots, true); SlotArrayPool.Return(m_FreeStack, true); m_Slots = newSlots; m_FreeStack = newFreeStack; m_SlotCapacity = newCap; } private void ReleaseSlot(int idx) { ref var slot = ref m_Slots[idx]; if (!slot.IsAlive()) return; T obj = slot.Obj; if (slot.SpawnCount == 0) MarkSlotUnavailable(idx); RemoveFromAllNameChain(idx); int targetHash = obj.Target.GetHashCode(); m_TargetMap.Remove(targetHash); obj.Release(false); MemoryPool.Release(obj); slot.Obj = null; slot.SetAlive(false); slot.SpawnCount = 0; slot.PrevByName = -1; slot.NextByName = -1; slot.PrevAvailableByName = -1; slot.NextAvailableByName = -1; slot.PrevUnused = -1; slot.NextUnused = -1; if (m_FreeTop >= m_SlotCapacity) { int newCap = m_SlotCapacity * 2; var newFreeStack = SlotArrayPool.Rent(newCap); Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop); SlotArrayPool.Return(m_FreeStack, true); m_FreeStack = newFreeStack; m_SlotCapacity = newCap; } m_FreeStack[m_FreeTop++] = idx; ShrinkStorageIfEmpty(); } private void RemoveFromAllNameChain(int idx) { ref var slot = ref m_Slots[idx]; string objectName = slot.Obj.Name ?? string.Empty; if (!m_AllNameHeadMap.TryGetValue(objectName, out int head)) return; int prev = slot.PrevByName; int next = slot.NextByName; if (prev >= 0) { m_Slots[prev].NextByName = next; } else { if (head != idx) { #if UNITY_EDITOR UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain is inconsistent."); #endif return; } if (next >= 0) m_AllNameHeadMap.AddOrUpdate(objectName, next); else m_AllNameHeadMap.Remove(objectName); } if (next >= 0) m_Slots[next].PrevByName = prev; slot.PrevByName = -1; slot.NextByName = -1; } private int ReleaseUnused(int maxReleaseCount, bool requireExpired, float expireThreshold) { int released = 0; int current = requireExpired ? m_UnusedHead : GetBudgetScanStart(); while (current >= 0 && released < maxReleaseCount) { ref var slot = ref m_Slots[current]; int next = slot.NextUnused; if (requireExpired && slot.LastUseTime > expireThreshold) { break; } if (CanReleaseSlot(ref slot)) { ReleaseSlot(current); released++; } current = next; } if (!requireExpired) { m_LastBudgetScanStart = current >= 0 ? current : m_UnusedHead; } return released; } private void ReleaseExpired(int maxReleaseCount, float expireThreshold) { ReleaseUnused(maxReleaseCount, true, expireThreshold); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private int GetBudgetScanStart() { if (m_LastBudgetScanStart >= 0) { ref var slot = ref m_Slots[m_LastBudgetScanStart]; if (slot.IsAlive() && slot.SpawnCount == 0) { return m_LastBudgetScanStart; } } return m_UnusedHead; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool CanReleaseSlot(ref ObjectSlot slot) { return slot.IsAlive() && slot.SpawnCount == 0 && !slot.Obj.Locked && slot.Obj.CustomCanReleaseFlag; } private void ShrinkStorageIfEmpty() { if (m_TargetMap.Count > 0 || m_SlotCapacity <= InitSlotCapacity) return; SlotArrayPool.Return(m_Slots, true); SlotArrayPool.Return(m_FreeStack, true); m_SlotCapacity = InitSlotCapacity; m_Slots = SlotArrayPool.Rent(InitSlotCapacity); m_FreeStack = SlotArrayPool.Rent(InitSlotCapacity); m_AllNameHeadMap.Clear(); m_AvailableNameHeadMap.Clear(); m_AvailableNameTailMap.Clear(); m_SlotCount = 0; m_FreeTop = 0; m_UnusedHead = -1; m_UnusedTail = -1; m_LastBudgetScanStart = -1; } [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] private void ValidateState() { #if !ENABLE_OBJECTPOOL_VALIDATION return; #else int aliveCount = 0; int unusedCount = 0; for (int i = 0; i < m_SlotCount; i++) { ref var slot = ref m_Slots[i]; if (!slot.IsAlive()) continue; aliveCount++; object target = slot.Obj.Target; int targetHash = target.GetHashCode(); if (!m_TargetMap.TryGetValue(targetHash, out int mappedIdx) || mappedIdx != i) { UnityEngine.Debug.LogError($"Object pool '{FullName}' target index map is inconsistent."); continue; } string objectName = slot.Obj.Name ?? string.Empty; if (!m_AllNameHeadMap.TryGetValue(objectName, out int head)) { UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name head is missing."); continue; } if (slot.PrevByName < 0 && head != i) { UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain head is inconsistent."); } if (slot.NextByName >= 0 && m_Slots[slot.NextByName].PrevByName != i) { UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain link is inconsistent."); } bool inUnusedList = m_UnusedHead == i || slot.PrevUnused >= 0 || slot.NextUnused >= 0; bool inAvailableList = false; if (slot.SpawnCount == 0) { unusedCount++; if (!inUnusedList) { UnityEngine.Debug.LogError($"Object pool '{FullName}' unused list is inconsistent."); } if (!m_AvailableNameHeadMap.TryGetValue(objectName, out int availableHead)) { UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name head is missing."); } else { inAvailableList = availableHead == i || slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0; if (!inAvailableList) { UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name chain is inconsistent."); } if (slot.NextAvailableByName >= 0 && m_Slots[slot.NextAvailableByName].PrevAvailableByName != i) { UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name link is inconsistent."); } } } else { if (inUnusedList) { UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in unused list."); } if (slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0) { UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in available chain."); } } } if (aliveCount != m_TargetMap.Count) { UnityEngine.Debug.LogError($"Object pool '{FullName}' alive count is inconsistent."); } int walkUnusedCount = 0; int current = m_UnusedHead; int prevUnused = -1; while (current >= 0) { ref var slot = ref m_Slots[current]; if (!slot.IsAlive() || slot.SpawnCount != 0) { UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain contains invalid slot."); } if (slot.PrevUnused != prevUnused) { UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain linkage is inconsistent."); } walkUnusedCount++; prevUnused = current; current = slot.NextUnused; } if (walkUnusedCount != unusedCount) { UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain count is inconsistent."); } #endif } private void MarkSlotAvailable(int idx) { AddToUnusedListTail(idx); AddToAvailableNameChain(idx); } private void MarkSlotUnavailable(int idx) { RemoveFromUnusedList(idx); RemoveFromAvailableNameChain(idx); } private void AddToUnusedListTail(int idx) { ref var slot = ref m_Slots[idx]; if (m_UnusedHead == idx || slot.PrevUnused >= 0 || slot.NextUnused >= 0) return; slot.PrevUnused = m_UnusedTail; slot.NextUnused = -1; if (m_UnusedTail >= 0) m_Slots[m_UnusedTail].NextUnused = idx; else m_UnusedHead = idx; m_UnusedTail = idx; } private void RemoveFromUnusedList(int idx) { ref var slot = ref m_Slots[idx]; if (m_UnusedHead != idx && slot.PrevUnused < 0 && slot.NextUnused < 0) return; int prev = slot.PrevUnused; int next = slot.NextUnused; if (prev >= 0) m_Slots[prev].NextUnused = next; else m_UnusedHead = next; if (next >= 0) m_Slots[next].PrevUnused = prev; else m_UnusedTail = prev; slot.PrevUnused = -1; slot.NextUnused = -1; if (m_LastBudgetScanStart == idx) m_LastBudgetScanStart = next >= 0 ? next : m_UnusedHead; } private void AddToAvailableNameChain(int idx) { ref var slot = ref m_Slots[idx]; if (slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0) { #if UNITY_EDITOR UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name chain is inconsistent."); #endif return; } string objectName = slot.Obj.Name ?? string.Empty; if (m_AvailableNameTailMap.TryGetValue(objectName, out int tail)) { m_Slots[tail].NextAvailableByName = idx; slot.PrevAvailableByName = tail; slot.NextAvailableByName = -1; m_AvailableNameTailMap.AddOrUpdate(objectName, idx); } else { slot.PrevAvailableByName = -1; slot.NextAvailableByName = -1; m_AvailableNameHeadMap.AddOrUpdate(objectName, idx); m_AvailableNameTailMap.AddOrUpdate(objectName, idx); } } private void RemoveFromAvailableNameChain(int idx) { ref var slot = ref m_Slots[idx]; string objectName = slot.Obj.Name ?? string.Empty; if (slot.PrevAvailableByName < 0 && slot.NextAvailableByName < 0 && (!m_AvailableNameHeadMap.TryGetValue(objectName, out int head) || head != idx)) { return; } int prev = slot.PrevAvailableByName; int next = slot.NextAvailableByName; if (prev >= 0) m_Slots[prev].NextAvailableByName = next; else if (next >= 0) m_AvailableNameHeadMap.AddOrUpdate(objectName, next); else m_AvailableNameHeadMap.Remove(objectName); if (next >= 0) m_Slots[next].PrevAvailableByName = prev; else if (prev >= 0) m_AvailableNameTailMap.AddOrUpdate(objectName, prev); else m_AvailableNameTailMap.Remove(objectName); slot.PrevAvailableByName = -1; slot.NextAvailableByName = -1; } private T SpawnAny(string name) { if (!m_AllNameHeadMap.TryGetValue(name, out int head)) return null; float now = Time.realtimeSinceStartup; ref var slot = ref m_Slots[head]; if (!slot.IsAlive() || !string.Equals(slot.Obj.Name, name, StringComparison.Ordinal)) return null; SpawnSlot(head, now); ValidateState(); return slot.Obj; } } } }