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("检查间隔")] public float checkInterval = 10f; [Header("配置路径")] public string poolConfigPath = "Assets/Bundles/Configs/PoolConfig"; [Header("Inspector显示设置")] 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 _groupLoaderCache = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _ownersByObject = new Dictionary(); private readonly Dictionary _resourceLoaders = new Dictionary(); private readonly List _configs = new List(); private readonly List _debugSnapshots = new List(); private CancellationTokenSource _shutdownTokenSource; private UniTask _initializeTask; private bool _initializationCompleted; private Exception _initializationException; private float _lastCleanupTime; private bool _isShuttingDown; public bool IsReady => _initializationCompleted && _initializationException == null; protected override void OnServiceInitialize() { _shutdownTokenSource = new CancellationTokenSource(); EnsureDefaultResourceLoaders(); EnsurePoolContainer(); _initializeTask = InitializeAsync(_shutdownTokenSource.Token); } protected override void OnServiceDestroy() { _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(); _lastCleanupTime = Time.time; _initializationCompleted = true; await PrewarmConfiguredPoolsAsync(cancellationToken); } catch (OperationCanceledException) { } catch (Exception exception) { _initializationException = exception; _initializationCompleted = true; Log.Error($"GameObjectPool initialization failed: {exception}"); } } private void Update() { if (!IsReady) { return; } if (Time.time - _lastCleanupTime < checkInterval) { return; } PerformCleanup(); _lastCleanupTime = Time.time; } 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) { EnsureReadyForSyncUse(); return GetGameObjectInternal(PoolConfig.NormalizeAssetPath(assetPath), null, parent); } public GameObject GetGameObjectByGroup(string group, string assetPath, Transform parent = null) { EnsureReadyForSyncUse(); return GetGameObjectInternal(PoolConfig.NormalizeAssetPath(assetPath), group, parent); } public async UniTask GetGameObjectAsync( string assetPath, Transform parent = null, CancellationToken cancellationToken = default) { await EnsureInitializedAsync(cancellationToken); return await GetGameObjectInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), null, parent, cancellationToken); } public async UniTask GetGameObjectAsyncByGroup( string group, string assetPath, Transform parent = null, CancellationToken cancellationToken = default) { await EnsureInitializedAsync(cancellationToken); return await GetGameObjectInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), group, parent, cancellationToken); } public void Release(GameObject gameObject) { if (gameObject == null) { return; } if (_ownersByObject.TryGetValue(gameObject, out RuntimePrefabPool pool)) { pool.Release(gameObject); return; } Log.Warning($"Trying to release untracked GameObject '{gameObject.name}'. Destroying it."); Destroy(gameObject); } public void Preload(string assetPath, int count = 1) { EnsureReadyForSyncUse(); PreloadInternal(PoolConfig.NormalizeAssetPath(assetPath), null, count); } public void PreloadByGroup(string group, string assetPath, int count = 1) { EnsureReadyForSyncUse(); PreloadInternal(PoolConfig.NormalizeAssetPath(assetPath), group, count); } public async UniTask PreloadAsync(string assetPath, int count = 1, CancellationToken cancellationToken = default) { await EnsureInitializedAsync(cancellationToken); await PreloadInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), null, count, cancellationToken); } public async UniTask PreloadAsyncByGroup( string group, string assetPath, int count = 1, CancellationToken cancellationToken = default) { await EnsureInitializedAsync(cancellationToken); await PreloadInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), group, count, cancellationToken); } public void ForceCleanup() { if (!IsReady) { return; } PerformCleanup(); _lastCleanupTime = Time.time; } public void ClearAllPools() { _isShuttingDown = true; foreach (RuntimePrefabPool pool in _poolsByKey.Values) { pool.Shutdown(); MemoryPool.Release(pool); } _isShuttingDown = false; _poolsByKey.Clear(); _ownersByObject.Clear(); _resolvedConfigCache.Clear(); _groupLoaderCache.Clear(); ReleaseDebugSnapshots(); } 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[gameObject] = pool; } internal void UnregisterOwnedObject(GameObject gameObject) { if (gameObject == null || _isShuttingDown) { return; } _ownersByObject.Remove(gameObject); } private GameObject GetGameObjectInternal(string assetPath, string group, Transform parent) { PoolConfig config = ResolveConfig(assetPath, group); if (config == null) { return LoadUnpooled(assetPath, group, parent); } RuntimePrefabPool pool = GetOrCreatePool(config, assetPath); return pool.Acquire(parent); } private async UniTask GetGameObjectInternalAsync( string assetPath, string group, Transform parent, CancellationToken cancellationToken) { PoolConfig config = ResolveConfig(assetPath, group); if (config == null) { return await LoadUnpooledAsync(assetPath, group, parent, cancellationToken); } RuntimePrefabPool pool = GetOrCreatePool(config, assetPath); return await pool.AcquireAsync(parent, cancellationToken); } private void PreloadInternal(string assetPath, string group, int count) { if (count <= 0) { return; } PoolConfig config = ResolveConfig(assetPath, group); if (config == null) { Log.Warning($"Asset '{assetPath}' has no matching pool config. Preload skipped."); return; } RuntimePrefabPool pool = GetOrCreatePool(config, assetPath); pool.Warmup(count); } private async UniTask PreloadInternalAsync( string assetPath, string group, int count, CancellationToken cancellationToken) { if (count <= 0) { return; } PoolConfig config = ResolveConfig(assetPath, group); if (config == null) { Log.Warning($"Asset '{assetPath}' has no matching pool config. Preload skipped."); return; } RuntimePrefabPool pool = GetOrCreatePool(config, assetPath); await pool.WarmupAsync(count, cancellationToken); } private RuntimePrefabPool GetOrCreatePool(PoolConfig 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.resourceLoaderType), this, _shutdownTokenSource != null ? _shutdownTokenSource.Token : default); _poolsByKey.Add(poolKey, pool); return pool; } private void PerformCleanup() { float now = Time.time; foreach (RuntimePrefabPool pool in _poolsByKey.Values) { pool.TrimExpiredInstances(now); } } 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() { _configs.Clear(); _resolvedConfigCache.Clear(); _groupLoaderCache.Clear(); IResourceService resourceService = AppServices.Require(); PoolConfigScriptableObject configAsset = resourceService.LoadAsset(poolConfigPath); if (configAsset == null || configAsset.configs == null) { return; } for (int i = 0; i < configAsset.configs.Count; i++) { PoolConfig config = configAsset.configs[i]; if (config == null) { continue; } config.Normalize(); if (string.IsNullOrWhiteSpace(config.assetPath)) { Log.Warning($"PoolConfig at index {i} has an empty asset path and was ignored."); continue; } _configs.Add(config); } _configs.Sort(PoolConfig.CompareByPriority); LogConfigWarnings(); for (int i = 0; i < _configs.Count; i++) { PoolConfig config = _configs[i]; if (!_groupLoaderCache.ContainsKey(config.group)) { _groupLoaderCache[config.group] = config.resourceLoaderType; } } resourceService.UnloadAsset(configAsset); } private async UniTask PrewarmConfiguredPoolsAsync(CancellationToken cancellationToken) { for (int i = 0; i < _configs.Count; i++) { PoolConfig config = _configs[i]; if (!config.preloadOnInitialize || config.prewarmCount <= 0) { continue; } if (config.matchMode != PoolMatchMode.Exact) { Log.Warning( $"PoolConfig '{config.assetPath}' uses Prefix mode and preloadOnInitialize. Prefix rules cannot infer a concrete asset to prewarm."); continue; } RuntimePrefabPool pool = GetOrCreatePool(config, config.assetPath); await pool.WarmupAsync(config.prewarmCount, cancellationToken); } } private PoolConfig ResolveConfig(string assetPath, string group) { if (string.IsNullOrWhiteSpace(assetPath)) { return null; } bool hasGroup = !string.IsNullOrEmpty(group); string cacheKey = hasGroup ? string.Concat(group, "|", assetPath) : assetPath; if (_resolvedConfigCache.TryGetValue(cacheKey, out PoolConfig cachedConfig)) { return cachedConfig; } PoolConfig matchedConfig = null; for (int i = 0; i < _configs.Count; i++) { PoolConfig candidate = _configs[i]; if (!candidate.Matches(assetPath, group)) { continue; } if (matchedConfig == null) { matchedConfig = candidate; continue; } bool samePriority = matchedConfig.matchMode == candidate.matchMode && matchedConfig.assetPath.Length == candidate.assetPath.Length; if (samePriority) { Log.Warning( $"Asset '{assetPath}' matched multiple pool configs with the same priority. Using '{matchedConfig.group}:{matchedConfig.assetPath}'."); } break; } _resolvedConfigCache[cacheKey] = matchedConfig; return matchedConfig; } 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) && _groupLoaderCache.TryGetValue(group, out PoolResourceLoaderType loaderType)) { return GetResourceLoader(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 LogConfigWarnings() { var seen = new HashSet<(string group, string assetPath)>(); for (int i = 0; i < _configs.Count; i++) { PoolConfig config = _configs[i]; var key = (config.group, config.assetPath); if (!seen.Add(key)) { Log.Warning($"Duplicate pool config detected: '{config.group}:{config.assetPath}'."); } } } private void ReleaseDebugSnapshots() { for (int i = 0; i < _debugSnapshots.Count; i++) { MemoryPool.Release(_debugSnapshots[i]); } _debugSnapshots.Clear(); } } }