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 instances = new List(); 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 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 static readonly Comparison 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); }; private PoolConfig _config; private string _assetPath; private IResourceLoader _loader; private GameObjectPool _service; private CancellationToken _shutdownToken; private Dictionary _instancesByObject; private LinkedList _inactiveInstances; private List _shutdownBuffer; private Transform _root; private GameObject _prefab; private UniTaskCompletionSource _prefabLoadSource; private int _activeCount; private float _lastPrefabTouchTime; public RuntimePrefabPool() { _instancesByObject = new Dictionary(); _inactiveInstances = new LinkedList(); _shutdownBuffer = new List(); } 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 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); gameObject.SetActive(false); Transform transform = gameObject.transform; transform.SetParent(_root, false); transform.localPosition = Vector3.zero; transform.localRotation = Quaternion.identity; transform.localScale = Vector3.one; 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(); 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(); instanceSnapshot.instanceName = instance.GameObject != null ? instance.GameObject.name : ""; 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(InstanceSnapshotComparer); return snapshot; } public void Shutdown() { _shutdownBuffer.AddRange(_instancesByObject.Values); foreach (RuntimePooledInstance instance in _shutdownBuffer) { DestroyInstance(instance); } _shutdownBuffer.Clear(); _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(); 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, false); } 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(); if (marker == null) { marker = gameObject.AddComponent(); } var instance = MemoryPool.Acquire(); 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(); _shutdownBuffer.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(':', '_'); } } }