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

584 lines
18 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
{
[Serializable]
public sealed class GameObjectPoolInstanceSnapshot : IMemory
{
public string instanceName;
public bool isActive;
public float idleDuration;
public GameObject gameObject;
public void Clear()
{
instanceName = null;
isActive = false;
idleDuration = 0f;
gameObject = null;
}
}
[Serializable]
public sealed class GameObjectPoolSnapshot : IMemory
{
public string group;
public string assetPath;
public PoolMatchMode matchMode;
public PoolResourceLoaderType loaderType;
public int capacity;
public int totalCount;
public int activeCount;
public int inactiveCount;
public float instanceIdleTimeout;
public float prefabUnloadDelay;
public bool prefabLoaded;
public float prefabIdleDuration;
public List<GameObjectPoolInstanceSnapshot> instances = new List<GameObjectPoolInstanceSnapshot>();
public void Clear()
{
group = null;
assetPath = null;
matchMode = default;
loaderType = default;
capacity = 0;
totalCount = 0;
activeCount = 0;
inactiveCount = 0;
instanceIdleTimeout = 0f;
prefabUnloadDelay = 0f;
prefabLoaded = false;
prefabIdleDuration = 0f;
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);
}
}
}
internal sealed class RuntimePooledInstance : IMemory
{
public GameObject GameObject { get; private set; }
public PooledGameObjectMarker Marker { get; private set; }
public LinkedListNode<RuntimePooledInstance> InactiveNode { get; set; }
public bool IsActive { get; set; }
public float LastReleaseTime { get; set; }
public void Initialize(GameObject gameObject, PooledGameObjectMarker marker)
{
GameObject = gameObject;
Marker = marker;
}
public void Clear()
{
GameObject = null;
Marker = null;
InactiveNode = null;
IsActive = false;
LastReleaseTime = 0f;
}
}
internal sealed class RuntimePrefabPool : IMemory
{
private PoolConfig _config;
private string _assetPath;
private IResourceLoader _loader;
private GameObjectPool _service;
private CancellationToken _shutdownToken;
private Dictionary<GameObject, RuntimePooledInstance> _instancesByObject;
private LinkedList<RuntimePooledInstance> _inactiveInstances;
private Transform _root;
private GameObject _prefab;
private UniTaskCompletionSource<GameObject> _prefabLoadSource;
private int _activeCount;
private float _lastPrefabTouchTime;
public RuntimePrefabPool()
{
_instancesByObject = new Dictionary<GameObject, RuntimePooledInstance>();
_inactiveInstances = new LinkedList<RuntimePooledInstance>();
}
public void Initialize(
PoolConfig config,
string assetPath,
IResourceLoader loader,
GameObjectPool service,
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;
_instancesByObject.Clear();
_inactiveInstances.Clear();
_prefab = null;
_prefabLoadSource = null;
_activeCount = 0;
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;
public PoolConfig Config => _config;
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);
public GameObject Acquire(Transform parent)
{
EnsurePrefabLoaded();
return AcquirePrepared(parent);
}
public async UniTask<GameObject> AcquireAsync(Transform parent, CancellationToken cancellationToken)
{
await EnsurePrefabLoadedAsync(cancellationToken);
return AcquirePrepared(parent);
}
public void Warmup(int count)
{
if (count <= 0)
{
return;
}
EnsurePrefabLoaded();
WarmupPrepared(count);
}
public async UniTask WarmupAsync(int count, CancellationToken cancellationToken)
{
if (count <= 0)
{
return;
}
await EnsurePrefabLoadedAsync(cancellationToken);
WarmupPrepared(count);
}
public bool Release(GameObject gameObject)
{
if (gameObject == null || !_instancesByObject.TryGetValue(gameObject, out RuntimePooledInstance instance))
{
return false;
}
if (!instance.IsActive)
{
return true;
}
instance.IsActive = false;
instance.LastReleaseTime = Time.time;
_activeCount = Mathf.Max(0, _activeCount - 1);
Transform transform = gameObject.transform;
transform.SetParent(_root, false);
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
transform.localScale = Vector3.one;
gameObject.SetActive(false);
instance.InactiveNode = _inactiveInstances.AddLast(instance);
TouchPrefab();
return true;
}
public void NotifyInstanceDestroyed(RuntimePooledInstance instance)
{
if (instance == null)
{
return;
}
RemoveInstance(instance);
MemoryPool.Release(instance);
}
public void TrimExpiredInstances(float now)
{
if (_config.instanceIdleTimeout > 0f)
{
while (_inactiveInstances.First != null)
{
RuntimePooledInstance candidate = _inactiveInstances.First.Value;
if (now - candidate.LastReleaseTime < _config.instanceIdleTimeout)
{
break;
}
DestroyInstance(candidate);
}
}
if (_prefab != null &&
_instancesByObject.Count == 0 &&
_config.prefabUnloadDelay > 0f &&
now - _lastPrefabTouchTime >= _config.prefabUnloadDelay)
{
_loader.UnloadAsset(_prefab);
_prefab = null;
}
}
public GameObjectPoolSnapshot CreateSnapshot()
{
var snapshot = MemoryPool.Acquire<GameObjectPoolSnapshot>();
snapshot.group = _config.group;
snapshot.assetPath = _assetPath;
snapshot.matchMode = _config.matchMode;
snapshot.loaderType = _config.resourceLoaderType;
snapshot.capacity = _config.capacity;
snapshot.totalCount = _instancesByObject.Count;
snapshot.activeCount = _activeCount;
snapshot.inactiveCount = _inactiveInstances.Count;
snapshot.instanceIdleTimeout = _config.instanceIdleTimeout;
snapshot.prefabUnloadDelay = _config.prefabUnloadDelay;
snapshot.prefabLoaded = _prefab != null;
snapshot.prefabIdleDuration = PrefabIdleDuration;
foreach (RuntimePooledInstance instance in _instancesByObject.Values)
{
var instanceSnapshot = MemoryPool.Acquire<GameObjectPoolInstanceSnapshot>();
instanceSnapshot.instanceName = instance.GameObject != null ? instance.GameObject.name : "<destroyed>";
instanceSnapshot.isActive = instance.IsActive;
instanceSnapshot.idleDuration = instance.IsActive ? 0f : Mathf.Max(0f, Time.time - instance.LastReleaseTime);
instanceSnapshot.gameObject = instance.GameObject;
snapshot.instances.Add(instanceSnapshot);
}
snapshot.instances.Sort((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);
});
return snapshot;
}
public void Shutdown()
{
var instances = new List<RuntimePooledInstance>(_instancesByObject.Values);
foreach (RuntimePooledInstance instance in instances)
{
DestroyInstance(instance);
}
_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;
}
}
private void WarmupPrepared(int requestedCount)
{
int targetCount = Mathf.Clamp(requestedCount, 0, _config.capacity);
if (targetCount <= _instancesByObject.Count)
{
return;
}
int toCreate = targetCount - _instancesByObject.Count;
for (int i = 0; i < toCreate; i++)
{
RuntimePooledInstance instance = CreateInstance();
ParkInactive(instance);
}
}
private GameObject AcquirePrepared(Transform parent)
{
RuntimePooledInstance instance = null;
if (_inactiveInstances.Last != null)
{
instance = _inactiveInstances.Last.Value;
RemoveInactiveNode(instance);
}
else if (_instancesByObject.Count < _config.capacity)
{
instance = CreateInstance();
}
if (instance == null)
{
Log.Warning($"Pool exhausted for '{_assetPath}'. Capacity: {_config.capacity}");
return null;
}
instance.IsActive = true;
_activeCount++;
TouchPrefab();
GameObject gameObject = instance.GameObject;
if (parent != null)
{
gameObject.transform.SetParent(parent);
}
gameObject.SetActive(true);
return gameObject;
}
private RuntimePooledInstance CreateInstance()
{
GameObject gameObject = GameObject.Instantiate(_prefab);
gameObject.SetActive(false);
gameObject.transform.SetParent(_root, false);
PooledGameObjectMarker marker = gameObject.GetComponent<PooledGameObjectMarker>();
if (marker == null)
{
marker = gameObject.AddComponent<PooledGameObjectMarker>();
}
var instance = MemoryPool.Acquire<RuntimePooledInstance>();
instance.Initialize(gameObject, marker);
marker.Bind(this, instance);
_instancesByObject.Add(gameObject, instance);
_service.RegisterOwnedObject(gameObject, this);
TouchPrefab();
return instance;
}
private void ParkInactive(RuntimePooledInstance instance)
{
if (instance == null)
{
return;
}
instance.IsActive = false;
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);
}
private void DestroyInstance(RuntimePooledInstance instance)
{
if (instance == null)
{
return;
}
RemoveInstance(instance);
if (instance.GameObject != null)
{
instance.Marker?.Detach();
GameObject.Destroy(instance.GameObject);
}
MemoryPool.Release(instance);
}
private void RemoveInstance(RuntimePooledInstance instance)
{
GameObject gameObject = instance?.GameObject;
if (gameObject == null)
{
return;
}
RemoveInactiveNode(instance);
if (instance.IsActive)
{
instance.IsActive = false;
_activeCount = Mathf.Max(0, _activeCount - 1);
}
_instancesByObject.Remove(gameObject);
_service.UnregisterOwnedObject(gameObject);
TouchPrefab();
}
private void RemoveInactiveNode(RuntimePooledInstance instance)
{
if (instance?.InactiveNode == null)
{
return;
}
_inactiveInstances.Remove(instance.InactiveNode);
instance.InactiveNode = null;
}
private void TouchPrefab()
{
_lastPrefabTouchTime = Time.time;
}
public void Clear()
{
_config = null;
_assetPath = null;
_loader = null;
_service = null;
_shutdownToken = default;
_instancesByObject.Clear();
_inactiveInstances.Clear();
_root = null;
_prefab = null;
_prefabLoadSource = null;
_activeCount = 0;
_lastPrefabTouchTime = 0f;
}
private static string SanitizeName(string value)
{
return string.IsNullOrWhiteSpace(value)
? "Unnamed"
: value.Replace('/', '_').Replace('\\', '_').Replace(':', '_');
}
}
}