using System; using System.Collections.Generic; using System.Threading; using AlicizaX.ObjectPool; using AlicizaX.Resource.Runtime; using Cysharp.Text; using Cysharp.Threading.Tasks; using UnityEngine; namespace AlicizaX { public sealed class GameObjectPoolManager : MonoServiceBehaviour { private enum BootstrapState : byte { Waiting = 0, Ready = 1 } private struct MaintenanceNode { public float dueTime; public int poolIndex; } private static readonly Comparison SnapshotComparer = CompareSnapshot; private const PoolResourceLoaderType DefaultDirectLoader = PoolResourceLoaderType.AssetBundle; [Header("Config Path")] public string poolConfigPath = "Assets/Bundles/Configs/PoolConfig"; [Header("Show Detailed Info")] public bool showDetailedInfo = true; [SerializeField] internal Transform poolContainer; private readonly IResourceLoader[] _resourceLoaders = new IResourceLoader[2]; private readonly List _debugSnapshots = new List(16); private RuntimeGameObjectPool[] _pools = new RuntimeGameObjectPool[8]; private int _poolCount; private PoolCompiledCatalog _catalog = PoolCompiledCatalog.Empty(); private StringOpenHashMap[] _rulePoolMaps = Array.Empty(); private bool[] _rulePoolMapInitialized = Array.Empty(); private StringOpenHashMap _directLoadWarnedPaths = new StringOpenHashMap(8); private StringOpenHashMap _groupRootMap = new StringOpenHashMap(8); private Transform[] _groupRoots = new Transform[4]; private int _groupRootCount; private BootstrapState _bootstrapState; private CancellationTokenSource _shutdownTokenSource; private MaintenanceNode[] _maintenanceHeap = new MaintenanceNode[8]; private int _maintenanceCount; public bool IsReady => _bootstrapState == BootstrapState.Ready; internal CancellationToken ShutdownToken => _shutdownTokenSource == null ? default : _shutdownTokenSource.Token; protected override void OnInitialize() { _shutdownTokenSource = new CancellationTokenSource(); EnsureDefaultResourceLoaders(); EnsurePoolContainer(); Application.lowMemory += OnLowMemory; _bootstrapState = BootstrapState.Waiting; enabled = true; } protected override void OnDestroyService() { Application.lowMemory -= OnLowMemory; _shutdownTokenSource?.Cancel(); ClearAllPools(); if (poolContainer != null) { Destroy(poolContainer.gameObject); poolContainer = null; } _shutdownTokenSource?.Dispose(); _shutdownTokenSource = null; } private void Update() { if (_bootstrapState == BootstrapState.Waiting) { TryBootstrap(); } if (_bootstrapState != BootstrapState.Ready) { enabled = true; return; } float now = Time.time; ProcessDueMaintenance(now); enabled = _bootstrapState == BootstrapState.Waiting || _maintenanceCount > 0; } public void SetResourceLoader(IResourceLoader resourceLoader) { SetResourceLoader(PoolResourceLoaderType.AssetBundle, resourceLoader); } public void SetResourceLoader(PoolResourceLoaderType loaderType, IResourceLoader resourceLoader) { if (resourceLoader == null) { throw new ArgumentNullException(nameof(resourceLoader)); } _resourceLoaders[(int)loaderType] = resourceLoader; } public GameObject GetGameObject(string assetPath, Transform parent = null) { EnsureReadyForSyncUse(); string normalizedAssetPath = PoolEntry.NormalizeAssetPath(assetPath); PoolSpawnContext context = PoolSpawnContext.Create(normalizedAssetPath, parent); return GetGameObjectInternal(normalizedAssetPath, context); } public async UniTask GetGameObjectAsync( string assetPath, Transform parent = null, CancellationToken cancellationToken = default) { await EnsureReadyAsync(cancellationToken); string normalizedAssetPath = PoolEntry.NormalizeAssetPath(assetPath); PoolSpawnContext context = PoolSpawnContext.Create(normalizedAssetPath, parent); return await GetGameObjectInternalAsync(normalizedAssetPath, context, cancellationToken); } public void Release(GameObject gameObject) { if (gameObject == null) { return; } if (gameObject.TryGetComponent(out GameObjectPoolHandle handle) && handle.TryRelease()) { return; } Destroy(gameObject); } public async UniTask PreloadAsync(string assetPath, int count = 1, CancellationToken cancellationToken = default) { await EnsureReadyAsync(cancellationToken); await PreloadInternalAsync(PoolEntry.NormalizeAssetPath(assetPath), count, cancellationToken); } public void ForceCleanup() { if (_bootstrapState != BootstrapState.Ready) { return; } float now = Time.time; for (int i = 0; i < _poolCount; i++) { RuntimeGameObjectPool pool = _pools[i]; if (pool != null) { pool.ExecuteMaintenance(now, false); } } } public void ClearAllPools() { for (int i = 0; i < _poolCount; i++) { RuntimeGameObjectPool pool = _pools[i]; if (pool == null) { continue; } pool.Shutdown(); MemoryPool.Release(pool); _pools[i] = null; } _poolCount = 0; _maintenanceCount = 0; for (int i = 0; i < _rulePoolMapInitialized.Length; i++) { if (_rulePoolMapInitialized[i]) { _rulePoolMaps[i].Clear(); } } ClearGroupRoots(); _directLoadWarnedPaths.Clear(); ReleaseDebugSnapshots(); } public GameObjectPoolSummarySnapshot GetDebugSummary() { int loadedPrefabCount = 0; int totalInstanceCount = 0; int activeInstanceCount = 0; int inactiveInstanceCount = 0; for (int i = 0; i < _poolCount; i++) { RuntimeGameObjectPool pool = _pools[i]; if (pool == null) { continue; } if (pool.IsPrefabLoaded) { loadedPrefabCount++; } totalInstanceCount += pool.TotalCount; activeInstanceCount += pool.ActiveCount; inactiveInstanceCount += pool.InactiveCount; } return new GameObjectPoolSummarySnapshot( _bootstrapState == BootstrapState.Ready, _bootstrapState == BootstrapState.Waiting, _poolCount, loadedPrefabCount, totalInstanceCount, activeInstanceCount, inactiveInstanceCount, _maintenanceCount); } public List GetDebugSnapshots() { ReleaseDebugSnapshots(); for (int i = 0; i < _poolCount; i++) { RuntimeGameObjectPool pool = _pools[i]; if (pool != null) { _debugSnapshots.Add(pool.CreateSnapshot()); } } _debugSnapshots.Sort(SnapshotComparer); return _debugSnapshots; } internal void ScheduleMaintenance(int poolIndex, float dueTime, ref int heapIndex) { if (dueTime >= float.MaxValue) { RemoveMaintenance(ref heapIndex); return; } if (heapIndex >= 0) { _maintenanceHeap[heapIndex].dueTime = dueTime; _maintenanceHeap[heapIndex].poolIndex = poolIndex; SiftMaintenanceUp(heapIndex); SiftMaintenanceDown(heapIndex); enabled = true; return; } EnsureMaintenanceCapacity(_maintenanceCount + 1); int insertIndex = _maintenanceCount++; _maintenanceHeap[insertIndex].dueTime = dueTime; _maintenanceHeap[insertIndex].poolIndex = poolIndex; heapIndex = insertIndex; _pools[poolIndex].SetMaintenanceHeapIndex(insertIndex); SiftMaintenanceUp(insertIndex); enabled = true; } internal void RemoveMaintenance(ref int heapIndex) { if (heapIndex < 0 || heapIndex >= _maintenanceCount) { heapIndex = -1; return; } RemoveMaintenanceAt(heapIndex); heapIndex = -1; } private bool TryBootstrap() { if (_bootstrapState == BootstrapState.Ready) { return true; } if (!YooAsset.YooAssets.Initialized) { return false; } LoadCatalog(); _bootstrapState = BootstrapState.Ready; enabled = _maintenanceCount > 0; return true; } private void LoadCatalog() { IResourceService resourceService = AppServices.Require(); PoolConfigScriptableObject configAsset = resourceService.LoadAsset(poolConfigPath); _catalog = configAsset == null ? PoolCompiledCatalog.Empty() : configAsset.BuildCatalog(); if (configAsset != null) { resourceService.UnloadAsset(configAsset); } _rulePoolMaps = _catalog.RuleCount == 0 ? Array.Empty() : new StringOpenHashMap[_catalog.RuleCount]; _rulePoolMapInitialized = _catalog.RuleCount == 0 ? Array.Empty() : new bool[_catalog.RuleCount]; } private GameObject GetGameObjectInternal(string assetPath, in PoolSpawnContext context) { int ruleIndex = _catalog.Resolve(assetPath, null); if (ruleIndex < 0) { WarnDirectLoadFallback(assetPath); return LoadDirect(assetPath, context.Parent); } ref readonly PoolCompiledRule rule = ref _catalog.GetRule(ruleIndex); RuntimeGameObjectPool pool = GetOrCreatePool(ruleIndex, assetPath); return pool.Acquire(context.WithGroup(rule.group)); } private async UniTask GetGameObjectInternalAsync( string assetPath, PoolSpawnContext context, CancellationToken cancellationToken) { int ruleIndex = _catalog.Resolve(assetPath, null); if (ruleIndex < 0) { WarnDirectLoadFallback(assetPath); return await LoadDirectAsync(assetPath, context.Parent, cancellationToken); } string ruleGroup = _catalog.GetRule(ruleIndex).group; RuntimeGameObjectPool pool = GetOrCreatePool(ruleIndex, assetPath); return await pool.AcquireAsync(context.WithGroup(ruleGroup), cancellationToken); } private async UniTask PreloadInternalAsync( string assetPath, int count, CancellationToken cancellationToken) { if (count <= 0) { return; } int ruleIndex = _catalog.Resolve(assetPath, null); if (ruleIndex < 0) { WarnDirectLoadFallback(assetPath); return; } RuntimeGameObjectPool pool = GetOrCreatePool(ruleIndex, assetPath); await pool.WarmupAsync(count, cancellationToken); } private RuntimeGameObjectPool GetOrCreatePool(int ruleIndex, string assetPath) { if (!_rulePoolMapInitialized[ruleIndex]) { _rulePoolMaps[ruleIndex] = new StringOpenHashMap(4); _rulePoolMapInitialized[ruleIndex] = true; } if (_rulePoolMaps[ruleIndex].TryGetValue(assetPath, out int poolIndex)) { return _pools[poolIndex]; } EnsurePoolCapacity(_poolCount + 1); ref readonly PoolCompiledRule rule = ref _catalog.GetRule(ruleIndex); var pool = MemoryPool.Acquire(); pool.Initialize(this, _poolCount, rule, assetPath, GetResourceLoader(rule.loaderType), GetOrCreateGroupRoot(rule.group)); _pools[_poolCount] = pool; _rulePoolMaps[ruleIndex].AddOrUpdate(assetPath, _poolCount); _poolCount++; enabled = true; return pool; } private GameObject LoadDirect(string assetPath, Transform parent) { IResourceLoader loader = GetResourceLoader(DefaultDirectLoader); return loader.LoadGameObject(assetPath, parent); } private async UniTask LoadDirectAsync(string assetPath, Transform parent, CancellationToken cancellationToken) { IResourceLoader loader = GetResourceLoader(DefaultDirectLoader); return await loader.LoadGameObjectAsync(assetPath, parent, cancellationToken); } private IResourceLoader GetResourceLoader(PoolResourceLoaderType loaderType) { IResourceLoader loader = _resourceLoaders[(int)loaderType]; if (loader == null) { throw new InvalidOperationException(ZString.Format("Resource loader not registered: {0}.", loaderType)); } return loader; } private void EnsureDefaultResourceLoaders() { if (_resourceLoaders[(int)PoolResourceLoaderType.AssetBundle] == null) { _resourceLoaders[(int)PoolResourceLoaderType.AssetBundle] = new AssetBundleResourceLoader(); } if (_resourceLoaders[(int)PoolResourceLoaderType.Resources] == null) { _resourceLoaders[(int)PoolResourceLoaderType.Resources] = new UnityResourcesLoader(); } } private void EnsurePoolContainer() { if (poolContainer != null) { return; } GameObject container = new GameObject("GameObjectPoolContainer"); poolContainer = container.transform; poolContainer.SetParent(transform, false); } private Transform GetOrCreateGroupRoot(string group) { string groupName = string.IsNullOrWhiteSpace(group) ? PoolEntry.DefaultGroup : group.Trim(); if (_groupRootMap.TryGetValue(groupName, out int groupIndex)) { Transform existingRoot = _groupRoots[groupIndex]; if (existingRoot != null) { return existingRoot; } } EnsureGroupRootCapacity(_groupRootCount + 1); GameObject rootObject = new GameObject(ZString.Format("[{0}]", groupName)); Transform root = rootObject.transform; root.SetParent(poolContainer, false); rootObject.SetActive(true); int newIndex = _groupRootCount++; _groupRoots[newIndex] = root; _groupRootMap.AddOrUpdate(groupName, newIndex); return root; } private void EnsureGroupRootCapacity(int required) { if (_groupRoots.Length >= required) { return; } int newCapacity = Mathf.Max(required, _groupRoots.Length << 1); var newRoots = new Transform[newCapacity]; Array.Copy(_groupRoots, 0, newRoots, 0, _groupRootCount); _groupRoots = newRoots; } private void ClearGroupRoots() { for (int i = 0; i < _groupRootCount; i++) { Transform root = _groupRoots[i]; if (root != null) { Destroy(root.gameObject); _groupRoots[i] = null; } } _groupRootCount = 0; _groupRootMap.Clear(); } private void WarnDirectLoadFallback(string assetPath) { if (_directLoadWarnedPaths.TryGetValue(assetPath, out _)) { return; } _directLoadWarnedPaths.AddOrUpdate(assetPath, 1); Log.Warning(ZString.Format( "[GameObjectPool] Asset not found in PoolConfig. Fallback to direct load and Release() will destroy it. Asset:{0}", assetPath)); } private void EnsureReadyForSyncUse() { if (!TryBootstrap()) { throw new InvalidOperationException( "GameObjectPool is waiting for resource bootstrap. Use async APIs or call after YooAssets initialization."); } } private async UniTask EnsureReadyAsync(CancellationToken cancellationToken) { while (!TryBootstrap()) { cancellationToken.ThrowIfCancellationRequested(); await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken); } } private void OnLowMemory() { if (_bootstrapState != BootstrapState.Ready) { return; } float now = Time.time; for (int i = 0; i < _poolCount; i++) { RuntimeGameObjectPool pool = _pools[i]; if (pool != null) { pool.ExecuteMaintenance(now, true); } } } private void ProcessDueMaintenance(float now) { while (_maintenanceCount > 0) { MaintenanceNode node = _maintenanceHeap[0]; if (node.dueTime > now) { return; } RemoveMaintenanceAt(0); RuntimeGameObjectPool pool = _pools[node.poolIndex]; pool?.ExecuteMaintenance(now, false); } } private void EnsurePoolCapacity(int required) { if (_pools.Length >= required) { return; } int newCapacity = Mathf.Max(required, _pools.Length << 1); var newPools = new RuntimeGameObjectPool[newCapacity]; Array.Copy(_pools, 0, newPools, 0, _poolCount); _pools = newPools; } private void EnsureMaintenanceCapacity(int required) { if (_maintenanceHeap.Length >= required) { return; } int newCapacity = Mathf.Max(required, _maintenanceHeap.Length << 1); var newHeap = new MaintenanceNode[newCapacity]; Array.Copy(_maintenanceHeap, 0, newHeap, 0, _maintenanceCount); _maintenanceHeap = newHeap; } private void RemoveMaintenanceAt(int heapIndex) { MaintenanceNode removed = _maintenanceHeap[heapIndex]; RuntimeGameObjectPool removedPool = _pools[removed.poolIndex]; removedPool?.SetMaintenanceHeapIndex(-1); int lastIndex = _maintenanceCount - 1; if (heapIndex != lastIndex) { MaintenanceNode moved = _maintenanceHeap[lastIndex]; _maintenanceHeap[heapIndex] = moved; RuntimeGameObjectPool movedPool = _pools[moved.poolIndex]; movedPool?.SetMaintenanceHeapIndex(heapIndex); } _maintenanceHeap[lastIndex] = default; _maintenanceCount = lastIndex; if (heapIndex < _maintenanceCount) { SiftMaintenanceUp(heapIndex); SiftMaintenanceDown(heapIndex); } } private void SiftMaintenanceUp(int index) { while (index > 0) { int parent = (index - 1) >> 1; if (_maintenanceHeap[parent].dueTime <= _maintenanceHeap[index].dueTime) { break; } SwapMaintenance(parent, index); index = parent; } } private void SiftMaintenanceDown(int index) { while (true) { int left = (index << 1) + 1; if (left >= _maintenanceCount) { return; } int right = left + 1; int smallest = right < _maintenanceCount && _maintenanceHeap[right].dueTime < _maintenanceHeap[left].dueTime ? right : left; if (_maintenanceHeap[index].dueTime <= _maintenanceHeap[smallest].dueTime) { return; } SwapMaintenance(index, smallest); index = smallest; } } private void SwapMaintenance(int left, int right) { MaintenanceNode temp = _maintenanceHeap[left]; _maintenanceHeap[left] = _maintenanceHeap[right]; _maintenanceHeap[right] = temp; _pools[_maintenanceHeap[left].poolIndex]?.SetMaintenanceHeapIndex(left); _pools[_maintenanceHeap[right].poolIndex]?.SetMaintenanceHeapIndex(right); } private void ReleaseDebugSnapshots() { for (int i = 0; i < _debugSnapshots.Count; i++) { MemoryPool.Release(_debugSnapshots[i]); } _debugSnapshots.Clear(); } private static int CompareSnapshot(GameObjectPoolSnapshot left, GameObjectPoolSnapshot right) { if (ReferenceEquals(left, right)) { return 0; } if (left == null) { return 1; } if (right == null) { return -1; } int groupCompare = string.Compare(left.group, right.group, StringComparison.Ordinal); if (groupCompare != 0) { return groupCompare; } return string.Compare(left.assetPath, right.assetPath, StringComparison.Ordinal); } } }