2026-03-26 16:14:05 +08:00
|
|
|
using System;
|
2025-10-11 15:18:09 +08:00
|
|
|
using System.Collections.Generic;
|
2026-04-22 13:04:31 +08:00
|
|
|
using System.Runtime.CompilerServices;
|
|
|
|
|
using UnityEngine;
|
2025-10-11 15:18:09 +08:00
|
|
|
|
|
|
|
|
namespace AlicizaX.ObjectPool
|
|
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
internal sealed partial class ObjectPoolService
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
|
|
|
|
private sealed class ObjectPool<T> : ObjectPoolBase, IObjectPool<T> where T : ObjectBase
|
|
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
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;
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
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)
|
|
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
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;
|
2026-04-22 13:04:31 +08:00
|
|
|
m_Capacity = capacity;
|
|
|
|
|
m_ExpireTime = expireTime;
|
2025-10-11 15:18:09 +08:00
|
|
|
m_Priority = priority;
|
|
|
|
|
m_AutoReleaseTime = 0f;
|
2026-04-22 13:04:31 +08:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +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
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
public override float AutoReleaseInterval
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
get => m_AutoReleaseInterval;
|
|
|
|
|
set
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
get => m_Capacity;
|
2025-10-11 15:18:09 +08:00
|
|
|
set
|
|
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
if (value < 0) throw new GameFrameworkException("Capacity is invalid.");
|
2025-10-11 15:18:09 +08:00
|
|
|
m_Capacity = value;
|
2026-04-22 13:04:31 +08:00
|
|
|
if (Count > m_Capacity) MarkRelease(Count - m_Capacity);
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override float ExpireTime
|
|
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
get => m_ExpireTime;
|
2025-10-11 15:18:09 +08:00
|
|
|
set
|
|
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
if (value < 0f) throw new GameFrameworkException("ExpireTime is invalid.");
|
2025-10-11 15:18:09 +08:00
|
|
|
m_ExpireTime = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override int Priority
|
|
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
get => m_Priority;
|
|
|
|
|
set => m_Priority = value;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
public override int ReleasePerFrameBudget
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
get => m_ReleasePerFrameBudget;
|
|
|
|
|
set => m_ReleasePerFrameBudget = Math.Max(1, value);
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
public void Register(T obj, bool spawned)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
public T Spawn() => Spawn(string.Empty);
|
|
|
|
|
|
|
|
|
|
public T Spawn(string name)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
float now = Time.realtimeSinceStartup;
|
|
|
|
|
int current = head;
|
|
|
|
|
while (current >= 0)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
SpawnSlot(current, now);
|
|
|
|
|
return slot.Obj;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
2026-04-22 13:04:31 +08:00
|
|
|
|
|
|
|
|
current = slot.NextSameName;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
return null;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
public bool CanSpawn() => CanSpawn(string.Empty);
|
2025-10-11 15:18:09 +08:00
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
public bool CanSpawn(string name)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
int current = head;
|
|
|
|
|
while (current >= 0)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
return false;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Unspawn(T obj)
|
|
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
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)
|
|
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
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
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +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
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
ReleaseSlot(current);
|
|
|
|
|
released++;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
2026-04-22 13:04:31 +08:00
|
|
|
current = next;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
if (released > 0)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
m_PendingReleaseCount = Math.Max(0, m_PendingReleaseCount - released);
|
|
|
|
|
ShrinkStorageIfEmpty();
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
internal override void Update(float elapseSeconds, float realElapseSeconds)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
m_AutoReleaseTime += realElapseSeconds;
|
|
|
|
|
if (m_AutoReleaseTime >= m_AutoReleaseInterval)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
m_AutoReleaseTime = 0f;
|
|
|
|
|
MarkRelease(Count - m_Capacity);
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +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
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
}
|
2026-04-22 13:04:31 +08:00
|
|
|
else if (checkExpire)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
ReleaseExpired(m_ReleasePerFrameBudget, expireThreshold);
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
internal override void Shutdown()
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
m_IsShuttingDown = true;
|
|
|
|
|
for (int i = 0; i < m_SlotCount; i++)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
public override ObjectInfo[] GetAllObjectInfos()
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
return list;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
public override void GetAllObjectInfos(List<ObjectInfo> results)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
}
|
2026-04-22 13:04:31 +08:00
|
|
|
}
|
2025-10-11 15:18:09 +08:00
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
internal override void OnLowMemory()
|
|
|
|
|
{
|
|
|
|
|
ReleaseAllUnused();
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
private void SpawnSlot(int idx, float now)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
ref var slot = ref m_Slots[idx];
|
|
|
|
|
if (slot.SpawnCount == 0)
|
|
|
|
|
RemoveFromUnusedList(idx);
|
2025-10-11 15:18:09 +08:00
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
slot.SpawnCount++;
|
|
|
|
|
slot.LastUseTime = now;
|
|
|
|
|
slot.Obj.LastUseTime = now;
|
|
|
|
|
slot.Obj.OnSpawn();
|
|
|
|
|
}
|
2025-10-11 15:18:09 +08:00
|
|
|
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
private void MarkRelease(int count)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
if (count > 0)
|
|
|
|
|
m_PendingReleaseCount = Math.Max(m_PendingReleaseCount, count);
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
private int AllocSlot()
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
private void GrowSlots()
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
private void ReleaseSlot(int idx)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
ref var slot = ref m_Slots[idx];
|
|
|
|
|
if (!slot.IsAlive()) return;
|
2026-03-23 20:50:11 +08:00
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
T obj = slot.Obj;
|
|
|
|
|
if (slot.SpawnCount == 0)
|
|
|
|
|
RemoveFromUnusedList(idx);
|
2025-10-11 15:18:09 +08:00
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
RemoveFromNameChain(idx);
|
|
|
|
|
m_TargetMap.Remove(obj.Target);
|
2025-10-11 15:18:09 +08:00
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
obj.Release(false);
|
|
|
|
|
MemoryPool.Release(obj);
|
2026-03-23 20:50:11 +08:00
|
|
|
|
2026-04-22 13:04:31 +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
|
|
|
|
2026-04-22 13:04:31 +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
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
ShrinkStorageIfEmpty();
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
private void RemoveFromNameChain(int idx)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
ref var slot = ref m_Slots[idx];
|
|
|
|
|
int nameHash = slot.NameHash;
|
|
|
|
|
if (!m_NameMap.TryGetValue(nameHash, out int head)) return;
|
2026-03-26 17:53:07 +08:00
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
if (head == idx)
|
2026-03-26 17:53:07 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
m_NameMap.Remove(nameHash);
|
|
|
|
|
if (slot.NextSameName >= 0)
|
|
|
|
|
m_NameMap.AddOrUpdate(nameHash, slot.NextSameName);
|
2026-03-26 17:53:07 +08:00
|
|
|
}
|
2026-04-22 13:04:31 +08:00
|
|
|
else
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
int current = head;
|
|
|
|
|
while (current >= 0)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
slot.NextSameName = -1;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
private int ReleaseUnused(int maxReleaseCount, bool requireExpired, float expireThreshold)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
int released = 0;
|
|
|
|
|
int current = requireExpired ? m_UnusedHead : GetBudgetScanStart();
|
|
|
|
|
|
|
|
|
|
while (current >= 0 && released < maxReleaseCount)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
ref var slot = ref m_Slots[current];
|
|
|
|
|
int next = slot.NextUnused;
|
2025-10-11 15:18:09 +08:00
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
if (requireExpired && slot.LastUseTime > expireThreshold)
|
|
|
|
|
{
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-10-11 15:18:09 +08:00
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
if (CanReleaseSlot(ref slot))
|
|
|
|
|
{
|
|
|
|
|
ReleaseSlot(current);
|
|
|
|
|
released++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current = next;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
if (!requireExpired)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
m_LastBudgetScanStart = current >= 0 ? current : m_UnusedHead;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
return released;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
private void ReleaseExpired(int maxReleaseCount, float expireThreshold)
|
2026-03-23 20:50:11 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
ReleaseUnused(maxReleaseCount, true, expireThreshold);
|
|
|
|
|
}
|
2026-03-23 20:50:11 +08:00
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
|
|
|
private int GetBudgetScanStart()
|
|
|
|
|
{
|
|
|
|
|
if (m_LastBudgetScanStart >= 0)
|
2026-03-23 20:50:11 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
return m_UnusedHead;
|
|
|
|
|
}
|
2026-03-23 20:50:11 +08:00
|
|
|
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
private void ShrinkStorageIfEmpty()
|
2026-03-23 20:50:11 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
if (m_TargetMap.Count > 0 || m_Slots.Length <= InitSlotCapacity)
|
|
|
|
|
return;
|
2026-03-23 20:50:11 +08:00
|
|
|
|
2026-04-22 13:04:31 +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
|
|
|
|
2026-04-22 13:04:31 +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;
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
if (m_UnusedTail >= 0 && m_Slots[m_UnusedTail].LastUseTime <= slot.LastUseTime)
|
2026-03-23 20:50:11 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
}
|
2026-04-22 13:04:31 +08:00
|
|
|
else
|
2026-03-23 20:50:11 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
{
|
2026-04-22 13:04:31 +08:00
|
|
|
prev = current;
|
|
|
|
|
current = m_Slots[current].NextUnused;
|
2026-03-23 20:50:11 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
slot.PrevUnused = prev;
|
|
|
|
|
slot.NextUnused = current;
|
2026-03-23 20:50:11 +08:00
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
if (prev >= 0)
|
|
|
|
|
m_Slots[prev].NextUnused = idx;
|
|
|
|
|
else
|
|
|
|
|
m_UnusedHead = idx;
|
2026-03-23 20:50:11 +08:00
|
|
|
|
2026-04-22 13:04:31 +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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
private void RemoveFromUnusedList(int idx)
|
2025-10-11 15:18:09 +08:00
|
|
|
{
|
2026-04-22 13:04:31 +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
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
int prev = slot.PrevUnused;
|
|
|
|
|
int next = slot.NextUnused;
|
2025-10-11 15:18:09 +08:00
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
if (prev >= 0)
|
|
|
|
|
m_Slots[prev].NextUnused = next;
|
|
|
|
|
else
|
|
|
|
|
m_UnusedHead = next;
|
2025-10-11 15:18:09 +08:00
|
|
|
|
2026-04-22 13:04:31 +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
|
|
|
|
2026-04-22 13:04:31 +08:00
|
|
|
if (m_LastBudgetScanStart == idx)
|
|
|
|
|
m_LastBudgetScanStart = next >= 0 ? next : m_UnusedHead;
|
2025-10-11 15:18:09 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|