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

610 lines
22 KiB
C#
Raw Normal View History

using System;
2025-10-11 15:18:09 +08:00
using System.Collections.Generic;
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 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<object, int> m_TargetMap;
private IntOpenHashMap m_NameMap;
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;
private const int DefaultReleasePerFrame = 8;
private const int InitSlotCapacity = 16;
public ObjectPool(string name, bool allowMultiSpawn,
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);
m_Slots = new ObjectSlot[initCap];
m_FreeStack = new int[initCap];
m_TargetMap = new Dictionary<object, int>(initCap, AlicizaX.ReferenceComparer<object>.Instance);
m_NameMap = new IntOpenHashMap(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;
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
{
if (value < 0f) throw new GameFrameworkException("AutoReleaseInterval is invalid.");
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
{
if (value < 0) throw new GameFrameworkException("Capacity is invalid.");
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
{
if (value < 0f) throw new GameFrameworkException("ExpireTime is invalid.");
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
{
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);
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) throw new GameFrameworkException("Name is invalid.");
int nameHash = name.GetHashCode();
if (!m_NameMap.TryGetValue(nameHash, out int head)) return null;
2025-10-11 15:18:09 +08:00
float now = Time.realtimeSinceStartup;
int current = head;
while (current >= 0)
2025-10-11 15:18:09 +08:00
{
ref var slot = ref m_Slots[current];
if (slot.IsAlive() && string.Equals(slot.Obj.Name, name)
&& (m_AllowMultiSpawn || slot.SpawnCount == 0))
2025-10-11 15:18:09 +08:00
{
SpawnSlot(current, now);
return slot.Obj;
2025-10-11 15:18:09 +08:00
}
current = slot.NextSameName;
2025-10-11 15:18:09 +08:00
}
return null;
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) throw new GameFrameworkException("Name is invalid.");
int nameHash = name.GetHashCode();
if (!m_NameMap.TryGetValue(nameHash, out int head)) return false;
2025-10-11 15:18:09 +08:00
int current = head;
while (current >= 0)
2025-10-11 15:18:09 +08:00
{
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;
2025-10-11 15:18:09 +08:00
}
return false;
2025-10-11 15:18:09 +08:00
}
public void Unspawn(T obj)
{
if (obj == null) throw new GameFrameworkException("Object is invalid.");
2025-10-11 15:18:09 +08:00
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))
2025-10-11 15:18:09 +08:00
{
if (m_IsShuttingDown) return;
throw new GameFrameworkException(
$"Cannot find target in pool '{Name}', type='{target.GetType().FullName}'");
2025-10-11 15:18:09 +08:00
}
UnspawnSlot(idx);
}
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);
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);
ShrinkStorageIfEmpty();
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;
if (m_PendingReleaseCount <= 0 && !checkExpire) 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
}
}
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_NameMap.Clear();
m_SlotCount = 0;
m_FreeTop = 0;
m_PendingReleaseCount = 0;
m_UnusedHead = -1;
m_UnusedTail = -1;
m_LastBudgetScanStart = -1;
m_IsShuttingDown = false;
2025-10-11 15:18:09 +08:00
}
public override ObjectInfo[] GetAllObjectInfos()
2025-10-11 15:18:09 +08:00
{
var list = new ObjectInfo[m_TargetMap.Count];
int write = 0;
for (int i = 0; i < m_SlotCount && write < list.Length; i++)
2025-10-11 15:18:09 +08:00
{
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);
2025-10-11 15:18:09 +08:00
}
return list;
2025-10-11 15:18:09 +08:00
}
public override void GetAllObjectInfos(List<ObjectInfo> results)
2025-10-11 15:18:09 +08:00
{
if (results == null) throw new GameFrameworkException("Results is invalid.");
results.Clear();
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;
results.Add(new ObjectInfo(slot.Obj.Name, slot.Obj.Locked,
slot.Obj.CustomCanReleaseFlag,
slot.Obj.LastUseTime, slot.SpawnCount));
2025-10-11 15:18:09 +08:00
}
}
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)
RemoveFromUnusedList(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)
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);
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();
return m_SlotCount++;
2025-10-11 15:18:09 +08:00
}
private void GrowSlots()
2025-10-11 15:18:09 +08:00
{
int newCap = Math.Max(m_Slots.Length * 2, InitSlotCapacity);
Array.Resize(ref m_Slots, newCap);
Array.Resize(ref m_FreeStack, newCap);
2025-10-11 15:18:09 +08:00
}
private void ReleaseSlot(int idx)
2025-10-11 15:18:09 +08:00
{
ref var slot = ref m_Slots[idx];
if (!slot.IsAlive()) return;
2026-03-23 20:50:11 +08:00
T obj = slot.Obj;
if (slot.SpawnCount == 0)
RemoveFromUnusedList(idx);
2025-10-11 15:18:09 +08:00
RemoveFromNameChain(idx);
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.NameHash = 0;
slot.NextSameName = -1;
slot.PrevUnused = -1;
slot.NextUnused = -1;
2025-10-11 15:18:09 +08:00
if (m_FreeTop >= m_FreeStack.Length)
Array.Resize(ref m_FreeStack, m_FreeStack.Length * 2);
m_FreeStack[m_FreeTop++] = idx;
2025-10-11 15:18:09 +08:00
ShrinkStorageIfEmpty();
2025-10-11 15:18:09 +08:00
}
private void RemoveFromNameChain(int idx)
2025-10-11 15:18:09 +08:00
{
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
2025-10-11 15:18:09 +08:00
{
int current = head;
while (current >= 0)
2025-10-11 15:18:09 +08:00
{
ref var chainSlot = ref m_Slots[current];
if (chainSlot.NextSameName == idx)
{
chainSlot.NextSameName = slot.NextSameName;
break;
}
current = chainSlot.NextSameName;
2025-10-11 15:18:09 +08:00
}
}
slot.NextSameName = -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)
{
break;
}
2025-10-11 15:18:09 +08:00
if (CanReleaseSlot(ref slot))
{
ReleaseSlot(current);
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
}
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
{
if (m_TargetMap.Count > 0 || m_Slots.Length <= InitSlotCapacity)
return;
2026-03-23 20:50:11 +08:00
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;
}
2026-03-23 20:50:11 +08:00
private void AddToUnusedList(int idx)
{
ref var slot = ref m_Slots[idx];
if (m_UnusedHead == idx || slot.PrevUnused >= 0 || slot.NextUnused >= 0)
2026-03-23 20:50:11 +08:00
return;
if (m_UnusedTail >= 0 && m_Slots[m_UnusedTail].LastUseTime <= slot.LastUseTime)
2026-03-23 20:50:11 +08:00
{
m_Slots[m_UnusedTail].NextUnused = idx;
slot.PrevUnused = m_UnusedTail;
slot.NextUnused = -1;
m_UnusedTail = idx;
2026-03-23 20:50:11 +08:00
}
else
2026-03-23 20:50:11 +08:00
{
int current = m_UnusedHead;
int prev = -1;
while (current >= 0 && m_Slots[current].LastUseTime <= slot.LastUseTime)
2026-03-23 20:50:11 +08:00
{
prev = current;
current = m_Slots[current].NextUnused;
2026-03-23 20:50:11 +08:00
}
slot.PrevUnused = prev;
slot.NextUnused = current;
2026-03-23 20:50:11 +08:00
if (prev >= 0)
m_Slots[prev].NextUnused = idx;
else
m_UnusedHead = idx;
2026-03-23 20:50:11 +08:00
if (current >= 0)
m_Slots[current].PrevUnused = idx;
else
m_UnusedTail = idx;
if (m_UnusedTail < 0)
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
}
}
}
}