using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; namespace AlicizaX.ObjectPool { internal sealed partial class ObjectPoolService { /// /// Unity main-thread object pool. Array-slot storage + IntOpenHashMap /// + object-based spawn/unspawn + frame-spread release. /// private sealed class ObjectPool : ObjectPoolBase, IObjectPool where T : ObjectBase { private struct ObjectSlot { public T Obj; public int SpawnCount; public float LastUseTime; public int NameHash; public int NextSameName; 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 IntOpenHashMap m_NameMap; 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_NameMap = new IntOpenHashMap(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; } 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.NameHash = (obj.Name ?? string.Empty).GetHashCode(); slot.NextSameName = -1; slot.PrevUnused = -1; slot.NextUnused = -1; slot.SetAlive(true); m_TargetMap[obj.Target] = idx; if (m_NameMap.TryGetValue(slot.NameHash, out int existingHead)) slot.NextSameName = existingHead; m_NameMap.AddOrUpdate(slot.NameHash, idx); obj.LastUseTime = slot.LastUseTime; if (spawned) obj.OnSpawn(); else AddToUnusedList(idx); if (Count > m_Capacity) MarkRelease(Count - m_Capacity); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public T Spawn() => Spawn(string.Empty); public T Spawn(string name) { if (name == null) throw new GameFrameworkException("Name is invalid."); int nameHash = name.GetHashCode(); if (!m_NameMap.TryGetValue(nameHash, out int head)) return null; float now = Time.realtimeSinceStartup; int current = head; while (current >= 0) { ref var slot = ref m_Slots[current]; if (slot.IsAlive() && string.Equals(slot.Obj.Name, name) && (m_AllowMultiSpawn || slot.SpawnCount == 0)) { SpawnSlot(current, now); return slot.Obj; } current = slot.NextSameName; } return null; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CanSpawn() => CanSpawn(string.Empty); public bool CanSpawn(string name) { if (name == null) throw new GameFrameworkException("Name is invalid."); int nameHash = name.GetHashCode(); if (!m_NameMap.TryGetValue(nameHash, out int head)) return false; int current = head; while (current >= 0) { ref var slot = ref m_Slots[current]; if (slot.IsAlive() && string.Equals(slot.Obj.Name, name) && (m_AllowMultiSpawn || slot.SpawnCount == 0)) return true; current = slot.NextSameName; } return false; } 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); } public override void Release() { MarkRelease(Count - m_Capacity); } public override void Release(int toReleaseCount) { MarkRelease(toReleaseCount); } public override void ReleaseAllUnused() { // Strong clear: release every currently unused releasable object, // ignoring frame budget and capacity target. 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(); } } 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_NameMap.Clear(); m_SlotCount = 0; m_FreeTop = 0; m_PendingReleaseCount = 0; m_UnusedHead = -1; m_UnusedTail = -1; m_LastBudgetScanStart = -1; m_IsShuttingDown = false; } public override ObjectInfo[] GetAllObjectInfos() { var list = new ObjectInfo[m_TargetMap.Count]; int write = 0; for (int i = 0; i < m_SlotCount && write < list.Length; i++) { ref var slot = ref m_Slots[i]; if (!slot.IsAlive()) continue; list[write++] = new ObjectInfo(slot.Obj.Name, slot.Obj.Locked, slot.Obj.CustomCanReleaseFlag, slot.Obj.LastUseTime, slot.SpawnCount); } return list; } public override void GetAllObjectInfos(List results) { if (results == null) throw new GameFrameworkException("Results is invalid."); results.Clear(); for (int i = 0; i < m_SlotCount; i++) { ref var slot = ref m_Slots[i]; if (!slot.IsAlive()) continue; results.Add(new ObjectInfo(slot.Obj.Name, slot.Obj.Locked, slot.Obj.CustomCanReleaseFlag, slot.Obj.LastUseTime, slot.SpawnCount)); } } 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) RemoveFromUnusedList(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) AddToUnusedList(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) RemoveFromUnusedList(idx); RemoveFromNameChain(idx); m_TargetMap.Remove(obj.Target); obj.Release(false); MemoryPool.Release(obj); slot.Obj = null; slot.SetAlive(false); slot.SpawnCount = 0; slot.NameHash = 0; slot.NextSameName = -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 RemoveFromNameChain(int idx) { ref var slot = ref m_Slots[idx]; int nameHash = slot.NameHash; if (!m_NameMap.TryGetValue(nameHash, out int head)) return; if (head == idx) { m_NameMap.Remove(nameHash); if (slot.NextSameName >= 0) m_NameMap.AddOrUpdate(nameHash, slot.NextSameName); } else { int current = head; while (current >= 0) { ref var chainSlot = ref m_Slots[current]; if (chainSlot.NextSameName == idx) { chainSlot.NextSameName = slot.NextSameName; break; } current = chainSlot.NextSameName; } } slot.NextSameName = -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_NameMap = new IntOpenHashMap(InitSlotCapacity); m_SlotCount = 0; m_FreeTop = 0; m_UnusedHead = -1; m_UnusedTail = -1; m_LastBudgetScanStart = -1; } private void AddToUnusedList(int idx) { ref var slot = ref m_Slots[idx]; if (m_UnusedHead == idx || slot.PrevUnused >= 0 || slot.NextUnused >= 0) return; if (m_UnusedTail >= 0 && m_Slots[m_UnusedTail].LastUseTime <= slot.LastUseTime) { m_Slots[m_UnusedTail].NextUnused = idx; slot.PrevUnused = m_UnusedTail; slot.NextUnused = -1; m_UnusedTail = idx; } else { int current = m_UnusedHead; int prev = -1; while (current >= 0 && m_Slots[current].LastUseTime <= slot.LastUseTime) { prev = current; current = m_Slots[current].NextUnused; } slot.PrevUnused = prev; slot.NextUnused = current; if (prev >= 0) m_Slots[prev].NextUnused = idx; else m_UnusedHead = idx; if (current >= 0) m_Slots[current].PrevUnused = idx; else m_UnusedTail = idx; if (m_UnusedTail < 0) 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; } } } }