com.alicizax.unity.framework/Runtime/ABase/GameObjectPool/RuntimePoolModels.cs

1156 lines
37 KiB
C#
Raw Normal View History

2026-03-26 10:49:41 +08:00
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace AlicizaX
{
2026-04-17 21:01:20 +08:00
public readonly struct PoolSpawnContext
{
public readonly string AssetPath;
public readonly string Group;
public readonly Transform Parent;
public readonly Vector3 Position;
public readonly Quaternion Rotation;
public readonly object UserData;
public readonly int OwnerId;
public readonly int TeamId;
public readonly uint SpawnFrame;
public PoolSpawnContext(
string assetPath,
string group,
Transform parent,
Vector3 position,
Quaternion rotation,
object userData = null,
int ownerId = 0,
int teamId = 0,
uint spawnFrame = 0)
{
AssetPath = assetPath;
Group = group;
Parent = parent;
Position = position;
Rotation = rotation;
UserData = userData;
OwnerId = ownerId;
TeamId = teamId;
SpawnFrame = spawnFrame;
}
public PoolSpawnContext WithParent(Transform parent)
{
return new PoolSpawnContext(AssetPath, Group, parent, Position, Rotation, UserData, OwnerId, TeamId, SpawnFrame);
}
public static PoolSpawnContext Create(string assetPath, string group, Transform parent, object userData = null)
{
return new PoolSpawnContext(
assetPath,
group,
parent,
Vector3.zero,
Quaternion.identity,
userData,
spawnFrame: (uint)Time.frameCount);
}
}
public interface IGameObjectPoolable
{
void OnPoolCreate();
void OnPoolGet(in PoolSpawnContext context);
void OnPoolRelease();
void OnPoolDestroy();
}
public interface IPoolAutoRecycle
{
bool TryGetAutoRecycleDelay(out float delaySeconds);
}
public interface IPoolResettablePhysics
{
void ResetPhysicsState();
}
public interface IPoolResettableVisual
{
void ResetVisualState();
}
public interface IPoolResettableAnimation
{
void ResetAnimationState();
}
public interface IPoolSleepable
{
void EnterSleep();
void ExitSleep(in PoolSpawnContext context);
}
public enum RuntimePooledObjectState
{
Uninitialized = 0,
Inactive = 1,
Active = 2,
Releasing = 3,
Destroying = 4
}
2026-03-26 10:49:41 +08:00
[Serializable]
public sealed class GameObjectPoolInstanceSnapshot : IMemory
{
public string instanceName;
public bool isActive;
public float idleDuration;
2026-04-17 21:01:20 +08:00
public float lifeDuration;
2026-03-26 10:49:41 +08:00
public GameObject gameObject;
public void Clear()
{
instanceName = null;
isActive = false;
idleDuration = 0f;
2026-04-17 21:01:20 +08:00
lifeDuration = 0f;
2026-03-26 10:49:41 +08:00
gameObject = null;
}
}
[Serializable]
public sealed class GameObjectPoolSnapshot : IMemory
{
2026-04-17 21:01:20 +08:00
public string entryName;
2026-03-26 10:49:41 +08:00
public string group;
public string assetPath;
public PoolResourceLoaderType loaderType;
2026-04-17 21:01:20 +08:00
public PoolOverflowPolicy overflowPolicy;
public int minRetained;
public int softCapacity;
public int hardCapacity;
2026-03-26 10:49:41 +08:00
public int totalCount;
public int activeCount;
public int inactiveCount;
public bool prefabLoaded;
public float prefabIdleDuration;
2026-04-17 21:01:20 +08:00
public int acquireCount;
public int releaseCount;
public int hitCount;
public int missCount;
public int expandCount;
public int exhaustedCount;
public int autoRecycleCount;
public int destroyCount;
public int peakActive;
public int peakTotal;
2026-03-26 10:49:41 +08:00
public List<GameObjectPoolInstanceSnapshot> instances = new List<GameObjectPoolInstanceSnapshot>();
public void Clear()
{
2026-04-17 21:01:20 +08:00
entryName = null;
2026-03-26 10:49:41 +08:00
group = null;
assetPath = null;
loaderType = default;
2026-04-17 21:01:20 +08:00
overflowPolicy = default;
minRetained = 0;
softCapacity = 0;
hardCapacity = 0;
2026-03-26 10:49:41 +08:00
totalCount = 0;
activeCount = 0;
inactiveCount = 0;
prefabLoaded = false;
prefabIdleDuration = 0f;
2026-04-17 21:01:20 +08:00
acquireCount = 0;
releaseCount = 0;
hitCount = 0;
missCount = 0;
expandCount = 0;
exhaustedCount = 0;
autoRecycleCount = 0;
destroyCount = 0;
peakActive = 0;
peakTotal = 0;
2026-03-26 10:49:41 +08:00
for (int i = 0; i < instances.Count; i++)
{
MemoryPool.Release(instances[i]);
}
instances.Clear();
}
}
[DisallowMultipleComponent]
public sealed class PooledGameObjectMarker : MonoBehaviour
{
private RuntimePrefabPool _owner;
private RuntimePooledInstance _instance;
internal void Bind(RuntimePrefabPool owner, RuntimePooledInstance instance)
{
_owner = owner;
_instance = instance;
}
public void Detach()
{
_owner = null;
_instance = null;
}
private void OnDestroy()
{
if (_owner != null && _instance != null)
{
_owner.NotifyInstanceDestroyed(_instance);
}
}
}
2026-04-17 21:01:20 +08:00
[DisallowMultipleComponent]
public sealed class PooledAutoRecycle : MonoBehaviour
{
private GameObjectPoolManager _manager;
private CancellationTokenSource _cts;
public void Schedule(GameObjectPoolManager manager, float delaySeconds)
{
Cancel();
if (manager == null || delaySeconds <= 0f)
{
return;
}
_manager = manager;
_cts = new CancellationTokenSource();
AutoReleaseAsync(delaySeconds, _cts.Token).Forget();
}
public void Cancel()
{
if (_cts == null)
{
return;
}
_cts.Cancel();
_cts.Dispose();
_cts = null;
}
private async UniTaskVoid AutoReleaseAsync(float delaySeconds, CancellationToken cancellationToken)
{
try
{
await UniTask.Delay(TimeSpan.FromSeconds(delaySeconds), cancellationToken: cancellationToken);
if (_manager != null && this != null && gameObject != null)
{
_manager.Release(gameObject);
}
}
catch (OperationCanceledException)
{
}
}
private void OnDestroy()
{
Cancel();
_manager = null;
}
}
internal sealed class RuntimePoolLifecycleCache : IMemory
{
public IGameObjectPoolable[] poolables;
public IPoolSleepable[] sleepables;
public IPoolResettablePhysics[] physicsResetters;
public IPoolResettableVisual[] visualResetters;
public IPoolResettableAnimation[] animationResetters;
public IPoolAutoRecycle[] autoRecyclers;
public void Clear()
{
poolables = null;
sleepables = null;
physicsResetters = null;
visualResetters = null;
animationResetters = null;
autoRecyclers = null;
}
}
2026-03-26 10:49:41 +08:00
internal sealed class RuntimePooledInstance : IMemory
{
public GameObject GameObject { get; private set; }
public PooledGameObjectMarker Marker { get; private set; }
2026-04-17 21:01:20 +08:00
public PooledAutoRecycle AutoRecycle { get; private set; }
public RuntimePoolLifecycleCache Lifecycle { get; private set; }
2026-03-26 10:49:41 +08:00
public LinkedListNode<RuntimePooledInstance> InactiveNode { get; set; }
2026-04-17 21:01:20 +08:00
public RuntimePooledObjectState State { get; set; }
2026-03-26 10:49:41 +08:00
public float LastReleaseTime { get; set; }
2026-04-17 21:01:20 +08:00
public float SpawnTime { get; set; }
2026-03-26 10:49:41 +08:00
2026-04-17 21:01:20 +08:00
public void Initialize(
GameObject gameObject,
PooledGameObjectMarker marker,
PooledAutoRecycle autoRecycle,
RuntimePoolLifecycleCache lifecycle)
2026-03-26 10:49:41 +08:00
{
GameObject = gameObject;
Marker = marker;
2026-04-17 21:01:20 +08:00
AutoRecycle = autoRecycle;
Lifecycle = lifecycle;
State = RuntimePooledObjectState.Inactive;
SpawnTime = Time.time;
LastReleaseTime = Time.time;
2026-03-26 10:49:41 +08:00
}
public void Clear()
{
GameObject = null;
Marker = null;
2026-04-17 21:01:20 +08:00
AutoRecycle = null;
Lifecycle = null;
2026-03-26 10:49:41 +08:00
InactiveNode = null;
2026-04-17 21:01:20 +08:00
State = RuntimePooledObjectState.Uninitialized;
2026-03-26 10:49:41 +08:00
LastReleaseTime = 0f;
2026-04-17 21:01:20 +08:00
SpawnTime = 0f;
2026-03-26 10:49:41 +08:00
}
}
internal sealed class RuntimePrefabPool : IMemory
{
2026-03-26 13:30:57 +08:00
private static readonly Comparison<GameObjectPoolInstanceSnapshot> InstanceSnapshotComparer = (left, right) =>
{
if (left == null && right == null) return 0;
if (left == null) return 1;
if (right == null) return -1;
if (left.isActive != right.isActive) return left.isActive ? -1 : 1;
return string.Compare(left.instanceName, right.instanceName, StringComparison.Ordinal);
};
2026-04-17 21:01:20 +08:00
private ResolvedPoolConfig _config;
2026-03-26 10:49:41 +08:00
private string _assetPath;
private IResourceLoader _loader;
private GameObjectPoolManager _service;
2026-03-26 10:49:41 +08:00
private CancellationToken _shutdownToken;
private Dictionary<GameObject, RuntimePooledInstance> _instancesByObject;
private LinkedList<RuntimePooledInstance> _inactiveInstances;
2026-04-17 21:01:20 +08:00
private List<RuntimePooledInstance> _destroyBuffer;
2026-03-26 10:49:41 +08:00
private Transform _root;
private GameObject _prefab;
private UniTaskCompletionSource<GameObject> _prefabLoadSource;
private int _activeCount;
private float _lastPrefabTouchTime;
2026-04-17 21:01:20 +08:00
private int _runtimeHardCapacity;
private int _acquireCount;
private int _releaseCount;
private int _hitCount;
private int _missCount;
private int _expandCount;
private int _exhaustedCount;
private int _autoRecycleCount;
private int _destroyCount;
private int _peakActive;
private int _peakTotal;
2026-03-26 10:49:41 +08:00
public RuntimePrefabPool()
{
_instancesByObject = new Dictionary<GameObject, RuntimePooledInstance>();
_inactiveInstances = new LinkedList<RuntimePooledInstance>();
2026-04-17 21:01:20 +08:00
_destroyBuffer = new List<RuntimePooledInstance>();
2026-03-26 10:49:41 +08:00
}
public void Initialize(
2026-04-17 21:01:20 +08:00
ResolvedPoolConfig config,
2026-03-26 10:49:41 +08:00
string assetPath,
IResourceLoader loader,
GameObjectPoolManager service,
2026-03-26 10:49:41 +08:00
CancellationToken shutdownToken)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_assetPath = assetPath ?? throw new ArgumentNullException(nameof(assetPath));
_loader = loader ?? throw new ArgumentNullException(nameof(loader));
_service = service ?? throw new ArgumentNullException(nameof(service));
_shutdownToken = shutdownToken;
_lastPrefabTouchTime = Time.time;
2026-04-17 21:01:20 +08:00
_runtimeHardCapacity = config.hardCapacity;
2026-03-26 10:49:41 +08:00
_instancesByObject.Clear();
_inactiveInstances.Clear();
2026-04-17 21:01:20 +08:00
_destroyBuffer.Clear();
2026-03-26 10:49:41 +08:00
_prefab = null;
_prefabLoadSource = null;
_activeCount = 0;
2026-04-17 21:01:20 +08:00
_acquireCount = 0;
_releaseCount = 0;
_hitCount = 0;
_missCount = 0;
_expandCount = 0;
_exhaustedCount = 0;
_autoRecycleCount = 0;
_destroyCount = 0;
_peakActive = 0;
_peakTotal = 0;
2026-03-26 10:49:41 +08:00
GameObject rootObject = new GameObject($"Pool_{SanitizeName(config.group)}_{SanitizeName(assetPath)}");
_root = rootObject.transform;
_root.SetParent(service.poolContainer, false);
rootObject.SetActive(false);
}
public string AssetPath => _assetPath;
2026-04-17 21:01:20 +08:00
public ResolvedPoolConfig Config => _config;
2026-03-26 10:49:41 +08:00
public int TotalCount => _instancesByObject.Count;
public int ActiveCount => _activeCount;
public int InactiveCount => _inactiveInstances.Count;
public bool IsPrefabLoaded => _prefab != null;
public float PrefabIdleDuration => _prefab == null ? 0f : Mathf.Max(0f, Time.time - _lastPrefabTouchTime);
2026-04-17 21:01:20 +08:00
public GameObject Acquire(in PoolSpawnContext context)
2026-03-26 10:49:41 +08:00
{
EnsurePrefabLoaded();
2026-04-17 21:01:20 +08:00
return AcquirePrepared(context);
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
public async UniTask<GameObject> AcquireAsync(PoolSpawnContext context, CancellationToken cancellationToken)
2026-03-26 10:49:41 +08:00
{
await EnsurePrefabLoadedAsync(cancellationToken);
2026-04-17 21:01:20 +08:00
return AcquirePrepared(context);
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
public async UniTask WarmupAsync(int targetCount, CancellationToken cancellationToken)
2026-03-26 10:49:41 +08:00
{
2026-04-17 21:01:20 +08:00
if (targetCount <= 0)
2026-03-26 10:49:41 +08:00
{
return;
}
2026-04-17 21:01:20 +08:00
await EnsurePrefabLoadedAsync(cancellationToken);
2026-03-26 10:49:41 +08:00
2026-04-17 21:01:20 +08:00
int desiredInactiveCount = Mathf.Clamp(targetCount, 0, _runtimeHardCapacity);
if (desiredInactiveCount <= _inactiveInstances.Count)
2026-03-26 10:49:41 +08:00
{
return;
}
2026-04-17 21:01:20 +08:00
int desiredTotalCount = Mathf.Min(_runtimeHardCapacity, _activeCount + desiredInactiveCount);
if (desiredTotalCount <= _instancesByObject.Count)
{
return;
}
float frameBudgetSeconds = _config.warmupFrameBudgetMs * 0.001f;
int createdThisFrame = 0;
float frameStartTime = Time.realtimeSinceStartup;
while (_instancesByObject.Count < desiredTotalCount)
{
RuntimePooledInstance instance = CreateInstance();
ParkInactive(instance);
createdThisFrame++;
bool budgetReachedByCount = createdThisFrame >= _config.warmupBatchPerFrame;
bool budgetReachedByTime = frameBudgetSeconds > 0f &&
Time.realtimeSinceStartup - frameStartTime >= frameBudgetSeconds;
if (budgetReachedByCount || budgetReachedByTime)
{
createdThisFrame = 0;
frameStartTime = Time.realtimeSinceStartup;
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
}
}
2026-03-26 10:49:41 +08:00
}
public bool Release(GameObject gameObject)
{
if (gameObject == null || !_instancesByObject.TryGetValue(gameObject, out RuntimePooledInstance instance))
{
return false;
}
2026-04-17 21:01:20 +08:00
if (instance.State != RuntimePooledObjectState.Active)
2026-03-26 10:49:41 +08:00
{
return true;
}
2026-04-17 21:01:20 +08:00
_releaseCount++;
InternalReleaseInstance(instance, true);
2026-03-26 10:49:41 +08:00
return true;
}
public void NotifyInstanceDestroyed(RuntimePooledInstance instance)
{
if (instance == null)
{
return;
}
2026-04-17 21:01:20 +08:00
RemoveInstance(instance, false);
ReleaseLifecycleCache(instance.Lifecycle);
2026-03-26 10:49:41 +08:00
MemoryPool.Release(instance);
}
2026-04-17 21:01:20 +08:00
public void Trim(int maxDestroyCount, bool aggressive)
2026-03-26 10:49:41 +08:00
{
2026-04-17 21:01:20 +08:00
if (_config.trimPolicy == PoolTrimPolicy.None)
2026-03-26 10:49:41 +08:00
{
2026-04-17 21:01:20 +08:00
return;
}
if (maxDestroyCount <= 0)
{
maxDestroyCount = _config.trimBatchPerTick;
}
int destroyBudget = Mathf.Max(1, maxDestroyCount);
float now = Time.time;
int remainingRetained = Mathf.Max(_config.minRetained, 0);
while (destroyBudget > 0 && _inactiveInstances.First != null)
{
if (_instancesByObject.Count <= remainingRetained)
2026-03-26 10:49:41 +08:00
{
2026-04-17 21:01:20 +08:00
break;
}
2026-03-26 10:49:41 +08:00
2026-04-17 21:01:20 +08:00
RuntimePooledInstance candidate = _inactiveInstances.First.Value;
bool expired = _config.idleTrimDelay > 0f && now - candidate.LastReleaseTime >= _config.idleTrimDelay;
bool overSoftCapacity = _instancesByObject.Count > _config.softCapacity;
bool canAggressiveTrim = aggressive && _config.aggressiveTrimOnLowMemory;
if (!expired && !overSoftCapacity && !canAggressiveTrim)
{
break;
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
DestroyInstance(candidate);
destroyBudget--;
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
if (!CanKeepPrefabLoaded(now, aggressive))
2026-03-26 10:49:41 +08:00
{
_loader.UnloadAsset(_prefab);
_prefab = null;
}
}
2026-04-17 21:01:20 +08:00
public bool NeedsTrim(float now)
{
2026-04-17 21:01:20 +08:00
if (_inactiveInstances.First == null)
{
2026-04-17 21:01:20 +08:00
return false;
}
2026-04-17 21:01:20 +08:00
if (_instancesByObject.Count > _config.softCapacity)
{
2026-04-17 21:01:20 +08:00
return true;
}
2026-04-17 21:01:20 +08:00
if (_config.idleTrimDelay <= 0f)
{
2026-04-17 21:01:20 +08:00
return false;
}
2026-04-17 21:01:20 +08:00
return now - _inactiveInstances.First.Value.LastReleaseTime >= _config.idleTrimDelay;
}
public bool NeedsPrefabUnload(float now, bool aggressive)
{
return !CanKeepPrefabLoaded(now, aggressive);
}
2026-03-26 10:49:41 +08:00
public GameObjectPoolSnapshot CreateSnapshot()
{
var snapshot = MemoryPool.Acquire<GameObjectPoolSnapshot>();
2026-04-17 21:01:20 +08:00
snapshot.entryName = _config.entryName;
2026-03-26 10:49:41 +08:00
snapshot.group = _config.group;
snapshot.assetPath = _assetPath;
2026-04-17 21:01:20 +08:00
snapshot.loaderType = _config.loaderType;
snapshot.overflowPolicy = _config.overflowPolicy;
snapshot.minRetained = _config.minRetained;
snapshot.softCapacity = _config.softCapacity;
snapshot.hardCapacity = _runtimeHardCapacity;
2026-03-26 10:49:41 +08:00
snapshot.totalCount = _instancesByObject.Count;
snapshot.activeCount = _activeCount;
snapshot.inactiveCount = _inactiveInstances.Count;
snapshot.prefabLoaded = _prefab != null;
snapshot.prefabIdleDuration = PrefabIdleDuration;
2026-04-17 21:01:20 +08:00
snapshot.acquireCount = _acquireCount;
snapshot.releaseCount = _releaseCount;
snapshot.hitCount = _hitCount;
snapshot.missCount = _missCount;
snapshot.expandCount = _expandCount;
snapshot.exhaustedCount = _exhaustedCount;
snapshot.autoRecycleCount = _autoRecycleCount;
snapshot.destroyCount = _destroyCount;
snapshot.peakActive = _peakActive;
snapshot.peakTotal = _peakTotal;
2026-03-26 10:49:41 +08:00
foreach (RuntimePooledInstance instance in _instancesByObject.Values)
{
var instanceSnapshot = MemoryPool.Acquire<GameObjectPoolInstanceSnapshot>();
instanceSnapshot.instanceName = instance.GameObject != null ? instance.GameObject.name : "<destroyed>";
2026-04-17 21:01:20 +08:00
instanceSnapshot.isActive = instance.State == RuntimePooledObjectState.Active;
instanceSnapshot.idleDuration = instance.State == RuntimePooledObjectState.Active
? 0f
: Mathf.Max(0f, Time.time - instance.LastReleaseTime);
instanceSnapshot.lifeDuration = Mathf.Max(0f, Time.time - instance.SpawnTime);
2026-03-26 10:49:41 +08:00
instanceSnapshot.gameObject = instance.GameObject;
snapshot.instances.Add(instanceSnapshot);
}
2026-03-26 13:30:57 +08:00
snapshot.instances.Sort(InstanceSnapshotComparer);
2026-03-26 10:49:41 +08:00
return snapshot;
}
public void Shutdown()
{
2026-04-17 21:01:20 +08:00
_destroyBuffer.AddRange(_instancesByObject.Values);
for (int i = 0; i < _destroyBuffer.Count; i++)
2026-03-26 10:49:41 +08:00
{
2026-04-17 21:01:20 +08:00
DestroyInstance(_destroyBuffer[i]);
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
_destroyBuffer.Clear();
2026-03-26 10:49:41 +08:00
_inactiveInstances.Clear();
_instancesByObject.Clear();
_activeCount = 0;
if (_prefab != null)
{
_loader.UnloadAsset(_prefab);
_prefab = null;
}
if (_root != null)
{
GameObject.Destroy(_root.gameObject);
}
_root = null;
}
private void EnsurePrefabLoaded()
{
if (_prefab != null)
{
TouchPrefab();
return;
}
if (_prefabLoadSource != null)
{
throw new InvalidOperationException(
$"Pool asset '{_assetPath}' is being loaded asynchronously. Use the async acquire API.");
}
_prefab = _loader.LoadPrefab(_assetPath);
if (_prefab == null)
{
throw new InvalidOperationException($"Failed to load pooled prefab '{_assetPath}'.");
}
TouchPrefab();
}
private async UniTask EnsurePrefabLoadedAsync(CancellationToken cancellationToken)
{
if (_prefab != null)
{
TouchPrefab();
return;
}
if (_prefabLoadSource != null)
{
await _prefabLoadSource.Task.AttachExternalCancellation(cancellationToken);
return;
}
_prefabLoadSource = new UniTaskCompletionSource<GameObject>();
try
{
GameObject prefab = await _loader.LoadPrefabAsync(_assetPath, _shutdownToken);
if (prefab == null)
{
throw new InvalidOperationException($"Failed to load pooled prefab '{_assetPath}'.");
}
_prefab = prefab;
TouchPrefab();
_prefabLoadSource.TrySetResult(prefab);
}
catch (Exception exception)
{
_prefabLoadSource.TrySetException(exception);
throw;
}
finally
{
_prefabLoadSource = null;
}
}
2026-04-17 21:01:20 +08:00
private GameObject AcquirePrepared(in PoolSpawnContext context)
2026-03-26 10:49:41 +08:00
{
2026-04-17 21:01:20 +08:00
_acquireCount++;
2026-03-26 10:49:41 +08:00
RuntimePooledInstance instance = null;
if (_inactiveInstances.Last != null)
{
instance = _inactiveInstances.Last.Value;
RemoveInactiveNode(instance);
2026-04-17 21:01:20 +08:00
_hitCount++;
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
else
2026-03-26 10:49:41 +08:00
{
2026-04-17 21:01:20 +08:00
_missCount++;
instance = AcquireOnMiss();
2026-03-26 10:49:41 +08:00
}
if (instance == null)
{
2026-04-17 21:01:20 +08:00
_exhaustedCount++;
2026-03-26 10:49:41 +08:00
return null;
}
2026-04-17 21:01:20 +08:00
ActivateInstance(instance, context);
return instance.GameObject;
}
private RuntimePooledInstance AcquireOnMiss()
{
if (_instancesByObject.Count < _runtimeHardCapacity)
{
return CreateInstance();
}
switch (_config.overflowPolicy)
{
case PoolOverflowPolicy.AutoExpand:
if (_config.allowRuntimeExpand)
{
_runtimeHardCapacity++;
_expandCount++;
return CreateInstance();
}
return null;
case PoolOverflowPolicy.RecycleOldestInactive:
if (_inactiveInstances.First != null)
{
RuntimePooledInstance oldest = _inactiveInstances.First.Value;
RemoveInactiveNode(oldest);
return oldest;
}
return null;
case PoolOverflowPolicy.InstantiateOneShot:
return CreateInstance();
case PoolOverflowPolicy.DropNewestRequest:
case PoolOverflowPolicy.FailFast:
default:
return null;
}
}
private void ActivateInstance(RuntimePooledInstance instance, in PoolSpawnContext context)
{
instance.State = RuntimePooledObjectState.Active;
2026-03-26 10:49:41 +08:00
_activeCount++;
2026-04-17 21:01:20 +08:00
_peakActive = Mathf.Max(_peakActive, _activeCount);
_peakTotal = Mathf.Max(_peakTotal, _instancesByObject.Count);
2026-03-26 10:49:41 +08:00
TouchPrefab();
GameObject gameObject = instance.GameObject;
2026-04-17 21:01:20 +08:00
Transform transform = gameObject.transform;
transform.SetParent(context.Parent != null ? context.Parent : null, false);
if (context.Position != Vector3.zero || context.Rotation != Quaternion.identity)
2026-03-26 10:49:41 +08:00
{
2026-04-17 21:01:20 +08:00
transform.SetPositionAndRotation(context.Position, context.Rotation);
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
ApplyGetLifecycle(instance, context);
if (_config.activationMode == PoolActivationMode.SetActive)
{
gameObject.SetActive(true);
}
2026-03-26 10:49:41 +08:00
}
private RuntimePooledInstance CreateInstance()
{
GameObject gameObject = GameObject.Instantiate(_prefab);
2026-04-17 21:01:20 +08:00
gameObject.name = $"{_prefab.name}(Pool)";
2026-03-26 10:49:41 +08:00
gameObject.SetActive(false);
gameObject.transform.SetParent(_root, false);
PooledGameObjectMarker marker = gameObject.GetComponent<PooledGameObjectMarker>();
if (marker == null)
{
marker = gameObject.AddComponent<PooledGameObjectMarker>();
}
2026-04-17 21:01:20 +08:00
PooledAutoRecycle autoRecycle = gameObject.GetComponent<PooledAutoRecycle>();
if (autoRecycle == null)
{
autoRecycle = gameObject.AddComponent<PooledAutoRecycle>();
}
RuntimePoolLifecycleCache lifecycle = BuildLifecycleCache(gameObject);
2026-03-26 10:49:41 +08:00
var instance = MemoryPool.Acquire<RuntimePooledInstance>();
2026-04-17 21:01:20 +08:00
instance.Initialize(gameObject, marker, autoRecycle, lifecycle);
2026-03-26 10:49:41 +08:00
marker.Bind(this, instance);
_instancesByObject.Add(gameObject, instance);
_service.RegisterOwnedObject(gameObject, this);
2026-04-17 21:01:20 +08:00
InvokeOnPoolCreate(lifecycle);
2026-03-26 10:49:41 +08:00
TouchPrefab();
2026-04-17 21:01:20 +08:00
_peakTotal = Mathf.Max(_peakTotal, _instancesByObject.Count);
2026-03-26 10:49:41 +08:00
return instance;
}
private void ParkInactive(RuntimePooledInstance instance)
{
if (instance == null)
{
return;
}
2026-04-17 21:01:20 +08:00
instance.State = RuntimePooledObjectState.Inactive;
2026-03-26 10:49:41 +08:00
instance.LastReleaseTime = Time.time;
Transform transform = instance.GameObject.transform;
transform.SetParent(_root, false);
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
transform.localScale = Vector3.one;
instance.GameObject.SetActive(false);
instance.InactiveNode = _inactiveInstances.AddLast(instance);
_service.NotifyPoolStateChanged();
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
private void InternalReleaseInstance(RuntimePooledInstance instance, bool notifyStateChanged)
{
if (instance == null || instance.GameObject == null)
{
return;
}
if (instance.State != RuntimePooledObjectState.Active)
{
return;
}
instance.State = RuntimePooledObjectState.Releasing;
instance.AutoRecycle?.Cancel();
ApplyReleaseLifecycle(instance);
if (_config.activationMode == PoolActivationMode.SetActive)
{
instance.GameObject.SetActive(false);
}
_activeCount = Mathf.Max(0, _activeCount - 1);
ParkInactive(instance);
TouchPrefab();
if (notifyStateChanged)
{
_service.NotifyPoolStateChanged();
}
}
2026-03-26 10:49:41 +08:00
private void DestroyInstance(RuntimePooledInstance instance)
{
if (instance == null)
{
return;
}
2026-04-17 21:01:20 +08:00
instance.State = RuntimePooledObjectState.Destroying;
RemoveInstance(instance, true);
2026-03-26 10:49:41 +08:00
if (instance.GameObject != null)
{
2026-04-17 21:01:20 +08:00
InvokeOnPoolDestroy(instance.Lifecycle);
instance.AutoRecycle?.Cancel();
2026-03-26 10:49:41 +08:00
instance.Marker?.Detach();
GameObject.Destroy(instance.GameObject);
}
2026-04-17 21:01:20 +08:00
ReleaseLifecycleCache(instance.Lifecycle);
2026-03-26 10:49:41 +08:00
MemoryPool.Release(instance);
}
2026-04-17 21:01:20 +08:00
private void RemoveInstance(RuntimePooledInstance instance, bool countDestroy)
2026-03-26 10:49:41 +08:00
{
GameObject gameObject = instance?.GameObject;
if (gameObject == null)
{
return;
}
RemoveInactiveNode(instance);
2026-04-17 21:01:20 +08:00
if (instance.State == RuntimePooledObjectState.Active)
2026-03-26 10:49:41 +08:00
{
_activeCount = Mathf.Max(0, _activeCount - 1);
}
_instancesByObject.Remove(gameObject);
_service.UnregisterOwnedObject(gameObject);
2026-04-17 21:01:20 +08:00
if (countDestroy)
{
_destroyCount++;
}
2026-03-26 10:49:41 +08:00
TouchPrefab();
_service.NotifyPoolStateChanged();
2026-03-26 10:49:41 +08:00
}
private void RemoveInactiveNode(RuntimePooledInstance instance)
{
if (instance?.InactiveNode == null)
{
return;
}
_inactiveInstances.Remove(instance.InactiveNode);
instance.InactiveNode = null;
}
2026-04-17 21:01:20 +08:00
private void ApplyGetLifecycle(RuntimePooledInstance instance, in PoolSpawnContext context)
{
if (instance.Lifecycle == null)
{
return;
}
if (_config.activationMode == PoolActivationMode.SleepWake && instance.Lifecycle.sleepables != null)
{
for (int i = 0; i < instance.Lifecycle.sleepables.Length; i++)
{
instance.Lifecycle.sleepables[i]?.ExitSleep(context);
}
}
if (instance.Lifecycle.poolables != null)
{
for (int i = 0; i < instance.Lifecycle.poolables.Length; i++)
{
instance.Lifecycle.poolables[i]?.OnPoolGet(context);
}
}
ScheduleAutoRecycle(instance);
}
private void ApplyReleaseLifecycle(RuntimePooledInstance instance)
{
if (instance.Lifecycle == null)
{
return;
}
if (_config.resetMode == PoolResetMode.FullReset || _config.resetMode == PoolResetMode.PoolableCallbacks)
{
if (instance.Lifecycle.physicsResetters != null)
{
for (int i = 0; i < instance.Lifecycle.physicsResetters.Length; i++)
{
instance.Lifecycle.physicsResetters[i]?.ResetPhysicsState();
}
}
if (instance.Lifecycle.visualResetters != null)
{
for (int i = 0; i < instance.Lifecycle.visualResetters.Length; i++)
{
instance.Lifecycle.visualResetters[i]?.ResetVisualState();
}
}
if (instance.Lifecycle.animationResetters != null)
{
for (int i = 0; i < instance.Lifecycle.animationResetters.Length; i++)
{
instance.Lifecycle.animationResetters[i]?.ResetAnimationState();
}
}
}
if (instance.Lifecycle.poolables != null)
{
for (int i = 0; i < instance.Lifecycle.poolables.Length; i++)
{
instance.Lifecycle.poolables[i]?.OnPoolRelease();
}
}
if (_config.activationMode == PoolActivationMode.SleepWake && instance.Lifecycle.sleepables != null)
{
for (int i = 0; i < instance.Lifecycle.sleepables.Length; i++)
{
instance.Lifecycle.sleepables[i]?.EnterSleep();
}
}
}
private void InvokeOnPoolCreate(RuntimePoolLifecycleCache lifecycle)
{
if (lifecycle?.poolables == null)
{
return;
}
for (int i = 0; i < lifecycle.poolables.Length; i++)
{
lifecycle.poolables[i]?.OnPoolCreate();
}
}
private void InvokeOnPoolDestroy(RuntimePoolLifecycleCache lifecycle)
{
if (lifecycle?.poolables == null)
{
return;
}
for (int i = 0; i < lifecycle.poolables.Length; i++)
{
lifecycle.poolables[i]?.OnPoolDestroy();
}
}
private void ScheduleAutoRecycle(RuntimePooledInstance instance)
{
if (instance?.AutoRecycle == null)
{
return;
}
float delay = _config.autoRecycleDelay;
if (delay <= 0f && instance.Lifecycle?.autoRecyclers != null)
{
for (int i = 0; i < instance.Lifecycle.autoRecyclers.Length; i++)
{
if (instance.Lifecycle.autoRecyclers[i] != null &&
instance.Lifecycle.autoRecyclers[i].TryGetAutoRecycleDelay(out delay) &&
delay > 0f)
{
break;
}
}
}
if (delay > 0f)
{
_autoRecycleCount++;
instance.AutoRecycle.Schedule(_service, delay);
}
}
private RuntimePoolLifecycleCache BuildLifecycleCache(GameObject gameObject)
{
var cache = MemoryPool.Acquire<RuntimePoolLifecycleCache>();
cache.poolables = gameObject.GetComponentsInChildren<IGameObjectPoolable>(true);
cache.sleepables = gameObject.GetComponentsInChildren<IPoolSleepable>(true);
cache.physicsResetters = gameObject.GetComponentsInChildren<IPoolResettablePhysics>(true);
cache.visualResetters = gameObject.GetComponentsInChildren<IPoolResettableVisual>(true);
cache.animationResetters = gameObject.GetComponentsInChildren<IPoolResettableAnimation>(true);
cache.autoRecyclers = gameObject.GetComponentsInChildren<IPoolAutoRecycle>(true);
return cache;
}
private void ReleaseLifecycleCache(RuntimePoolLifecycleCache lifecycle)
{
if (lifecycle == null)
{
return;
}
MemoryPool.Release(lifecycle);
}
private bool CanKeepPrefabLoaded(float now, bool aggressive)
{
if (_prefab == null)
{
return true;
}
if (_config.keepPrefabResident)
{
return true;
}
if (_instancesByObject.Count > 0)
{
return true;
}
if (_config.prefabUnloadDelay <= 0f)
{
return false;
}
if (aggressive && _config.aggressiveTrimOnLowMemory)
{
return false;
}
return now - _lastPrefabTouchTime < _config.prefabUnloadDelay;
}
2026-03-26 10:49:41 +08:00
private void TouchPrefab()
{
_lastPrefabTouchTime = Time.time;
}
public void Clear()
{
_config = null;
_assetPath = null;
_loader = null;
_service = null;
_shutdownToken = default;
_instancesByObject.Clear();
_inactiveInstances.Clear();
2026-04-17 21:01:20 +08:00
_destroyBuffer.Clear();
2026-03-26 10:49:41 +08:00
_root = null;
_prefab = null;
_prefabLoadSource = null;
_activeCount = 0;
_lastPrefabTouchTime = 0f;
2026-04-17 21:01:20 +08:00
_runtimeHardCapacity = 0;
_acquireCount = 0;
_releaseCount = 0;
_hitCount = 0;
_missCount = 0;
_expandCount = 0;
_exhaustedCount = 0;
_autoRecycleCount = 0;
_destroyCount = 0;
_peakActive = 0;
_peakTotal = 0;
2026-03-26 10:49:41 +08:00
}
private static string SanitizeName(string value)
{
return string.IsNullOrWhiteSpace(value)
? "Unnamed"
: value.Replace('/', '_').Replace('\\', '_').Replace(':', '_');
}
}
}