using System; using System.Collections.Generic; using System.Threading; using AlicizaX.Resource.Runtime; using Cysharp.Threading.Tasks; using UnityEngine; namespace AlicizaX { public sealed class GameObjectPoolManager : MonoServiceBehaviour { private static readonly Comparison SnapshotComparer = (left, right) => { if (left == null && right == null) return 0; if (left == null) return 1; if (right == null) return -1; int groupCompare = string.Compare(left.group, right.group, StringComparison.Ordinal); return groupCompare != 0 ? groupCompare : string.Compare(left.assetPath, right.assetPath, StringComparison.Ordinal); }; [Header("Cleanup Interval")] public float checkInterval = 5f; [Header("Config Path")] public string poolConfigPath = "Assets/Bundles/Configs/PoolConfig"; [Header("Show Detailed Info")] public bool showDetailedInfo = true; [SerializeField] internal Transform poolContainer; private const PoolResourceLoaderType DefaultDirectLoadResourceLoaderType = PoolResourceLoaderType.AssetBundle; private readonly Dictionary _poolsByKey = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _resolvedConfigCache = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _ownersByObject = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _resourceLoaders = new Dictionary(); private readonly List _entries = new List(); private readonly List _debugSnapshots = new List(); private CancellationTokenSource _shutdownTokenSource; private UniTask _initializeTask; private bool _initializationCompleted; private Exception _initializationException; private bool _cleanupScheduled; private float _nextCleanupTime = float.MaxValue; private bool _isShuttingDown; private bool _aggressiveCleanupRequested; public bool IsReady => _initializationCompleted && _initializationException == null; protected override void OnInitialize() { _shutdownTokenSource = new CancellationTokenSource(); EnsureDefaultResourceLoaders(); EnsurePoolContainer(); enabled = false; Application.lowMemory += OnLowMemory; _initializeTask = InitializeAsync(_shutdownTokenSource.Token); } protected override void OnDestroyService() { Application.lowMemory -= OnLowMemory; _shutdownTokenSource?.Cancel(); ClearAllPools(); if (poolContainer != null) { Destroy(poolContainer.gameObject); poolContainer = null; } _shutdownTokenSource?.Dispose(); _shutdownTokenSource = null; } private async UniTask InitializeAsync(CancellationToken cancellationToken) { try { await UniTask.WaitUntil(() => YooAsset.YooAssets.Initialized, cancellationToken: cancellationToken); LoadConfigs(); _initializationCompleted = true; ScheduleCleanup(); } catch (OperationCanceledException) { } catch (Exception exception) { _initializationException = exception; _initializationCompleted = true; Log.Error($"GameObjectPool initialization failed: {exception}"); } } private void Update() { if (!IsReady || !_cleanupScheduled) { enabled = false; return; } if (Time.time < _nextCleanupTime) { return; } PerformCleanup(_aggressiveCleanupRequested); _aggressiveCleanupRequested = false; ScheduleCleanup(); } 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[loaderType] = resourceLoader; } public GameObject GetGameObject(string assetPath, Transform parent = null, object userData = null) { EnsureReadyForSyncUse(); string normalized = PoolEntry.NormalizeAssetPath(assetPath); PoolSpawnContext context = PoolSpawnContext.Create(normalized, null, parent, userData); return GetGameObjectInternal(normalized, null, context); } public GameObject GetGameObject(string assetPath, in PoolSpawnContext context) { EnsureReadyForSyncUse(); string normalized = PoolEntry.NormalizeAssetPath(assetPath); PoolSpawnContext resolvedContext = new PoolSpawnContext( normalized, context.Group, context.Parent, context.Position, context.Rotation, context.UserData, context.OwnerId, context.TeamId, context.SpawnFrame == 0 ? (uint)Time.frameCount : context.SpawnFrame); return GetGameObjectInternal(normalized, context.Group, resolvedContext); } public GameObject GetGameObject( string assetPath, Vector3 position, Quaternion rotation, Transform parent = null, object userData = null) { EnsureReadyForSyncUse(); string normalized = PoolEntry.NormalizeAssetPath(assetPath); PoolSpawnContext context = new PoolSpawnContext( normalized, null, parent, position, rotation, userData, spawnFrame: (uint)Time.frameCount); return GetGameObjectInternal(normalized, null, context); } public GameObject GetGameObjectByGroup(string group, string assetPath, Transform parent = null, object userData = null) { EnsureReadyForSyncUse(); string normalized = PoolEntry.NormalizeAssetPath(assetPath); PoolSpawnContext context = PoolSpawnContext.Create(normalized, group, parent, userData); return GetGameObjectInternal(normalized, group, context); } public GameObject GetGameObjectByGroup(string group, string assetPath, in PoolSpawnContext context) { EnsureReadyForSyncUse(); string normalized = PoolEntry.NormalizeAssetPath(assetPath); PoolSpawnContext resolvedContext = new PoolSpawnContext( normalized, group, context.Parent, context.Position, context.Rotation, context.UserData, context.OwnerId, context.TeamId, context.SpawnFrame == 0 ? (uint)Time.frameCount : context.SpawnFrame); return GetGameObjectInternal(normalized, group, resolvedContext); } public GameObject GetGameObjectByGroup( string group, string assetPath, Vector3 position, Quaternion rotation, Transform parent = null, object userData = null) { EnsureReadyForSyncUse(); string normalized = PoolEntry.NormalizeAssetPath(assetPath); PoolSpawnContext context = new PoolSpawnContext( normalized, group, parent, position, rotation, userData, spawnFrame: (uint)Time.frameCount); return GetGameObjectInternal(normalized, group, context); } public async UniTask GetGameObjectAsync( string assetPath, Transform parent = null, object userData = null, CancellationToken cancellationToken = default) { await EnsureInitializedAsync(cancellationToken); string normalized = PoolEntry.NormalizeAssetPath(assetPath); PoolSpawnContext context = PoolSpawnContext.Create(normalized, null, parent, userData); return await GetGameObjectInternalAsync(normalized, null, context, cancellationToken); } public async UniTask GetGameObjectAsync( string assetPath, PoolSpawnContext context, CancellationToken cancellationToken = default) { await EnsureInitializedAsync(cancellationToken); string normalized = PoolEntry.NormalizeAssetPath(assetPath); PoolSpawnContext resolvedContext = new PoolSpawnContext( normalized, context.Group, context.Parent, context.Position, context.Rotation, context.UserData, context.OwnerId, context.TeamId, context.SpawnFrame == 0 ? (uint)Time.frameCount : context.SpawnFrame); return await GetGameObjectInternalAsync(normalized, context.Group, resolvedContext, cancellationToken); } public async UniTask GetGameObjectAsyncByGroup( string group, string assetPath, Transform parent = null, object userData = null, CancellationToken cancellationToken = default) { await EnsureInitializedAsync(cancellationToken); string normalized = PoolEntry.NormalizeAssetPath(assetPath); PoolSpawnContext context = PoolSpawnContext.Create(normalized, group, parent, userData); return await GetGameObjectInternalAsync(normalized, group, context, cancellationToken); } public async UniTask GetGameObjectAsyncByGroup( string group, string assetPath, PoolSpawnContext context, CancellationToken cancellationToken = default) { await EnsureInitializedAsync(cancellationToken); string normalized = PoolEntry.NormalizeAssetPath(assetPath); PoolSpawnContext resolvedContext = new PoolSpawnContext( normalized, group, context.Parent, context.Position, context.Rotation, context.UserData, context.OwnerId, context.TeamId, context.SpawnFrame == 0 ? (uint)Time.frameCount : context.SpawnFrame); return await GetGameObjectInternalAsync(normalized, group, resolvedContext, cancellationToken); } public void Release(GameObject gameObject) { if (gameObject == null) { return; } string instanceKey = GetInstanceKey(gameObject); if (_ownersByObject.TryGetValue(instanceKey, out RuntimePrefabPool pool)) { pool.Release(gameObject); return; } Log.Warning($"Trying to release untracked GameObject '{gameObject.name}'. Destroying it."); Destroy(gameObject); } public async UniTask PreloadAsync(string assetPath, int count = 1, CancellationToken cancellationToken = default) { await EnsureInitializedAsync(cancellationToken); await PreloadInternalAsync(PoolEntry.NormalizeAssetPath(assetPath), null, count, cancellationToken); } public async UniTask PreloadAsyncByGroup( string group, string assetPath, int count = 1, CancellationToken cancellationToken = default) { await EnsureInitializedAsync(cancellationToken); await PreloadInternalAsync(PoolEntry.NormalizeAssetPath(assetPath), group, count, cancellationToken); } public void ForceCleanup() { if (!IsReady) { return; } PerformCleanup(false); ScheduleCleanup(); } public void ClearAllPools() { _isShuttingDown = true; foreach (RuntimePrefabPool pool in _poolsByKey.Values) { pool.Shutdown(); MemoryPool.Release(pool); } _isShuttingDown = false; _poolsByKey.Clear(); _ownersByObject.Clear(); _resolvedConfigCache.Clear(); _entries.Clear(); ReleaseDebugSnapshots(); _cleanupScheduled = false; _nextCleanupTime = float.MaxValue; enabled = false; } public List GetDebugSnapshots() { ReleaseDebugSnapshots(); foreach (RuntimePrefabPool pool in _poolsByKey.Values) { _debugSnapshots.Add(pool.CreateSnapshot()); } _debugSnapshots.Sort(SnapshotComparer); return _debugSnapshots; } internal void RegisterOwnedObject(GameObject gameObject, RuntimePrefabPool pool) { if (gameObject == null || pool == null) { return; } _ownersByObject[GetInstanceKey(gameObject)] = pool; NotifyPoolStateChanged(); } internal void UnregisterOwnedObject(GameObject gameObject) { if (gameObject == null || _isShuttingDown) { return; } _ownersByObject.Remove(GetInstanceKey(gameObject)); NotifyPoolStateChanged(); } internal void NotifyPoolStateChanged() { if (!IsReady) { return; } ScheduleCleanup(); } private GameObject GetGameObjectInternal(string assetPath, string group, PoolSpawnContext context) { ResolvedPoolConfig config = ResolveConfig(assetPath, group); if (config == null) { return LoadUnpooled(assetPath, group, context.Parent); } RuntimePrefabPool pool = GetOrCreatePool(config, assetPath); return pool.Acquire(context); } private async UniTask GetGameObjectInternalAsync( string assetPath, string group, PoolSpawnContext context, CancellationToken cancellationToken) { ResolvedPoolConfig config = ResolveConfig(assetPath, group); if (config == null) { return await LoadUnpooledAsync(assetPath, group, context.Parent, cancellationToken); } RuntimePrefabPool pool = GetOrCreatePool(config, assetPath); return await pool.AcquireAsync(context, cancellationToken); } private async UniTask PreloadInternalAsync( string assetPath, string group, int count, CancellationToken cancellationToken) { if (count <= 0) { return; } ResolvedPoolConfig config = ResolveConfig(assetPath, group); if (config == null) { Log.Warning($"Asset '{assetPath}' has no matching pool rule. Preload skipped."); return; } RuntimePrefabPool pool = GetOrCreatePool(config, assetPath); await pool.WarmupAsync(count, cancellationToken); } private RuntimePrefabPool GetOrCreatePool(ResolvedPoolConfig config, string assetPath) { EnsurePoolContainer(); string poolKey = config.BuildResolvedPoolKey(assetPath); if (_poolsByKey.TryGetValue(poolKey, out RuntimePrefabPool existingPool)) { return existingPool; } var pool = MemoryPool.Acquire(); pool.Initialize( config, assetPath, GetResourceLoader(config.loaderType), this, _shutdownTokenSource != null ? _shutdownTokenSource.Token : default); _poolsByKey.Add(poolKey, pool); ScheduleCleanup(); return pool; } private void PerformCleanup(bool aggressive) { foreach (RuntimePrefabPool pool in _poolsByKey.Values) { pool.Trim(aggressive ? int.MaxValue : 0, aggressive); } } private void ScheduleCleanup() { if (!IsReady) { _cleanupScheduled = false; _nextCleanupTime = float.MaxValue; enabled = false; return; } float now = Time.time; bool hasWork = false; float nextDueTime = now + Mathf.Max(checkInterval, 0.1f); foreach (RuntimePrefabPool pool in _poolsByKey.Values) { if (pool.NeedsTrim(now) || pool.NeedsPrefabUnload(now, false)) { hasWork = true; break; } } _nextCleanupTime = hasWork ? nextDueTime : float.MaxValue; _cleanupScheduled = hasWork; enabled = hasWork; } private void OnLowMemory() { if (!IsReady) { return; } _aggressiveCleanupRequested = true; PerformCleanup(true); ScheduleCleanup(); } private void EnsureDefaultResourceLoaders() { if (!_resourceLoaders.ContainsKey(PoolResourceLoaderType.AssetBundle)) { _resourceLoaders[PoolResourceLoaderType.AssetBundle] = new AssetBundleResourceLoader(); } if (!_resourceLoaders.ContainsKey(PoolResourceLoaderType.Resources)) { _resourceLoaders[PoolResourceLoaderType.Resources] = new UnityResourcesLoader(); } } private void EnsurePoolContainer() { if (poolContainer != null) { return; } var container = new GameObject("GameObjectPoolContainer"); poolContainer = container.transform; poolContainer.SetParent(transform, false); } private void LoadConfigs() { _entries.Clear(); _resolvedConfigCache.Clear(); IResourceService resourceService = AppServices.Require(); PoolConfigScriptableObject configAsset = resourceService.LoadAsset(poolConfigPath); if (configAsset == null) { return; } PoolConfigCatalog catalog = configAsset.BuildCatalog(); _entries.AddRange(catalog.entries); resourceService.UnloadAsset(configAsset); } private ResolvedPoolConfig ResolveConfig(string assetPath, string group) { if (string.IsNullOrWhiteSpace(assetPath)) { return null; } string cacheKey = string.IsNullOrWhiteSpace(group) ? assetPath : string.Concat(group, "|", assetPath); if (_resolvedConfigCache.TryGetValue(cacheKey, out ResolvedPoolConfig cachedConfig)) { return cachedConfig; } for (int i = 0; i < _entries.Count; i++) { PoolEntry entry = _entries[i]; if (!entry.Matches(assetPath, group)) { continue; } ResolvedPoolConfig resolved = ResolvedPoolConfig.From(entry); _resolvedConfigCache[cacheKey] = resolved; return resolved; } _resolvedConfigCache[cacheKey] = null; return null; } private GameObject LoadUnpooled(string assetPath, string group, Transform parent) { IResourceLoader loader = GetDirectLoadResourceLoader(group); return loader.LoadGameObject(assetPath, parent); } private async UniTask LoadUnpooledAsync( string assetPath, string group, Transform parent, CancellationToken cancellationToken) { IResourceLoader loader = GetDirectLoadResourceLoader(group); return await loader.LoadGameObjectAsync(assetPath, parent, cancellationToken); } private IResourceLoader GetDirectLoadResourceLoader(string group) { if (!string.IsNullOrWhiteSpace(group)) { for (int i = 0; i < _entries.Count; i++) { PoolEntry entry = _entries[i]; if (string.Equals(entry.group, group, StringComparison.Ordinal)) { return GetResourceLoader(entry.loaderType); } } } return GetResourceLoader(DefaultDirectLoadResourceLoaderType); } private IResourceLoader GetResourceLoader(PoolResourceLoaderType loaderType) { if (_resourceLoaders.TryGetValue(loaderType, out IResourceLoader loader) && loader != null) { return loader; } throw new InvalidOperationException($"Resource loader not registered: {loaderType}"); } private void EnsureReadyForSyncUse() { if (!_initializationCompleted) { throw new InvalidOperationException( "GameObjectPool is still initializing. Use the async APIs or wait until initialization completes."); } if (_initializationException != null) { throw new InvalidOperationException("GameObjectPool initialization failed.", _initializationException); } } private async UniTask EnsureInitializedAsync(CancellationToken cancellationToken) { await _initializeTask.AttachExternalCancellation(cancellationToken); if (_initializationException != null) { throw new InvalidOperationException("GameObjectPool initialization failed.", _initializationException); } } private void ReleaseDebugSnapshots() { for (int i = 0; i < _debugSnapshots.Count; i++) { MemoryPool.Release(_debugSnapshots[i]); } _debugSnapshots.Clear(); } private static string GetInstanceKey(GameObject gameObject) { return gameObject.GetInstanceID().ToString(); } } }