com.alicizax.unity.framework/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs

965 lines
33 KiB
C#
Raw Normal View History

using System;
2025-10-11 15:18:09 +08:00
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using UnityEngine;
2025-10-11 15:18:09 +08:00
namespace AlicizaX.ObjectPool
{
internal sealed partial class ObjectPoolService
2025-10-11 15:18:09 +08:00
{
private sealed class ObjectPool<T> : ObjectPoolBase, IObjectPool<T> 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;
2026-04-27 12:06:09 +08:00
private ReferenceOpenHashMap m_TargetMap;
2026-04-24 14:33:26 +08:00
private StringOpenHashMap m_AllNameHeadMap;
private StringOpenHashMap m_NameCursorMap;
2025-10-11 15:18:09 +08:00
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;
2026-04-24 14:33:26 +08:00
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,
2026-04-27 12:06:09 +08:00
float autoReleaseInterval, int capacity, float expireTime, int priority)
2025-10-11 15:18:09 +08:00
: base(name)
{
int initCap = Math.Min(Math.Max(capacity, 1), InitSlotCapacity);
2026-04-24 14:33:26 +08:00
m_Slots = SlotArrayPool<ObjectSlot>.Rent(initCap);
m_FreeStack = SlotArrayPool<int>.Rent(initCap);
2026-04-27 12:06:09 +08:00
m_TargetMap = new ReferenceOpenHashMap(initCap);
2026-04-24 14:33:26 +08:00
m_AllNameHeadMap = new StringOpenHashMap(initCap);
m_NameCursorMap = new StringOpenHashMap(initCap);
2025-10-11 15:18:09 +08:00
m_AllowMultiSpawn = allowMultiSpawn;
m_AutoReleaseInterval = autoReleaseInterval;
m_Capacity = capacity;
m_ExpireTime = expireTime;
2025-10-11 15:18:09 +08:00
m_Priority = priority;
m_AutoReleaseTime = 0f;
m_PendingReleaseCount = 0;
m_ReleasePerFrameBudget = DefaultReleasePerFrame;
m_UnusedHead = -1;
m_UnusedTail = -1;
m_LastBudgetScanStart = -1;
m_IsShuttingDown = false;
2026-04-24 14:33:26 +08:00
m_ShrinkCounter = 0;
2025-10-11 15:18:09 +08:00
}
public override Type ObjectType => typeof(T);
public override int Count => m_TargetMap.Count;
public override bool AllowMultiSpawn => m_AllowMultiSpawn;
2025-10-11 15:18:09 +08:00
public override float AutoReleaseInterval
2025-10-11 15:18:09 +08:00
{
get => m_AutoReleaseInterval;
set
2025-10-11 15:18:09 +08:00
{
2026-04-24 14:33:26 +08:00
if (value < 0f)
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError("AutoReleaseInterval is invalid.");
#endif
return;
}
m_AutoReleaseInterval = value;
2025-10-11 15:18:09 +08:00
}
}
public override int Capacity
{
get => m_Capacity;
2025-10-11 15:18:09 +08:00
set
{
2026-04-24 14:33:26 +08:00
if (value < 0)
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError("Capacity is invalid.");
#endif
return;
}
2025-10-11 15:18:09 +08:00
m_Capacity = value;
if (Count > m_Capacity) MarkRelease(Count - m_Capacity);
2025-10-11 15:18:09 +08:00
}
}
public override float ExpireTime
{
get => m_ExpireTime;
2025-10-11 15:18:09 +08:00
set
{
2026-04-24 14:33:26 +08:00
if (value < 0f)
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError("ExpireTime is invalid.");
#endif
return;
}
2025-10-11 15:18:09 +08:00
m_ExpireTime = value;
}
}
public override int Priority
{
get => m_Priority;
set => m_Priority = value;
2025-10-11 15:18:09 +08:00
}
public override int ReleasePerFrameBudget
2025-10-11 15:18:09 +08:00
{
get => m_ReleasePerFrameBudget;
set => m_ReleasePerFrameBudget = Math.Max(1, value);
2025-10-11 15:18:09 +08:00
}
public void Register(T obj, bool spawned)
2025-10-11 15:18:09 +08:00
{
2026-04-24 14:33:26 +08:00
if (obj == null) return;
if (obj.Target == null) return;
2026-04-27 12:06:09 +08:00
if (m_TargetMap.TryGetValue(obj.Target, out int existingIdx) && m_Slots[existingIdx].IsAlive())
2026-04-24 14:33:26 +08:00
{
#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);
2026-04-27 12:06:09 +08:00
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);
}
2026-04-24 14:33:26 +08:00
m_AllNameHeadMap.AddOrUpdate(objectName, idx);
obj.LastUseTime = slot.LastUseTime;
if (spawned)
obj.OnSpawn();
else
MarkSlotAvailable(idx);
UpdateActiveState();
ValidateState();
2025-10-11 15:18:09 +08:00
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Spawn() => Spawn(string.Empty);
public T Spawn(string name)
2025-10-11 15:18:09 +08:00
{
if (name == null) name = string.Empty;
if (m_AllowMultiSpawn)
return SpawnAny(name);
int head = FindAvailableByName(name);
if (head < 0) return null;
2026-04-23 19:09:56 +08:00
ref var slot = ref m_Slots[head];
if (!slot.IsAlive() || slot.SpawnCount != 0 || !string.Equals(slot.Obj.Name, name, StringComparison.Ordinal))
2026-04-24 14:33:26 +08:00
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain is inconsistent.");
2026-04-24 14:33:26 +08:00
#endif
return null;
}
2025-10-11 15:18:09 +08:00
float now = Time.realtimeSinceStartup;
2026-04-23 19:09:56 +08:00
SpawnSlot(head, now);
ValidateState();
return slot.Obj;
2025-10-11 15:18:09 +08:00
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool CanSpawn() => CanSpawn(string.Empty);
2025-10-11 15:18:09 +08:00
public bool CanSpawn(string name)
2025-10-11 15:18:09 +08:00
{
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;
2025-10-11 15:18:09 +08:00
}
public void Unspawn(T obj)
{
2026-04-24 14:33:26 +08:00
if (obj == null) return;
2025-10-11 15:18:09 +08:00
Unspawn(obj.Target);
}
public void Unspawn(object target)
{
2026-04-24 14:33:26 +08:00
if (target == null) return;
2026-04-27 12:06:09 +08:00
if (!m_TargetMap.TryGetValue(target, out int idx))
2025-10-11 15:18:09 +08:00
{
if (m_IsShuttingDown) return;
2026-04-24 14:33:26 +08:00
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Cannot find target in pool '{Name}', type='{target.GetType().FullName}'");
#endif
return;
2025-10-11 15:18:09 +08:00
}
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)
2025-10-11 15:18:09 +08:00
{
int next = m_Slots[current].NextUnused;
ref var slot = ref m_Slots[current];
if (CanReleaseSlot(ref slot))
2025-10-11 15:18:09 +08:00
{
ReleaseSlot(current, false);
released++;
2025-10-11 15:18:09 +08:00
}
current = next;
2025-10-11 15:18:09 +08:00
}
if (released > 0)
2025-10-11 15:18:09 +08:00
{
m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released);
TrimSlotCountTail();
ShrinkStorageIfEmpty();
UpdateActiveState();
ValidateState();
2025-10-11 15:18:09 +08:00
}
}
internal override void Update(float elapseSeconds, float realElapseSeconds)
2025-10-11 15:18:09 +08:00
{
m_AutoReleaseTime += realElapseSeconds;
if (m_AutoReleaseTime >= m_AutoReleaseInterval)
2025-10-11 15:18:09 +08:00
{
m_AutoReleaseTime = 0f;
MarkRelease(Count - m_Capacity);
2025-10-11 15:18:09 +08:00
}
bool checkExpire = m_ExpireTime < float.MaxValue;
2026-04-24 14:33:26 +08:00
if (m_PendingReleaseCount <= 0 && !checkExpire)
{
TryProgressiveShrink();
UpdateActiveState();
2026-04-24 14:33:26 +08:00
return;
}
float now = Time.realtimeSinceStartup;
float expireThreshold = checkExpire ? now - m_ExpireTime : float.MinValue;
if (m_PendingReleaseCount > 0)
2025-10-11 15:18:09 +08:00
{
int releaseBudget = Math.Min(m_ReleasePerFrameBudget, m_PendingReleaseCount);
int releasedByBudget = ReleaseUnused(releaseBudget, false, float.MinValue);
m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - releasedByBudget);
2025-10-11 15:18:09 +08:00
}
else if (checkExpire)
2025-10-11 15:18:09 +08:00
{
ReleaseExpired(m_ReleasePerFrameBudget, expireThreshold);
2025-10-11 15:18:09 +08:00
}
2026-04-24 14:33:26 +08:00
TryProgressiveShrink();
UpdateActiveState();
2026-04-24 14:33:26 +08:00
}
private void TryProgressiveShrink()
{
m_ShrinkCounter++;
if (m_ShrinkCounter < ShrinkCheckInterval)
return;
m_ShrinkCounter = 0;
TrimSlotCountTail();
2026-04-27 12:06:09 +08:00
int slotArrayLen = m_Slots.Length;
int aliveCount = m_TargetMap.Count;
if (aliveCount == 0 || slotArrayLen <= InitSlotCapacity)
2026-04-24 14:33:26 +08:00
return;
float usageRatio = (float)aliveCount / slotArrayLen;
2026-04-24 14:33:26 +08:00
if (usageRatio < 0.25f)
{
int targetCapacity = Math.Max(NextPowerOf2(Math.Max(m_SlotCount, aliveCount)), InitSlotCapacity);
2026-04-27 12:06:09 +08:00
if (targetCapacity < slotArrayLen && m_SlotCount <= targetCapacity)
2026-04-24 14:33:26 +08:00
{
var newSlots = SlotArrayPool<ObjectSlot>.Rent(targetCapacity);
var newFreeStack = SlotArrayPool<int>.Rent(targetCapacity);
2026-04-27 12:06:09 +08:00
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];
}
2026-04-24 14:33:26 +08:00
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_Slots = newSlots;
m_FreeStack = newFreeStack;
2026-04-27 12:06:09 +08:00
m_FreeTop = newFreeTop;
2026-04-24 14:33:26 +08:00
}
}
2025-10-11 15:18:09 +08:00
}
internal override void Shutdown()
2025-10-11 15:18:09 +08:00
{
m_IsShuttingDown = true;
for (int i = 0; i < m_SlotCount; i++)
2025-10-11 15:18:09 +08:00
{
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);
2025-10-11 15:18:09 +08:00
}
m_TargetMap.Clear();
m_AllNameHeadMap.Clear();
m_NameCursorMap.Clear();
2026-04-24 14:33:26 +08:00
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.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();
2025-10-11 15:18:09 +08:00
}
2026-04-23 19:09:56 +08:00
public override int GetAllObjectInfos(ObjectInfo[] results)
2025-10-11 15:18:09 +08:00
{
2026-04-24 14:33:26 +08:00
if (results == null)
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError("Results is invalid.");
#endif
return 0;
}
2026-04-23 19:09:56 +08:00
int write = 0;
2026-04-23 19:09:56 +08:00
int capacity = results.Length;
for (int i = 0; i < m_SlotCount; i++)
2025-10-11 15:18:09 +08:00
{
ref var slot = ref m_Slots[i];
if (!slot.IsAlive()) continue;
2025-10-11 15:18:09 +08:00
2026-04-23 19:09:56 +08:00
if (write < capacity)
{
results[write] = new ObjectInfo(slot.Obj.Name, slot.Obj.Locked,
slot.Obj.CustomCanReleaseFlag,
slot.Obj.LastUseTime, slot.SpawnCount);
}
2025-10-11 15:18:09 +08:00
2026-04-23 19:09:56 +08:00
write++;
2025-10-11 15:18:09 +08:00
}
2026-04-23 19:09:56 +08:00
return write;
}
2025-10-11 15:18:09 +08:00
internal override void OnLowMemory()
{
ReleaseAllUnused();
2025-10-11 15:18:09 +08:00
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SpawnSlot(int idx, float now)
2025-10-11 15:18:09 +08:00
{
ref var slot = ref m_Slots[idx];
if (slot.SpawnCount == 0)
MarkSlotUnavailable(idx);
2025-10-11 15:18:09 +08:00
slot.SpawnCount++;
slot.LastUseTime = now;
slot.Obj.LastUseTime = now;
slot.Obj.OnSpawn();
}
2025-10-11 15:18:09 +08:00
[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)
2026-04-24 14:33:26 +08:00
{
#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);
2025-10-11 15:18:09 +08:00
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void MarkRelease(int count)
2025-10-11 15:18:09 +08:00
{
if (count > 0)
m_PendingReleaseCount = Math.Max(m_PendingReleaseCount, count);
2025-10-11 15:18:09 +08:00
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int AllocSlot()
2025-10-11 15:18:09 +08:00
{
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++;
2025-10-11 15:18:09 +08:00
}
private void GrowSlots()
2025-10-11 15:18:09 +08:00
{
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;
2026-04-24 14:33:26 +08:00
var newSlots = SlotArrayPool<ObjectSlot>.Rent(newCap);
var newFreeStack = SlotArrayPool<int>.Rent(newCap);
Array.Copy(m_Slots, 0, newSlots, 0, m_SlotCount);
Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop);
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_Slots = newSlots;
m_FreeStack = newFreeStack;
2025-10-11 15:18:09 +08:00
}
private void ReleaseSlot(int idx, bool compactStorage = true)
2025-10-11 15:18:09 +08:00
{
ref var slot = ref m_Slots[idx];
if (!slot.IsAlive()) return;
if (slot.SpawnCount > 0) return;
2026-03-23 20:50:11 +08:00
T obj = slot.Obj;
MarkSlotUnavailable(idx);
2025-10-11 15:18:09 +08:00
RemoveFromAllNameChain(idx);
2026-04-27 12:06:09 +08:00
m_TargetMap.Remove(obj.Target);
2025-10-11 15:18:09 +08:00
obj.Release(false);
MemoryPool.Release(obj);
2026-03-23 20:50:11 +08:00
slot.Obj = null;
slot.SetAlive(false);
slot.SpawnCount = 0;
slot.PrevByName = -1;
slot.NextByName = -1;
slot.PrevUnused = -1;
slot.NextUnused = -1;
2025-10-11 15:18:09 +08:00
2026-04-27 12:06:09 +08:00
if (m_FreeTop >= m_FreeStack.Length)
2026-04-24 14:33:26 +08:00
{
2026-04-27 12:06:09 +08:00
int newCap = m_FreeStack.Length * 2;
2026-04-24 14:33:26 +08:00
var newFreeStack = SlotArrayPool<int>.Rent(newCap);
Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_FreeStack = newFreeStack;
}
m_FreeStack[m_FreeTop++] = idx;
2025-10-11 15:18:09 +08:00
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;
2025-10-11 15:18:09 +08:00
}
private void RemoveFromAllNameChain(int idx)
2025-10-11 15:18:09 +08:00
{
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
2025-10-11 15:18:09 +08:00
{
if (head != idx)
2026-04-24 14:33:26 +08:00
{
#if UNITY_EDITOR
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain is inconsistent.");
#endif
return;
}
if (next >= 0)
2026-04-24 14:33:26 +08:00
m_AllNameHeadMap.AddOrUpdate(objectName, next);
else
m_AllNameHeadMap.Remove(objectName);
2025-10-11 15:18:09 +08:00
}
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;
2025-10-11 15:18:09 +08:00
}
private int ReleaseUnused(int maxReleaseCount, bool requireExpired, float expireThreshold)
2025-10-11 15:18:09 +08:00
{
int released = 0;
int current = requireExpired ? m_UnusedHead : GetBudgetScanStart();
while (current >= 0 && released < maxReleaseCount)
2025-10-11 15:18:09 +08:00
{
ref var slot = ref m_Slots[current];
int next = slot.NextUnused;
2025-10-11 15:18:09 +08:00
if (requireExpired && slot.LastUseTime > expireThreshold)
{
current = next;
continue;
}
2025-10-11 15:18:09 +08:00
if (CanReleaseSlot(ref slot))
{
ReleaseSlot(current, false);
released++;
}
current = next;
2025-10-11 15:18:09 +08:00
}
if (!requireExpired)
2025-10-11 15:18:09 +08:00
{
m_LastBudgetScanStart = current >= 0 ? current : m_UnusedHead;
2025-10-11 15:18:09 +08:00
}
if (released > 0)
{
TrimSlotCountTail();
ShrinkStorageIfEmpty();
}
return released;
2025-10-11 15:18:09 +08:00
}
private void ReleaseExpired(int maxReleaseCount, float expireThreshold)
2026-03-23 20:50:11 +08:00
{
ReleaseUnused(maxReleaseCount, true, expireThreshold);
}
2026-03-23 20:50:11 +08:00
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int GetBudgetScanStart()
{
if (m_LastBudgetScanStart >= 0)
2026-03-23 20:50:11 +08:00
{
ref var slot = ref m_Slots[m_LastBudgetScanStart];
if (slot.IsAlive() && slot.SpawnCount == 0)
{
return m_LastBudgetScanStart;
}
2026-03-23 20:50:11 +08:00
}
return m_UnusedHead;
}
2026-03-23 20:50:11 +08:00
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool CanReleaseSlot(ref ObjectSlot slot)
{
return slot.IsAlive()
&& slot.SpawnCount == 0
&& !slot.Obj.Locked
&& slot.Obj.CustomCanReleaseFlag;
2026-03-23 20:50:11 +08:00
}
private void ShrinkStorageIfEmpty()
2026-03-23 20:50:11 +08:00
{
2026-04-27 12:06:09 +08:00
if (m_TargetMap.Count > 0 || m_Slots.Length <= InitSlotCapacity)
return;
2026-03-23 20:50:11 +08:00
2026-04-24 14:33:26 +08:00
SlotArrayPool<ObjectSlot>.Return(m_Slots, true);
SlotArrayPool<int>.Return(m_FreeStack, true);
m_Slots = SlotArrayPool<ObjectSlot>.Rent(InitSlotCapacity);
m_FreeStack = SlotArrayPool<int>.Rent(InitSlotCapacity);
m_AllNameHeadMap.Clear();
m_NameCursorMap.Clear();
m_SlotCount = 0;
m_FreeTop = 0;
m_UnusedHead = -1;
m_UnusedTail = -1;
m_LastBudgetScanStart = -1;
}
2026-03-23 20:50:11 +08:00
[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++)
2026-03-23 20:50:11 +08:00
{
ref var slot = ref m_Slots[i];
if (!slot.IsAlive())
continue;
aliveCount++;
object target = slot.Obj.Target;
2026-04-27 12:06:09 +08:00
if (!m_TargetMap.TryGetValue(target, out int mappedIdx) || mappedIdx != i)
2026-04-24 14:33:26 +08:00
{
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))
2026-04-24 14:33:26 +08:00
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name head is missing.");
continue;
}
if (slot.PrevByName < 0 && head != i)
2026-04-24 14:33:26 +08:00
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain head is inconsistent.");
}
if (slot.NextByName >= 0 && m_Slots[slot.NextByName].PrevByName != i)
2026-04-24 14:33:26 +08:00
{
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)
2026-03-23 20:50:11 +08:00
{
unusedCount++;
if (!inUnusedList)
2026-04-24 14:33:26 +08:00
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused list is inconsistent.");
}
}
else
{
if (inUnusedList)
2026-04-24 14:33:26 +08:00
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in unused list.");
}
}
2026-03-23 20:50:11 +08:00
}
if (aliveCount != m_TargetMap.Count)
2026-04-24 14:33:26 +08:00
{
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)
2026-04-24 14:33:26 +08:00
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain contains invalid slot.");
}
if (slot.PrevUnused != prevUnused)
2026-04-24 14:33:26 +08:00
{
UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain linkage is inconsistent.");
}
walkUnusedCount++;
prevUnused = current;
current = slot.NextUnused;
}
if (walkUnusedCount != unusedCount)
2026-04-24 14:33:26 +08:00
{
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;
2026-03-23 20:50:11 +08:00
}
private void RemoveFromUnusedList(int idx)
2025-10-11 15:18:09 +08:00
{
ref var slot = ref m_Slots[idx];
if (m_UnusedHead != idx && slot.PrevUnused < 0 && slot.NextUnused < 0)
return;
2025-10-11 15:18:09 +08:00
int prev = slot.PrevUnused;
int next = slot.NextUnused;
2025-10-11 15:18:09 +08:00
if (prev >= 0)
m_Slots[prev].NextUnused = next;
else
m_UnusedHead = next;
2025-10-11 15:18:09 +08:00
if (next >= 0)
m_Slots[next].PrevUnused = prev;
else
m_UnusedTail = prev;
slot.PrevUnused = -1;
slot.NextUnused = -1;
2026-03-23 20:50:11 +08:00
if (m_LastBudgetScanStart == idx)
m_LastBudgetScanStart = next >= 0 ? next : m_UnusedHead;
2025-10-11 15:18:09 +08:00
}
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);
2026-04-23 19:09:56 +08:00
ValidateState();
return slot.Obj;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateActiveState()
{
IsActive = m_TargetMap.Count > 0 || m_PendingReleaseCount > 0;
}
2025-10-11 15:18:09 +08:00
}
}
}