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 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_FreeStack; private int m_FreeTop; private ReferenceOpenHashMap m_TargetMap; private StringOpenHashMap m_AllNameHeadMap; private StringOpenHashMap m_NameCursorMap; private readonly bool m_AllowMultiSpawn; private float m_AutoReleaseInterval; private int m_Capacity; private float m_ExpireTime; private int m_Priority; 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) : base(name) { int initCap = Math.Min(Math.Max(capacity, 1), InitSlotCapacity); m_Slots = SlotArrayPool.Rent(initCap); m_FreeStack = SlotArrayPool.Rent(initCap); m_TargetMap = new ReferenceOpenHashMap(initCap); m_AllNameHeadMap = new StringOpenHashMap(initCap); m_NameCursorMap = new StringOpenHashMap(initCap); m_AllowMultiSpawn = allowMultiSpawn; m_AutoReleaseInterval = autoReleaseInterval; m_Capacity = capacity; m_ExpireTime = expireTime; m_Priority = priority; 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; if (m_TargetMap.TryGetValue(obj.Target, 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; } if (!EnsureRegisterCapacity()) { #if UNITY_EDITOR UnityEngine.Debug.LogError($"Object pool '{FullName}' capacity is full."); #endif return; } int idx = AllocSlot(); if (idx < 0) return; 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.PrevUnused = -1; slot.NextUnused = -1; slot.SetAlive(true); m_TargetMap.AddOrUpdate(obj.Target, idx); string objectName = obj.Name ?? string.Empty; if (m_AllNameHeadMap.TryGetValue(objectName, out int existingHead)) { m_Slots[existingHead].PrevByName = idx; slot.NextByName = existingHead; } else { m_NameCursorMap.AddOrUpdate(objectName, idx); } m_AllNameHeadMap.AddOrUpdate(objectName, idx); obj.LastUseTime = slot.LastUseTime; if (spawned) obj.OnSpawn(); else MarkSlotAvailable(idx); UpdateActiveState(); ValidateState(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Spawn() => Spawn(string.Empty); public T Spawn(string name) { if (name == null) name = string.Empty; if (m_AllowMultiSpawn) return SpawnAny(name); int head = FindAvailableByName(name); if (head < 0) 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}' all-name chain 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) name = string.Empty; if (m_AllowMultiSpawn) return m_AllNameHeadMap.ContainsKey(name); return FindAvailableByName(name) >= 0; } private int FindAvailableByName(string name) { if (!m_AllNameHeadMap.TryGetValue(name, out int head)) return -1; ref var headSlot = ref m_Slots[head]; if (headSlot.IsAlive() && headSlot.SpawnCount == 0 && string.Equals(headSlot.Obj.Name, name, StringComparison.Ordinal)) { return head; } int current = GetValidNameCursor(name, head); if (current == head) current = headSlot.NextByName >= 0 ? headSlot.NextByName : head; int first = current; do { ref var slot = ref m_Slots[current]; if (slot.IsAlive() && slot.SpawnCount == 0 && string.Equals(slot.Obj.Name, name, StringComparison.Ordinal)) { int nextCursor = slot.NextByName >= 0 ? slot.NextByName : head; m_NameCursorMap.AddOrUpdate(name, nextCursor); return current; } current = slot.NextByName >= 0 ? slot.NextByName : head; } while (current != first); return -1; } private int GetValidNameCursor(string name, int head) { if (m_NameCursorMap.TryGetValue(name, out int cursor) && cursor >= 0 && cursor < m_SlotCount) { ref var slot = ref m_Slots[cursor]; if (slot.IsAlive() && string.Equals(slot.Obj.Name, name, StringComparison.Ordinal)) return cursor; } m_NameCursorMap.AddOrUpdate(name, head); return head; } public void Unspawn(T obj) { if (obj == null) return; Unspawn(obj.Target); } public void Unspawn(object target) { if (target == null) return; if (!m_TargetMap.TryGetValue(target, 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, false); released++; } current = next; } if (released > 0) { m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released); TrimSlotCountTail(); ShrinkStorageIfEmpty(); UpdateActiveState(); 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(); UpdateActiveState(); 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(); UpdateActiveState(); } private void TryProgressiveShrink() { m_ShrinkCounter++; if (m_ShrinkCounter < ShrinkCheckInterval) return; m_ShrinkCounter = 0; TrimSlotCountTail(); int slotArrayLen = m_Slots.Length; int aliveCount = m_TargetMap.Count; if (aliveCount == 0 || slotArrayLen <= InitSlotCapacity) return; float usageRatio = (float)aliveCount / slotArrayLen; if (usageRatio < 0.25f) { int targetCapacity = Math.Max(NextPowerOf2(Math.Max(m_SlotCount, aliveCount)), InitSlotCapacity); if (targetCapacity < slotArrayLen && m_SlotCount <= targetCapacity) { var newSlots = SlotArrayPool.Rent(targetCapacity); var newFreeStack = SlotArrayPool.Rent(targetCapacity); Array.Copy(m_Slots, 0, newSlots, 0, m_SlotCount); int newFreeTop = 0; for (int i = 0; i < m_FreeTop; i++) { if (m_FreeStack[i] < targetCapacity) newFreeStack[newFreeTop++] = m_FreeStack[i]; } SlotArrayPool.Return(m_Slots, true); SlotArrayPool.Return(m_FreeStack, true); m_Slots = newSlots; m_FreeStack = newFreeStack; m_FreeTop = newFreeTop; } } } 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_NameCursorMap.Clear(); SlotArrayPool.Return(m_Slots, true); SlotArrayPool.Return(m_FreeStack, true); m_Slots = null; m_FreeStack = null; m_SlotCount = 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(); if (m_SlotCount >= m_Slots.Length) return -1; } return m_SlotCount++; } private void GrowSlots() { int currentCap = m_Slots.Length; int maxCap = m_Capacity == int.MaxValue ? int.MaxValue : Math.Max(m_Capacity, InitSlotCapacity); int newCap = Math.Min(Math.Max(currentCap * 2, InitSlotCapacity), maxCap); if (newCap <= currentCap) return; 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; } private void ReleaseSlot(int idx, bool compactStorage = true) { ref var slot = ref m_Slots[idx]; if (!slot.IsAlive()) return; if (slot.SpawnCount > 0) return; T obj = slot.Obj; MarkSlotUnavailable(idx); RemoveFromAllNameChain(idx); m_TargetMap.Remove(obj.Target); obj.Release(false); MemoryPool.Release(obj); slot.Obj = null; slot.SetAlive(false); slot.SpawnCount = 0; slot.PrevByName = -1; slot.NextByName = -1; slot.PrevUnused = -1; slot.NextUnused = -1; if (m_FreeTop >= m_FreeStack.Length) { int newCap = m_FreeStack.Length * 2; var newFreeStack = SlotArrayPool.Rent(newCap); Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop); SlotArrayPool.Return(m_FreeStack, true); m_FreeStack = newFreeStack; } m_FreeStack[m_FreeTop++] = idx; if (compactStorage) { TrimSlotCountTail(); ShrinkStorageIfEmpty(); } } private bool EnsureRegisterCapacity() { if (m_Capacity == int.MaxValue || Count < m_Capacity) return true; int released = ReleaseUnused(1, false, float.MinValue); if (released > 0) { m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released); return Count < m_Capacity; } return false; } private void TrimSlotCountTail() { while (m_SlotCount > 0 && !m_Slots[m_SlotCount - 1].IsAlive()) m_SlotCount--; int write = 0; for (int i = 0; i < m_FreeTop; i++) { int freeIndex = m_FreeStack[i]; if (freeIndex < m_SlotCount) m_FreeStack[write++] = freeIndex; } m_FreeTop = write; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int NextPowerOf2(int value) { value--; value |= value >> 1; value |= value >> 2; value |= value >> 4; value |= value >> 8; value |= value >> 16; return value + 1; } 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 (m_NameCursorMap.TryGetValue(objectName, out int cursor) && cursor == idx) { if (next >= 0) m_NameCursorMap.AddOrUpdate(objectName, next); else if (prev >= 0) m_NameCursorMap.AddOrUpdate(objectName, prev); else m_NameCursorMap.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) { current = next; continue; } if (CanReleaseSlot(ref slot)) { ReleaseSlot(current, false); released++; } current = next; } if (!requireExpired) { m_LastBudgetScanStart = current >= 0 ? current : m_UnusedHead; } if (released > 0) { TrimSlotCountTail(); ShrinkStorageIfEmpty(); } 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_Slots.Length <= InitSlotCapacity) return; SlotArrayPool.Return(m_Slots, true); SlotArrayPool.Return(m_FreeStack, true); m_Slots = SlotArrayPool.Rent(InitSlotCapacity); m_FreeStack = SlotArrayPool.Rent(InitSlotCapacity); m_AllNameHeadMap.Clear(); m_NameCursorMap.Clear(); m_SlotCount = 0; m_FreeTop = 0; m_UnusedHead = -1; m_UnusedTail = -1; m_LastBudgetScanStart = -1; } [Conditional("UNITY_EDITOR")] private void ValidateState() { #if UNITY_EDITOR && ENABLE_OBJECTPOOL_VALIDATION 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; if (!m_TargetMap.TryGetValue(target, 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; if (slot.SpawnCount == 0) { unusedCount++; if (!inUnusedList) { UnityEngine.Debug.LogError($"Object pool '{FullName}' unused list is inconsistent."); } } else { if (inUnusedList) { UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in unused list."); } } } 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); ref var slot = ref m_Slots[idx]; if (slot.IsAlive()) m_NameCursorMap.AddOrUpdate(slot.Obj.Name ?? string.Empty, idx); } private void MarkSlotUnavailable(int idx) { RemoveFromUnusedList(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 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; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void UpdateActiveState() { IsActive = m_TargetMap.Count > 0 || m_PendingReleaseCount > 0; } } } }