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

588 lines
20 KiB
C#
Raw Normal View History

2026-03-26 10:49:41 +08:00
using System;
using System.Collections.Generic;
using System.Threading;
using AlicizaX.Resource.Runtime;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace AlicizaX
{
2026-03-26 19:50:59 +08:00
public sealed class GameObjectPoolManager : MonoServiceBehaviour<AppScope>
2026-03-26 10:49:41 +08:00
{
2026-03-26 13:30:57 +08:00
private static readonly Comparison<GameObjectPoolSnapshot> 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);
};
2026-03-26 10:49:41 +08:00
[Header("检查间隔")] public float checkInterval = 10f;
2026-03-26 10:49:41 +08:00
[Header("配置路径")] public string poolConfigPath = "Assets/Bundles/Configs/PoolConfig";
2026-03-26 10:49:41 +08:00
[Header("Inspector显示设置")] public bool showDetailedInfo = true;
[SerializeField] internal Transform poolContainer;
2026-03-26 10:49:41 +08:00
private const PoolResourceLoaderType DefaultDirectLoadResourceLoaderType = PoolResourceLoaderType.AssetBundle;
private readonly Dictionary<string, RuntimePrefabPool> _poolsByKey = new Dictionary<string, RuntimePrefabPool>(StringComparer.Ordinal);
private readonly Dictionary<string, PoolConfig> _resolvedConfigCache = new Dictionary<string, PoolConfig>(StringComparer.Ordinal);
2026-03-26 13:30:57 +08:00
private readonly Dictionary<string, PoolResourceLoaderType> _groupLoaderCache = new Dictionary<string, PoolResourceLoaderType>(StringComparer.Ordinal);
2026-03-26 10:49:41 +08:00
private readonly Dictionary<GameObject, RuntimePrefabPool> _ownersByObject = new Dictionary<GameObject, RuntimePrefabPool>();
private readonly Dictionary<PoolResourceLoaderType, IResourceLoader> _resourceLoaders = new Dictionary<PoolResourceLoaderType, IResourceLoader>();
private readonly List<PoolConfig> _configs = new List<PoolConfig>();
private readonly List<GameObjectPoolSnapshot> _debugSnapshots = new List<GameObjectPoolSnapshot>();
private CancellationTokenSource _shutdownTokenSource;
private UniTask _initializeTask;
private bool _initializationCompleted;
private Exception _initializationException;
private float _lastCleanupTime;
2026-03-26 13:30:57 +08:00
private bool _isShuttingDown;
2026-03-26 10:49:41 +08:00
public bool IsReady => _initializationCompleted && _initializationException == null;
2026-03-26 19:55:46 +08:00
protected override void OnInitialize()
2026-03-26 10:49:41 +08:00
{
_shutdownTokenSource = new CancellationTokenSource();
EnsureDefaultResourceLoaders();
EnsurePoolContainer();
_initializeTask = InitializeAsync(_shutdownTokenSource.Token);
}
2026-03-26 19:55:46 +08:00
protected override void OnDestroyService()
2026-03-26 10:49:41 +08:00
{
_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<GameObject> GetGameObjectAsync(
string assetPath,
Transform parent = null,
CancellationToken cancellationToken = default)
{
await EnsureInitializedAsync(cancellationToken);
return await GetGameObjectInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), null, parent, cancellationToken);
}
public async UniTask<GameObject> 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()
{
2026-03-26 13:30:57 +08:00
_isShuttingDown = true;
2026-03-26 10:49:41 +08:00
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
{
pool.Shutdown();
MemoryPool.Release(pool);
}
2026-03-26 13:30:57 +08:00
_isShuttingDown = false;
2026-03-26 10:49:41 +08:00
_poolsByKey.Clear();
_ownersByObject.Clear();
_resolvedConfigCache.Clear();
2026-03-26 13:30:57 +08:00
_groupLoaderCache.Clear();
2026-03-26 10:49:41 +08:00
ReleaseDebugSnapshots();
}
public List<GameObjectPoolSnapshot> GetDebugSnapshots()
{
ReleaseDebugSnapshots();
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
{
_debugSnapshots.Add(pool.CreateSnapshot());
}
2026-03-26 13:30:57 +08:00
_debugSnapshots.Sort(SnapshotComparer);
2026-03-26 10:49:41 +08:00
return _debugSnapshots;
}
internal void RegisterOwnedObject(GameObject gameObject, RuntimePrefabPool pool)
{
if (gameObject == null || pool == null)
{
return;
}
_ownersByObject[gameObject] = pool;
}
internal void UnregisterOwnedObject(GameObject gameObject)
{
2026-03-26 13:30:57 +08:00
if (gameObject == null || _isShuttingDown)
2026-03-26 10:49:41 +08:00
{
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<GameObject> 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<RuntimePrefabPool>();
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))
{
2026-03-26 13:30:57 +08:00
_resourceLoaders[PoolResourceLoaderType.AssetBundle] = new AssetBundleResourceLoader();
2026-03-26 10:49:41 +08:00
}
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();
2026-03-26 13:30:57 +08:00
_groupLoaderCache.Clear();
2026-03-26 10:49:41 +08:00
IResourceService resourceService = AppServices.Require<IResourceService>();
PoolConfigScriptableObject configAsset = resourceService.LoadAsset<PoolConfigScriptableObject>(poolConfigPath);
2026-03-26 10:49:41 +08:00
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();
2026-03-26 13:30:57 +08:00
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);
2026-03-26 10:49:41 +08:00
}
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;
}
2026-03-26 13:30:57 +08:00
bool hasGroup = !string.IsNullOrEmpty(group);
string cacheKey = hasGroup ? string.Concat(group, "|", assetPath) : assetPath;
2026-03-26 10:49:41 +08:00
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<GameObject> 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)
{
2026-03-26 13:30:57 +08:00
if (!string.IsNullOrWhiteSpace(group) &&
_groupLoaderCache.TryGetValue(group, out PoolResourceLoaderType loaderType))
2026-03-26 10:49:41 +08:00
{
2026-03-26 13:30:57 +08:00
return GetResourceLoader(loaderType);
2026-03-26 10:49:41 +08:00
}
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()
{
2026-03-26 13:30:57 +08:00
var seen = new HashSet<(string group, string assetPath)>();
2026-03-26 10:49:41 +08:00
for (int i = 0; i < _configs.Count; i++)
{
2026-03-26 13:30:57 +08:00
PoolConfig config = _configs[i];
var key = (config.group, config.assetPath);
if (!seen.Add(key))
2026-03-26 10:49:41 +08:00
{
2026-03-26 13:30:57 +08:00
Log.Warning($"Duplicate pool config detected: '{config.group}:{config.assetPath}'.");
2026-03-26 10:49:41 +08:00
}
}
}
private void ReleaseDebugSnapshots()
{
for (int i = 0; i < _debugSnapshots.Count; i++)
{
MemoryPool.Release(_debugSnapshots[i]);
}
_debugSnapshots.Clear();
}
}
}