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_FreeStack; private int m_FreeTop; private readonly Dictionary m_TargetMap; private readonly Dictionary m_AllNameHeadMap; private readonly Dictionary m_AvailableNameHeadMap; private readonly Dictionary m_AvailableNameTailMap; 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 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 = new ObjectSlot[initCap]; m_FreeStack = new int[initCap]; m_TargetMap = new Dictionary(initCap, AlicizaX.ReferenceComparer.Instance); m_AllNameHeadMap = new Dictionary(initCap, StringComparer.Ordinal); m_AvailableNameHeadMap = new Dictionary(initCap, StringComparer.Ordinal); m_AvailableNameTailMap = new Dictionary(initCap, StringComparer.Ordinal); 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; } 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) throw new GameFrameworkException("AutoReleaseInterval is invalid."); m_AutoReleaseInterval = value; } } public override int Capacity { get => m_Capacity; set { if (value < 0) throw new GameFrameworkException("Capacity is invalid."); m_Capacity = value; if (Count > m_Capacity) MarkRelease(Count - m_Capacity); } } public override float ExpireTime { get => m_ExpireTime; set { if (value < 0f) throw new GameFrameworkException("ExpireTime is invalid."); 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) throw new GameFrameworkException("Object is invalid."); if (obj.Target == null) throw new GameFrameworkException("Object target is invalid."); if (m_TargetMap.TryGetValue(obj.Target, out int existingIdx) && m_Slots[existingIdx].IsAlive()) throw new GameFrameworkException( $"Target '{obj.Target.GetType().FullName}' is already registered in pool '{FullName}'."); 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[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; } m_AllNameHeadMap[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) throw new GameFrameworkException("Name is invalid."); 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)) throw new GameFrameworkException($"Object pool '{FullName}' available-name head is inconsistent."); 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) throw new GameFrameworkException("Name is invalid."); if (m_AllowMultiSpawn) return m_AllNameHeadMap.ContainsKey(name); return m_AvailableNameHeadMap.ContainsKey(name); } public void Unspawn(T obj) { if (obj == null) throw new GameFrameworkException("Object is invalid."); Unspawn(obj.Target); } public void Unspawn(object target) { if (target == null) throw new GameFrameworkException("Target is invalid."); if (!m_TargetMap.TryGetValue(target, out int idx)) { if (m_IsShuttingDown) return; throw new GameFrameworkException( $"Cannot find target in pool '{Name}', type='{target.GetType().FullName}'"); } 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) 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); } } 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(); 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) throw new GameFrameworkException("Results is invalid."); 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) throw new GameFrameworkException($"Object '{slot.Obj.Name}' spawn count < 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_Slots.Length * 2, InitSlotCapacity); Array.Resize(ref m_Slots, newCap); Array.Resize(ref m_FreeStack, 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); 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.PrevAvailableByName = -1; slot.NextAvailableByName = -1; slot.PrevUnused = -1; slot.NextUnused = -1; if (m_FreeTop >= m_FreeStack.Length) Array.Resize(ref m_FreeStack, m_FreeStack.Length * 2); 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) throw new GameFrameworkException($"Object pool '{FullName}' all-name chain is inconsistent."); if (next >= 0) m_AllNameHeadMap[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_Slots.Length <= InitSlotCapacity) return; m_Slots = new ObjectSlot[InitSlotCapacity]; m_FreeStack = new int[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() { 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) throw new GameFrameworkException($"Object pool '{FullName}' target index map is inconsistent."); string objectName = slot.Obj.Name ?? string.Empty; if (!m_AllNameHeadMap.TryGetValue(objectName, out int head)) throw new GameFrameworkException($"Object pool '{FullName}' all-name head is missing."); if (slot.PrevByName < 0 && head != i) throw new GameFrameworkException($"Object pool '{FullName}' all-name chain head is inconsistent."); if (slot.NextByName >= 0 && m_Slots[slot.NextByName].PrevByName != i) throw new GameFrameworkException($"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) throw new GameFrameworkException($"Object pool '{FullName}' unused list is inconsistent."); if (!m_AvailableNameHeadMap.TryGetValue(objectName, out int availableHead)) throw new GameFrameworkException($"Object pool '{FullName}' available-name head is missing."); inAvailableList = availableHead == i || slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0; if (!inAvailableList) throw new GameFrameworkException($"Object pool '{FullName}' available-name chain is inconsistent."); if (slot.NextAvailableByName >= 0 && m_Slots[slot.NextAvailableByName].PrevAvailableByName != i) throw new GameFrameworkException($"Object pool '{FullName}' available-name link is inconsistent."); } else { if (inUnusedList) throw new GameFrameworkException($"Object pool '{FullName}' spawned object exists in unused list."); if (slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0) throw new GameFrameworkException($"Object pool '{FullName}' spawned object exists in available chain."); } } if (aliveCount != m_TargetMap.Count) throw new GameFrameworkException($"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) throw new GameFrameworkException($"Object pool '{FullName}' unused chain contains invalid slot."); if (slot.PrevUnused != prevUnused) throw new GameFrameworkException($"Object pool '{FullName}' unused chain linkage is inconsistent."); walkUnusedCount++; prevUnused = current; current = slot.NextUnused; } if (walkUnusedCount != unusedCount) throw new GameFrameworkException($"Object pool '{FullName}' unused chain count is inconsistent."); } 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) throw new GameFrameworkException($"Object pool '{FullName}' available-name chain is inconsistent."); 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[objectName] = idx; } else { slot.PrevAvailableByName = -1; slot.NextAvailableByName = -1; m_AvailableNameHeadMap[objectName] = idx; m_AvailableNameTailMap[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[objectName] = next; else m_AvailableNameHeadMap.Remove(objectName); if (next >= 0) m_Slots[next].PrevAvailableByName = prev; else if (prev >= 0) m_AvailableNameTailMap[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; } } } }