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;
|
2026-03-26 16:14:05 +08:00
|
|
|
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,
|
2026-03-26 16:14:05 +08:00
|
|
|
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-03-31 17:25:20 +08:00
|
|
|
{
|
2026-04-17 21:01:20 +08:00
|
|
|
if (_inactiveInstances.First == null)
|
2026-03-31 17:25:20 +08:00
|
|
|
{
|
2026-04-17 21:01:20 +08:00
|
|
|
return false;
|
2026-03-31 17:25:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 21:01:20 +08:00
|
|
|
if (_instancesByObject.Count > _config.softCapacity)
|
2026-03-31 17:25:20 +08:00
|
|
|
{
|
2026-04-17 21:01:20 +08:00
|
|
|
return true;
|
2026-03-31 17:25:20 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-17 21:01:20 +08:00
|
|
|
if (_config.idleTrimDelay <= 0f)
|
2026-03-31 17:25:20 +08:00
|
|
|
{
|
2026-04-17 21:01:20 +08:00
|
|
|
return false;
|
2026-03-31 17:25:20 +08:00
|
|
|
}
|
|
|
|
|
|
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-31 17:25:20 +08:00
|
|
|
}
|
|
|
|
|
|
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);
|
2026-03-31 17:25:20 +08:00
|
|
|
_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();
|
2026-03-31 17:25:20 +08:00
|
|
|
_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(':', '_');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|