diff --git a/Runtime/ABase/Structs/TypeNamePair.cs b/Runtime/ABase/Structs/TypeNamePair.cs index 76aa787..4ec532e 100644 --- a/Runtime/ABase/Structs/TypeNamePair.cs +++ b/Runtime/ABase/Structs/TypeNamePair.cs @@ -28,11 +28,6 @@ namespace AlicizaX /// 名称。 public TypeNamePair(Type type, string name) { - if (type == null) - { - throw new GameFrameworkException("Type is invalid."); - } - m_Type = type; m_Name = name ?? string.Empty; } @@ -61,11 +56,21 @@ namespace AlicizaX { if (m_Type == null) { - throw new GameFrameworkException("Type is invalid."); + return string.Empty; } string typeName = m_Type.FullName; - return (string.IsNullOrEmpty(m_Name) ? typeName : Utility.Text.Format("{0}.{1}", typeName, m_Name)) ?? string.Empty; + if (string.IsNullOrEmpty(m_Name)) + return typeName ?? string.Empty; + + // 使用 ZString 避免字符串分配 + using (var sb = Cysharp.Text.ZString.CreateStringBuilder()) + { + sb.Append(typeName); + sb.Append('.'); + sb.Append(m_Name); + return sb.ToString(); + } } /// diff --git a/Runtime/ObjectPool/IObjectPoolService.cs b/Runtime/ObjectPool/IObjectPoolService.cs index 8862518..7c75286 100644 --- a/Runtime/ObjectPool/IObjectPoolService.cs +++ b/Runtime/ObjectPool/IObjectPoolService.cs @@ -9,6 +9,7 @@ namespace AlicizaX.ObjectPool public readonly int? Capacity; public readonly float? ExpireTime; public readonly int Priority; + public readonly ReleaseStrategy ReleaseStrategy; public ObjectPoolCreateOptions( string name = "", @@ -16,7 +17,8 @@ namespace AlicizaX.ObjectPool float? autoReleaseInterval = null, int? capacity = null, float? expireTime = null, - int priority = 0) + int priority = 0, + ReleaseStrategy releaseStrategy = ReleaseStrategy.LRU) { Name = name ?? string.Empty; AllowMultiSpawn = allowMultiSpawn; @@ -24,10 +26,11 @@ namespace AlicizaX.ObjectPool Capacity = capacity; ExpireTime = expireTime; Priority = priority; + ReleaseStrategy = releaseStrategy; } public ObjectPoolCreateOptions WithName(string name) - => new ObjectPoolCreateOptions(name, AllowMultiSpawn, AutoReleaseInterval, Capacity, ExpireTime, Priority); + => new ObjectPoolCreateOptions(name, AllowMultiSpawn, AutoReleaseInterval, Capacity, ExpireTime, Priority, ReleaseStrategy); public static ObjectPoolCreateOptions Single(string name = "") => new ObjectPoolCreateOptions(name: name); diff --git a/Runtime/ObjectPool/IPoolableObject.cs b/Runtime/ObjectPool/IPoolableObject.cs new file mode 100644 index 0000000..1aad388 --- /dev/null +++ b/Runtime/ObjectPool/IPoolableObject.cs @@ -0,0 +1,18 @@ +namespace AlicizaX.ObjectPool +{ + /// + /// 可池化对象接口,支持自定义回收和重用逻辑 + /// + public interface IPoolableObject + { + /// + /// 对象被回收到池中时调用(重置状态) + /// + void OnRecycle(); + + /// + /// 对象从池中取出时调用(初始化状态) + /// + void OnReuse(); + } +} diff --git a/Runtime/ObjectPool/IPoolableObject.cs.meta b/Runtime/ObjectPool/IPoolableObject.cs.meta new file mode 100644 index 0000000..876e9d4 --- /dev/null +++ b/Runtime/ObjectPool/IPoolableObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c4c7354bf5820e408151d8ecbd1bab1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ObjectPool/ObjectBase.cs b/Runtime/ObjectPool/ObjectBase.cs index 9ae0c1d..dd6704d 100644 --- a/Runtime/ObjectPool/ObjectBase.cs +++ b/Runtime/ObjectPool/ObjectBase.cs @@ -54,4 +54,71 @@ namespace AlicizaX.ObjectPool m_LastUseTime = 0f; } } + + /// + /// 泛型对象池基类,消除装箱开销 + /// + public abstract class ObjectBase : IMemory where T : class + { + private string m_Name; + private T m_Target; + private bool m_Locked; + private float m_LastUseTime; + + public string Name => m_Name; + public T Target => m_Target; + + public bool Locked + { + get => m_Locked; + set => m_Locked = value; + } + + public float LastUseTime + { + get => m_LastUseTime; + internal set => m_LastUseTime = value; + } + + public virtual bool CustomCanReleaseFlag => true; + + protected void Initialize(T target) + { + Initialize(string.Empty, target, false); + } + + protected void Initialize(string name, T target) + { + Initialize(name, target, false); + } + + protected void Initialize(string name, T target, bool locked) + { + m_Name = name ?? string.Empty; + m_Target = target; + m_Locked = locked; + m_LastUseTime = 0f; + + if (target is IPoolableObject poolable) + poolable.OnReuse(); + } + + protected internal virtual void OnSpawn() { } + + protected internal virtual void OnUnspawn() + { + if (m_Target is IPoolableObject poolable) + poolable.OnRecycle(); + } + + protected internal abstract void Release(bool isShutdown); + + public virtual void Clear() + { + m_Name = null; + m_Target = null; + m_Locked = false; + m_LastUseTime = 0f; + } + } } diff --git a/Runtime/ObjectPool/ObjectPoolBase.cs b/Runtime/ObjectPool/ObjectPoolBase.cs index 5e8cee1..c5352bc 100644 --- a/Runtime/ObjectPool/ObjectPoolBase.cs +++ b/Runtime/ObjectPool/ObjectPoolBase.cs @@ -21,7 +21,18 @@ namespace AlicizaX.ObjectPool get { if (m_FullName == null) - m_FullName = new TypeNamePair(ObjectType, m_Name).ToString(); + { + using (var sb = Cysharp.Text.ZString.CreateStringBuilder()) + { + sb.Append(ObjectType.FullName); + if (!string.IsNullOrEmpty(m_Name)) + { + sb.Append('.'); + sb.Append(m_Name); + } + m_FullName = sb.ToString(); + } + } return m_FullName; } } diff --git a/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs b/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs index f03b233..e6c00f1 100644 --- a/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs +++ b/Runtime/ObjectPool/ObjectPoolService.ObjectPool.cs @@ -36,19 +36,21 @@ namespace AlicizaX.ObjectPool private ObjectSlot[] m_Slots; private int m_SlotCount; + private int m_SlotCapacity; 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 IntOpenHashMap m_TargetMap; + private StringOpenHashMap m_AllNameHeadMap; + private StringOpenHashMap m_AvailableNameHeadMap; + private StringOpenHashMap m_AvailableNameTailMap; private readonly bool m_AllowMultiSpawn; private float m_AutoReleaseInterval; private int m_Capacity; private float m_ExpireTime; private int m_Priority; + private ReleaseStrategy m_ReleaseStrategy; private float m_AutoReleaseTime; private int m_PendingReleaseCount; @@ -57,26 +59,30 @@ namespace AlicizaX.ObjectPool private int m_UnusedTail; private int m_LastBudgetScanStart; private bool m_IsShuttingDown; + 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, - float autoReleaseInterval, int capacity, float expireTime, int priority) + float autoReleaseInterval, int capacity, float expireTime, int priority, ReleaseStrategy releaseStrategy) : 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_SlotCapacity = initCap; + m_Slots = SlotArrayPool.Rent(initCap); + m_FreeStack = SlotArrayPool.Rent(initCap); + m_TargetMap = new IntOpenHashMap(initCap); + m_AllNameHeadMap = new StringOpenHashMap(initCap); + m_AvailableNameHeadMap = new StringOpenHashMap(initCap); + m_AvailableNameTailMap = new StringOpenHashMap(initCap); m_AllowMultiSpawn = allowMultiSpawn; m_AutoReleaseInterval = autoReleaseInterval; m_Capacity = capacity; m_ExpireTime = expireTime; m_Priority = priority; + m_ReleaseStrategy = releaseStrategy; m_AutoReleaseTime = 0f; m_PendingReleaseCount = 0; m_ReleasePerFrameBudget = DefaultReleasePerFrame; @@ -84,6 +90,7 @@ namespace AlicizaX.ObjectPool m_UnusedTail = -1; m_LastBudgetScanStart = -1; m_IsShuttingDown = false; + m_ShrinkCounter = 0; } public override Type ObjectType => typeof(T); @@ -95,7 +102,13 @@ namespace AlicizaX.ObjectPool get => m_AutoReleaseInterval; set { - if (value < 0f) throw new GameFrameworkException("AutoReleaseInterval is invalid."); + if (value < 0f) + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError("AutoReleaseInterval is invalid."); +#endif + return; + } m_AutoReleaseInterval = value; } } @@ -105,7 +118,13 @@ namespace AlicizaX.ObjectPool get => m_Capacity; set { - if (value < 0) throw new GameFrameworkException("Capacity is invalid."); + if (value < 0) + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError("Capacity is invalid."); +#endif + return; + } m_Capacity = value; if (Count > m_Capacity) MarkRelease(Count - m_Capacity); } @@ -116,7 +135,13 @@ namespace AlicizaX.ObjectPool get => m_ExpireTime; set { - if (value < 0f) throw new GameFrameworkException("ExpireTime is invalid."); + if (value < 0f) + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError("ExpireTime is invalid."); +#endif + return; + } m_ExpireTime = value; } } @@ -135,11 +160,17 @@ namespace AlicizaX.ObjectPool 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}'."); + if (obj == null) return; + if (obj.Target == null) return; + + int targetHash = obj.Target.GetHashCode(); + if (m_TargetMap.TryGetValue(targetHash, out int existingIdx) && m_Slots[existingIdx].IsAlive()) + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError($"Target '{obj.Target.GetType().FullName}' is already registered in pool '{FullName}'."); +#endif + return; + } int idx = AllocSlot(); ref var slot = ref m_Slots[idx]; @@ -154,7 +185,7 @@ namespace AlicizaX.ObjectPool slot.NextUnused = -1; slot.SetAlive(true); - m_TargetMap[obj.Target] = idx; + m_TargetMap.AddOrUpdate(targetHash, idx); string objectName = obj.Name ?? string.Empty; if (m_AllNameHeadMap.TryGetValue(objectName, out int existingHead)) @@ -162,7 +193,7 @@ namespace AlicizaX.ObjectPool m_Slots[existingHead].PrevByName = idx; slot.NextByName = existingHead; } - m_AllNameHeadMap[objectName] = idx; + m_AllNameHeadMap.AddOrUpdate(objectName, idx); obj.LastUseTime = slot.LastUseTime; if (spawned) @@ -179,14 +210,19 @@ namespace AlicizaX.ObjectPool public T Spawn(string name) { - if (name == null) throw new GameFrameworkException("Name is invalid."); + if (name == null) return null; 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."); + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name head is inconsistent."); +#endif + return null; + } float now = Time.realtimeSinceStartup; SpawnSlot(head, now); @@ -199,7 +235,7 @@ namespace AlicizaX.ObjectPool public bool CanSpawn(string name) { - if (name == null) throw new GameFrameworkException("Name is invalid."); + if (name == null) return false; if (m_AllowMultiSpawn) return m_AllNameHeadMap.ContainsKey(name); @@ -208,18 +244,21 @@ namespace AlicizaX.ObjectPool public void Unspawn(T obj) { - if (obj == null) throw new GameFrameworkException("Object is invalid."); + if (obj == null) return; 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 (target == null) return; + int targetHash = target.GetHashCode(); + if (!m_TargetMap.TryGetValue(targetHash, out int idx)) { if (m_IsShuttingDown) return; - throw new GameFrameworkException( - $"Cannot find target in pool '{Name}', type='{target.GetType().FullName}'"); +#if UNITY_EDITOR + UnityEngine.Debug.LogError($"Cannot find target in pool '{Name}', type='{target.GetType().FullName}'"); +#endif + return; } UnspawnSlot(idx); @@ -270,7 +309,11 @@ namespace AlicizaX.ObjectPool } bool checkExpire = m_ExpireTime < float.MaxValue; - if (m_PendingReleaseCount <= 0 && !checkExpire) return; + if (m_PendingReleaseCount <= 0 && !checkExpire) + { + TryProgressiveShrink(); + return; + } float now = Time.realtimeSinceStartup; float expireThreshold = checkExpire ? now - m_ExpireTime : float.MinValue; @@ -284,6 +327,41 @@ namespace AlicizaX.ObjectPool { ReleaseExpired(m_ReleasePerFrameBudget, expireThreshold); } + + TryProgressiveShrink(); + } + + private void TryProgressiveShrink() + { + m_ShrinkCounter++; + if (m_ShrinkCounter < ShrinkCheckInterval) + return; + + m_ShrinkCounter = 0; + + if (m_TargetMap.Count == 0 || m_SlotCapacity <= InitSlotCapacity) + return; + + float usageRatio = (float)m_TargetMap.Count / m_SlotCapacity; + if (usageRatio < 0.25f) + { + int targetCapacity = Math.Max(m_SlotCapacity / 2, InitSlotCapacity); + if (targetCapacity < m_SlotCapacity) + { + var newSlots = SlotArrayPool.Rent(targetCapacity); + var newFreeStack = SlotArrayPool.Rent(targetCapacity); + + Array.Copy(m_Slots, 0, newSlots, 0, Math.Min(m_SlotCount, targetCapacity)); + Array.Copy(m_FreeStack, 0, newFreeStack, 0, Math.Min(m_FreeTop, targetCapacity)); + + SlotArrayPool.Return(m_Slots, true); + SlotArrayPool.Return(m_FreeStack, true); + + m_Slots = newSlots; + m_FreeStack = newFreeStack; + m_SlotCapacity = targetCapacity; + } + } } internal override void Shutdown() @@ -303,7 +381,14 @@ namespace AlicizaX.ObjectPool m_AllNameHeadMap.Clear(); m_AvailableNameHeadMap.Clear(); m_AvailableNameTailMap.Clear(); + + SlotArrayPool.Return(m_Slots, true); + SlotArrayPool.Return(m_FreeStack, true); + m_Slots = null; + m_FreeStack = null; + m_SlotCount = 0; + m_SlotCapacity = 0; m_FreeTop = 0; m_PendingReleaseCount = 0; m_UnusedHead = -1; @@ -315,7 +400,13 @@ namespace AlicizaX.ObjectPool public override int GetAllObjectInfos(ObjectInfo[] results) { - if (results == null) throw new GameFrameworkException("Results is invalid."); + if (results == null) + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError("Results is invalid."); +#endif + return 0; + } int write = 0; int capacity = results.Length; @@ -365,7 +456,12 @@ namespace AlicizaX.ObjectPool slot.Obj.OnUnspawn(); slot.SpawnCount--; if (slot.SpawnCount < 0) - throw new GameFrameworkException($"Object '{slot.Obj.Name}' spawn count < 0."); + { +#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) @@ -393,9 +489,19 @@ namespace AlicizaX.ObjectPool private void GrowSlots() { - int newCap = Math.Max(m_Slots.Length * 2, InitSlotCapacity); - Array.Resize(ref m_Slots, newCap); - Array.Resize(ref m_FreeStack, newCap); + int newCap = Math.Max(m_SlotCapacity * 2, InitSlotCapacity); + var newSlots = SlotArrayPool.Rent(newCap); + var newFreeStack = SlotArrayPool.Rent(newCap); + + Array.Copy(m_Slots, 0, newSlots, 0, m_SlotCount); + Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop); + + SlotArrayPool.Return(m_Slots, true); + SlotArrayPool.Return(m_FreeStack, true); + + m_Slots = newSlots; + m_FreeStack = newFreeStack; + m_SlotCapacity = newCap; } private void ReleaseSlot(int idx) @@ -408,7 +514,8 @@ namespace AlicizaX.ObjectPool MarkSlotUnavailable(idx); RemoveFromAllNameChain(idx); - m_TargetMap.Remove(obj.Target); + int targetHash = obj.Target.GetHashCode(); + m_TargetMap.Remove(targetHash); obj.Release(false); MemoryPool.Release(obj); @@ -423,8 +530,15 @@ namespace AlicizaX.ObjectPool slot.PrevUnused = -1; slot.NextUnused = -1; - if (m_FreeTop >= m_FreeStack.Length) - Array.Resize(ref m_FreeStack, m_FreeStack.Length * 2); + if (m_FreeTop >= m_SlotCapacity) + { + int newCap = m_SlotCapacity * 2; + var newFreeStack = SlotArrayPool.Rent(newCap); + Array.Copy(m_FreeStack, 0, newFreeStack, 0, m_FreeTop); + SlotArrayPool.Return(m_FreeStack, true); + m_FreeStack = newFreeStack; + m_SlotCapacity = newCap; + } m_FreeStack[m_FreeTop++] = idx; ShrinkStorageIfEmpty(); @@ -446,10 +560,15 @@ namespace AlicizaX.ObjectPool else { if (head != idx) - throw new GameFrameworkException($"Object pool '{FullName}' all-name chain is inconsistent."); + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain is inconsistent."); +#endif + return; + } if (next >= 0) - m_AllNameHeadMap[objectName] = next; + m_AllNameHeadMap.AddOrUpdate(objectName, next); else m_AllNameHeadMap.Remove(objectName); } @@ -524,11 +643,15 @@ namespace AlicizaX.ObjectPool private void ShrinkStorageIfEmpty() { - if (m_TargetMap.Count > 0 || m_Slots.Length <= InitSlotCapacity) + if (m_TargetMap.Count > 0 || m_SlotCapacity <= InitSlotCapacity) return; - m_Slots = new ObjectSlot[InitSlotCapacity]; - m_FreeStack = new int[InitSlotCapacity]; + SlotArrayPool.Return(m_Slots, true); + SlotArrayPool.Return(m_FreeStack, true); + + m_SlotCapacity = InitSlotCapacity; + m_Slots = SlotArrayPool.Rent(InitSlotCapacity); + m_FreeStack = SlotArrayPool.Rent(InitSlotCapacity); m_AllNameHeadMap.Clear(); m_AvailableNameHeadMap.Clear(); m_AvailableNameTailMap.Clear(); @@ -542,6 +665,9 @@ namespace AlicizaX.ObjectPool [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")] private void ValidateState() { +#if !ENABLE_OBJECTPOOL_VALIDATION + return; +#else int aliveCount = 0; int unusedCount = 0; for (int i = 0; i < m_SlotCount; i++) @@ -553,18 +679,29 @@ namespace AlicizaX.ObjectPool 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."); + int targetHash = target.GetHashCode(); + if (!m_TargetMap.TryGetValue(targetHash, out int mappedIdx) || mappedIdx != i) + { + 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)) - throw new GameFrameworkException($"Object pool '{FullName}' all-name head is missing."); + { + UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name head is missing."); + continue; + } if (slot.PrevByName < 0 && head != i) - throw new GameFrameworkException($"Object pool '{FullName}' all-name chain head is inconsistent."); + { + UnityEngine.Debug.LogError($"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."); + { + UnityEngine.Debug.LogError($"Object pool '{FullName}' all-name chain link is inconsistent."); + } bool inUnusedList = m_UnusedHead == i || slot.PrevUnused >= 0 || slot.NextUnused >= 0; bool inAvailableList = false; @@ -573,30 +710,46 @@ namespace AlicizaX.ObjectPool { unusedCount++; if (!inUnusedList) - throw new GameFrameworkException($"Object pool '{FullName}' unused list is inconsistent."); + { + UnityEngine.Debug.LogError($"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."); + { + UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name head is missing."); + } + else + { + inAvailableList = availableHead == i || slot.PrevAvailableByName >= 0 || slot.NextAvailableByName >= 0; + if (!inAvailableList) + { + UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name chain is inconsistent."); + } - 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."); + if (slot.NextAvailableByName >= 0 && m_Slots[slot.NextAvailableByName].PrevAvailableByName != i) + { + UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name link is inconsistent."); + } + } } else { if (inUnusedList) - throw new GameFrameworkException($"Object pool '{FullName}' spawned object exists in unused list."); + { + UnityEngine.Debug.LogError($"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."); + { + UnityEngine.Debug.LogError($"Object pool '{FullName}' spawned object exists in available chain."); + } } } if (aliveCount != m_TargetMap.Count) - throw new GameFrameworkException($"Object pool '{FullName}' alive count is inconsistent."); + { + UnityEngine.Debug.LogError($"Object pool '{FullName}' alive count is inconsistent."); + } int walkUnusedCount = 0; int current = m_UnusedHead; @@ -605,9 +758,13 @@ namespace AlicizaX.ObjectPool { ref var slot = ref m_Slots[current]; if (!slot.IsAlive() || slot.SpawnCount != 0) - throw new GameFrameworkException($"Object pool '{FullName}' unused chain contains invalid slot."); + { + UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain contains invalid slot."); + } if (slot.PrevUnused != prevUnused) - throw new GameFrameworkException($"Object pool '{FullName}' unused chain linkage is inconsistent."); + { + UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain linkage is inconsistent."); + } walkUnusedCount++; prevUnused = current; @@ -615,7 +772,10 @@ namespace AlicizaX.ObjectPool } if (walkUnusedCount != unusedCount) - throw new GameFrameworkException($"Object pool '{FullName}' unused chain count is inconsistent."); + { + UnityEngine.Debug.LogError($"Object pool '{FullName}' unused chain count is inconsistent."); + } +#endif } private void MarkSlotAvailable(int idx) @@ -677,7 +837,12 @@ namespace AlicizaX.ObjectPool { 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."); + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError($"Object pool '{FullName}' available-name chain is inconsistent."); +#endif + return; + } string objectName = slot.Obj.Name ?? string.Empty; if (m_AvailableNameTailMap.TryGetValue(objectName, out int tail)) @@ -685,14 +850,14 @@ namespace AlicizaX.ObjectPool m_Slots[tail].NextAvailableByName = idx; slot.PrevAvailableByName = tail; slot.NextAvailableByName = -1; - m_AvailableNameTailMap[objectName] = idx; + m_AvailableNameTailMap.AddOrUpdate(objectName, idx); } else { slot.PrevAvailableByName = -1; slot.NextAvailableByName = -1; - m_AvailableNameHeadMap[objectName] = idx; - m_AvailableNameTailMap[objectName] = idx; + m_AvailableNameHeadMap.AddOrUpdate(objectName, idx); + m_AvailableNameTailMap.AddOrUpdate(objectName, idx); } } @@ -713,14 +878,14 @@ namespace AlicizaX.ObjectPool if (prev >= 0) m_Slots[prev].NextAvailableByName = next; else if (next >= 0) - m_AvailableNameHeadMap[objectName] = next; + m_AvailableNameHeadMap.AddOrUpdate(objectName, next); else m_AvailableNameHeadMap.Remove(objectName); if (next >= 0) m_Slots[next].PrevAvailableByName = prev; else if (prev >= 0) - m_AvailableNameTailMap[objectName] = prev; + m_AvailableNameTailMap.AddOrUpdate(objectName, prev); else m_AvailableNameTailMap.Remove(objectName); diff --git a/Runtime/ObjectPool/ObjectPoolService.cs b/Runtime/ObjectPool/ObjectPoolService.cs index ca796de..a911cde 100644 --- a/Runtime/ObjectPool/ObjectPoolService.cs +++ b/Runtime/ObjectPool/ObjectPoolService.cs @@ -92,7 +92,13 @@ namespace AlicizaX.ObjectPool int IObjectPoolServiceDebugView.GetAllObjectPools(bool sort, ObjectPoolBase[] results) { - if (results == null) throw new GameFrameworkException("Results is invalid."); + if (results == null) + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError("Results is invalid."); +#endif + return 0; + } List source = m_ObjectPoolList; if (sort) @@ -115,7 +121,12 @@ namespace AlicizaX.ObjectPool { var key = new TypeNamePair(typeof(T), options.Name); if (m_ObjectPools.ContainsKey(key)) - throw new GameFrameworkException($"Already exist object pool '{key}'."); + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError($"Already exist object pool '{key}'."); +#endif + return null; + } var pool = new ObjectPool( options.Name ?? string.Empty, @@ -123,7 +134,8 @@ namespace AlicizaX.ObjectPool options.AutoReleaseInterval ?? DefaultAutoReleaseInterval, options.Capacity ?? DefaultCapacity, options.ExpireTime ?? DefaultExpireTime, - options.Priority); + options.Priority, + options.ReleaseStrategy); m_ObjectPools.Add(key, pool); m_ObjectPoolIndexMap.Add(pool, m_ObjectPoolList.Count); @@ -136,7 +148,12 @@ namespace AlicizaX.ObjectPool ValidateObjectType(objectType); var key = new TypeNamePair(objectType, options.Name); if (m_ObjectPools.ContainsKey(key)) - throw new GameFrameworkException($"Already exist object pool '{key}'."); + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError($"Already exist object pool '{key}'."); +#endif + return null; + } var poolType = typeof(ObjectPool<>).MakeGenericType(objectType); var pool = (ObjectPoolBase)Activator.CreateInstance(poolType, @@ -145,7 +162,8 @@ namespace AlicizaX.ObjectPool options.AutoReleaseInterval ?? DefaultAutoReleaseInterval, options.Capacity ?? DefaultCapacity, options.ExpireTime ?? DefaultExpireTime, - options.Priority); + options.Priority, + options.ReleaseStrategy); m_ObjectPools.Add(key, pool); m_ObjectPoolIndexMap.Add(pool, m_ObjectPoolList.Count); @@ -175,13 +193,25 @@ namespace AlicizaX.ObjectPool public bool DestroyObjectPool(IObjectPool objectPool) where T : ObjectBase { - if (objectPool == null) throw new GameFrameworkException("Object pool is invalid."); + if (objectPool == null) + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError("Object pool is invalid."); +#endif + return false; + } return InternalDestroy(new TypeNamePair(typeof(T), objectPool.Name)); } public bool DestroyObjectPool(ObjectPoolBase objectPool) { - if (objectPool == null) throw new GameFrameworkException("Object pool is invalid."); + if (objectPool == null) + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError("Object pool is invalid."); +#endif + return false; + } return InternalDestroy(new TypeNamePair(objectPool.ObjectType, objectPool.Name)); } @@ -253,9 +283,18 @@ namespace AlicizaX.ObjectPool private static void ValidateObjectType(Type objectType) { if (objectType == null) - throw new GameFrameworkException("Object type is invalid."); + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError("Object type is invalid."); +#endif + return; + } if (!typeof(ObjectBase).IsAssignableFrom(objectType)) - throw new GameFrameworkException($"Object type '{objectType.FullName}' is invalid."); + { +#if UNITY_EDITOR + UnityEngine.Debug.LogError($"Object type '{objectType.FullName}' is invalid."); +#endif + } } private static int ObjectPoolComparer(ObjectPoolBase a, ObjectPoolBase b) diff --git a/Runtime/ObjectPool/ReleaseStrategy.cs b/Runtime/ObjectPool/ReleaseStrategy.cs new file mode 100644 index 0000000..68f629a --- /dev/null +++ b/Runtime/ObjectPool/ReleaseStrategy.cs @@ -0,0 +1,28 @@ +namespace AlicizaX.ObjectPool +{ + /// + /// 对象池释放策略 + /// + public enum ReleaseStrategy + { + /// + /// LRU (Least Recently Used) - 最近最少使用 + /// + LRU = 0, + + /// + /// LFU (Least Frequently Used) - 最不经常使用 + /// + LFU = 1, + + /// + /// Priority - 基于优先级 + /// + Priority = 2, + + /// + /// Hybrid - 混合策略 (LRU + Priority) + /// + Hybrid = 3 + } +} diff --git a/Runtime/ObjectPool/ReleaseStrategy.cs.meta b/Runtime/ObjectPool/ReleaseStrategy.cs.meta new file mode 100644 index 0000000..5ecc7ad --- /dev/null +++ b/Runtime/ObjectPool/ReleaseStrategy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 311d5e5b578ed15428d9565ab237becc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ObjectPool/SlotArrayPool.cs b/Runtime/ObjectPool/SlotArrayPool.cs new file mode 100644 index 0000000..41a41ca --- /dev/null +++ b/Runtime/ObjectPool/SlotArrayPool.cs @@ -0,0 +1,24 @@ +using System; +using System.Buffers; + +namespace AlicizaX.ObjectPool +{ + /// + /// 数组池管理器,避免频繁分配数组 + /// + internal static class SlotArrayPool + { + private static readonly ArrayPool s_Pool = ArrayPool.Create(256, 50); + + public static T[] Rent(int minimumLength) + { + return s_Pool.Rent(minimumLength); + } + + public static void Return(T[] array, bool clearArray = false) + { + if (array != null) + s_Pool.Return(array, clearArray); + } + } +} diff --git a/Runtime/ObjectPool/SlotArrayPool.cs.meta b/Runtime/ObjectPool/SlotArrayPool.cs.meta new file mode 100644 index 0000000..2785757 --- /dev/null +++ b/Runtime/ObjectPool/SlotArrayPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ec27a420ef8eea14fade722558971daa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ObjectPool/StringOpenHashMap.cs b/Runtime/ObjectPool/StringOpenHashMap.cs new file mode 100644 index 0000000..d7fa7ed --- /dev/null +++ b/Runtime/ObjectPool/StringOpenHashMap.cs @@ -0,0 +1,183 @@ +using System; +using System.Runtime.CompilerServices; + +namespace AlicizaX.ObjectPool +{ + /// + /// 字符串键的开放寻址哈希表,零GC实现 + /// + internal struct StringOpenHashMap + { + private int[] m_Buckets; + private string[] m_Keys; + private int[] m_Values; + private int[] m_Next; + private int m_Count; + private int m_FreeList; + private int m_Mask; + private int m_AllocCount; + + private const int MinCapacity = 8; + + public int Count => m_Count; + + public StringOpenHashMap(int capacity) + { + int cap = NextPowerOf2(Math.Max(capacity, MinCapacity)); + m_Mask = cap - 1; + m_Buckets = new int[cap]; + m_Keys = new string[cap]; + m_Values = new int[cap]; + m_Next = new int[cap]; + m_Count = 0; + m_FreeList = 0; + m_AllocCount = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetValue(string key, out int value) + { + if (m_Buckets == null || key == null) { value = -1; return false; } + int hash = key.GetHashCode() & 0x7FFFFFFF; + int i = m_Buckets[hash & m_Mask]; + while (i > 0) + { + int idx = i - 1; + if (m_Keys[idx] == key) { value = m_Values[idx]; return true; } + i = m_Next[idx]; + } + value = -1; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AddOrUpdate(string key, int value) + { + if (key == null) return; + if (m_Count >= ((m_Mask + 1) * 3 >> 2)) + Grow(); + + int hash = key.GetHashCode() & 0x7FFFFFFF; + int bucket = hash & m_Mask; + int i = m_Buckets[bucket]; + while (i > 0) + { + int ei = i - 1; + if (m_Keys[ei] == key) { m_Values[ei] = value; return; } + i = m_Next[ei]; + } + + int idx; + if (m_FreeList > 0) + { + idx = m_FreeList - 1; + m_FreeList = m_Next[idx]; + } + else + { + if (m_AllocCount > m_Mask) { Grow(); bucket = hash & m_Mask; } + idx = m_AllocCount++; + } + + m_Keys[idx] = key; + m_Values[idx] = value; + m_Next[idx] = m_Buckets[bucket]; + m_Buckets[bucket] = idx + 1; + m_Count++; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Remove(string key) + { + if (m_Buckets == null || key == null) return false; + int hash = key.GetHashCode() & 0x7FFFFFFF; + int bucket = hash & m_Mask; + int prev = 0; + int i = m_Buckets[bucket]; + while (i > 0) + { + int idx = i - 1; + if (m_Keys[idx] == key) + { + if (prev == 0) m_Buckets[bucket] = m_Next[idx]; + else m_Next[prev - 1] = m_Next[idx]; + m_Keys[idx] = null; + m_Values[idx] = -1; + m_Next[idx] = m_FreeList; + m_FreeList = idx + 1; + m_Count--; + return true; + } + prev = i; + i = m_Next[idx]; + } + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool ContainsKey(string key) + { + return TryGetValue(key, out _); + } + + public void Clear() + { + if (m_Buckets == null) return; + int cap = m_Mask + 1; + Array.Clear(m_Buckets, 0, cap); + Array.Clear(m_Keys, 0, cap); + Array.Clear(m_Values, 0, cap); + Array.Clear(m_Next, 0, cap); + m_Count = 0; + m_FreeList = 0; + m_AllocCount = 0; + } + + private void Grow() + { + int newCap = (m_Mask + 1) << 1; + if (newCap < MinCapacity) newCap = MinCapacity; + int newMask = newCap - 1; + var newBuckets = new int[newCap]; + var newKeys = new string[newCap]; + var newValues = new int[newCap]; + var newNext = new int[newCap]; + + int newAlloc = 0; + int oldCap = m_Mask + 1; + for (int b = 0; b < oldCap; b++) + { + int i = m_Buckets[b]; + while (i > 0) + { + int old = i - 1; + int ni = newAlloc++; + newKeys[ni] = m_Keys[old]; + newValues[ni] = m_Values[old]; + int hash = newKeys[ni].GetHashCode() & 0x7FFFFFFF; + int nb = hash & newMask; + newNext[ni] = newBuckets[nb]; + newBuckets[nb] = ni + 1; + i = m_Next[old]; + } + } + + m_Buckets = newBuckets; + m_Keys = newKeys; + m_Values = newValues; + m_Next = newNext; + m_Mask = newMask; + m_AllocCount = newAlloc; + m_FreeList = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int NextPowerOf2(int v) + { + v--; + v |= v >> 1; v |= v >> 2; v |= v >> 4; + v |= v >> 8; v |= v >> 16; + return v + 1; + } + } +} diff --git a/Runtime/ObjectPool/StringOpenHashMap.cs.meta b/Runtime/ObjectPool/StringOpenHashMap.cs.meta new file mode 100644 index 0000000..4d96ef6 --- /dev/null +++ b/Runtime/ObjectPool/StringOpenHashMap.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ac1ce9399b68c57439691aae9aec01ca +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: