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

668 lines
23 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
2026-04-17 21:01:20 +08:00
[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;
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);
2026-04-17 21:01:20 +08:00
private readonly Dictionary<string, ResolvedPoolConfig> _resolvedConfigCache = new Dictionary<string, ResolvedPoolConfig>(StringComparer.Ordinal);
private readonly Dictionary<string, RuntimePrefabPool> _ownersByObject = new Dictionary<string, RuntimePrefabPool>(StringComparer.Ordinal);
2026-03-26 10:49:41 +08:00
private readonly Dictionary<PoolResourceLoaderType, IResourceLoader> _resourceLoaders = new Dictionary<PoolResourceLoaderType, IResourceLoader>();
2026-04-17 21:01:20 +08:00
private readonly List<PoolEntry> _entries = new List<PoolEntry>();
2026-03-26 10:49:41 +08:00
private readonly List<GameObjectPoolSnapshot> _debugSnapshots = new List<GameObjectPoolSnapshot>();
private CancellationTokenSource _shutdownTokenSource;
private UniTask _initializeTask;
private bool _initializationCompleted;
private Exception _initializationException;
private bool _cleanupScheduled;
private float _nextCleanupTime = float.MaxValue;
2026-03-26 13:30:57 +08:00
private bool _isShuttingDown;
2026-04-17 21:01:20 +08:00
private bool _aggressiveCleanupRequested;
2026-03-26 13:30:57 +08:00
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();
enabled = false;
Application.lowMemory += OnLowMemory;
2026-03-26 10:49:41 +08:00
_initializeTask = InitializeAsync(_shutdownTokenSource.Token);
}
2026-03-26 19:55:46 +08:00
protected override void OnDestroyService()
2026-03-26 10:49:41 +08:00
{
Application.lowMemory -= OnLowMemory;
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();
_initializationCompleted = true;
ScheduleCleanup();
2026-03-26 10:49:41 +08:00
}
catch (OperationCanceledException)
{
}
catch (Exception exception)
{
_initializationException = exception;
_initializationCompleted = true;
Log.Error($"GameObjectPool initialization failed: {exception}");
}
}
private void Update()
{
if (!IsReady || !_cleanupScheduled)
2026-03-26 10:49:41 +08:00
{
enabled = false;
2026-03-26 10:49:41 +08:00
return;
}
if (Time.time < _nextCleanupTime)
2026-03-26 10:49:41 +08:00
{
return;
}
2026-04-17 21:01:20 +08:00
PerformCleanup(_aggressiveCleanupRequested);
_aggressiveCleanupRequested = false;
ScheduleCleanup();
2026-03-26 10:49:41 +08:00
}
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;
}
2026-04-17 21:01:20 +08:00
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)
2026-03-26 10:49:41 +08:00
{
EnsureReadyForSyncUse();
2026-04-17 21:01:20 +08:00
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext context = PoolSpawnContext.Create(normalized, group, parent, userData);
return GetGameObjectInternal(normalized, group, context);
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
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)
2026-03-26 10:49:41 +08:00
{
EnsureReadyForSyncUse();
2026-04-17 21:01:20 +08:00
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext context = new PoolSpawnContext(
normalized,
group,
parent,
position,
rotation,
userData,
spawnFrame: (uint)Time.frameCount);
return GetGameObjectInternal(normalized, group, context);
2026-03-26 10:49:41 +08:00
}
public async UniTask<GameObject> GetGameObjectAsync(
string assetPath,
Transform parent = null,
2026-04-17 21:01:20 +08:00
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<GameObject> GetGameObjectAsync(
string assetPath,
PoolSpawnContext context,
2026-03-26 10:49:41 +08:00
CancellationToken cancellationToken = default)
{
await EnsureInitializedAsync(cancellationToken);
2026-04-17 21:01:20 +08:00
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);
2026-03-26 10:49:41 +08:00
}
public async UniTask<GameObject> GetGameObjectAsyncByGroup(
string group,
string assetPath,
Transform parent = null,
2026-04-17 21:01:20 +08:00
object userData = null,
2026-03-26 10:49:41 +08:00
CancellationToken cancellationToken = default)
{
await EnsureInitializedAsync(cancellationToken);
2026-04-17 21:01:20 +08:00
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext context = PoolSpawnContext.Create(normalized, group, parent, userData);
return await GetGameObjectInternalAsync(normalized, group, context, cancellationToken);
}
public async UniTask<GameObject> 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);
2026-03-26 10:49:41 +08:00
}
public void Release(GameObject gameObject)
{
if (gameObject == null)
{
return;
}
2026-04-17 21:01:20 +08:00
string instanceKey = GetInstanceKey(gameObject);
if (_ownersByObject.TryGetValue(instanceKey, out RuntimePrefabPool pool))
2026-03-26 10:49:41 +08:00
{
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);
2026-04-17 21:01:20 +08:00
await PreloadInternalAsync(PoolEntry.NormalizeAssetPath(assetPath), null, count, cancellationToken);
2026-03-26 10:49:41 +08:00
}
public async UniTask PreloadAsyncByGroup(
string group,
string assetPath,
int count = 1,
CancellationToken cancellationToken = default)
{
await EnsureInitializedAsync(cancellationToken);
2026-04-17 21:01:20 +08:00
await PreloadInternalAsync(PoolEntry.NormalizeAssetPath(assetPath), group, count, cancellationToken);
2026-03-26 10:49:41 +08:00
}
public void ForceCleanup()
{
if (!IsReady)
{
return;
}
2026-04-17 21:01:20 +08:00
PerformCleanup(false);
ScheduleCleanup();
2026-03-26 10:49:41 +08:00
}
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-04-17 21:01:20 +08:00
_entries.Clear();
2026-03-26 10:49:41 +08:00
ReleaseDebugSnapshots();
_cleanupScheduled = false;
_nextCleanupTime = float.MaxValue;
enabled = false;
2026-03-26 10:49:41 +08:00
}
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;
}
2026-04-17 21:01:20 +08:00
_ownersByObject[GetInstanceKey(gameObject)] = pool;
NotifyPoolStateChanged();
2026-03-26 10:49:41 +08:00
}
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;
}
2026-04-17 21:01:20 +08:00
_ownersByObject.Remove(GetInstanceKey(gameObject));
NotifyPoolStateChanged();
}
internal void NotifyPoolStateChanged()
{
if (!IsReady)
{
return;
}
ScheduleCleanup();
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
private GameObject GetGameObjectInternal(string assetPath, string group, PoolSpawnContext context)
2026-03-26 10:49:41 +08:00
{
2026-04-17 21:01:20 +08:00
ResolvedPoolConfig config = ResolveConfig(assetPath, group);
2026-03-26 10:49:41 +08:00
if (config == null)
{
2026-04-17 21:01:20 +08:00
return LoadUnpooled(assetPath, group, context.Parent);
2026-03-26 10:49:41 +08:00
}
RuntimePrefabPool pool = GetOrCreatePool(config, assetPath);
2026-04-17 21:01:20 +08:00
return pool.Acquire(context);
2026-03-26 10:49:41 +08:00
}
private async UniTask<GameObject> GetGameObjectInternalAsync(
string assetPath,
string group,
2026-04-17 21:01:20 +08:00
PoolSpawnContext context,
2026-03-26 10:49:41 +08:00
CancellationToken cancellationToken)
{
2026-04-17 21:01:20 +08:00
ResolvedPoolConfig config = ResolveConfig(assetPath, group);
2026-03-26 10:49:41 +08:00
if (config == null)
{
2026-04-17 21:01:20 +08:00
return await LoadUnpooledAsync(assetPath, group, context.Parent, cancellationToken);
2026-03-26 10:49:41 +08:00
}
RuntimePrefabPool pool = GetOrCreatePool(config, assetPath);
2026-04-17 21:01:20 +08:00
return await pool.AcquireAsync(context, cancellationToken);
2026-03-26 10:49:41 +08:00
}
private async UniTask PreloadInternalAsync(
string assetPath,
string group,
int count,
CancellationToken cancellationToken)
{
if (count <= 0)
{
return;
}
2026-04-17 21:01:20 +08:00
ResolvedPoolConfig config = ResolveConfig(assetPath, group);
2026-03-26 10:49:41 +08:00
if (config == null)
{
2026-04-17 21:01:20 +08:00
Log.Warning($"Asset '{assetPath}' has no matching pool rule. Preload skipped.");
2026-03-26 10:49:41 +08:00
return;
}
RuntimePrefabPool pool = GetOrCreatePool(config, assetPath);
await pool.WarmupAsync(count, cancellationToken);
}
2026-04-17 21:01:20 +08:00
private RuntimePrefabPool GetOrCreatePool(ResolvedPoolConfig config, string assetPath)
2026-03-26 10:49:41 +08:00
{
EnsurePoolContainer();
string poolKey = config.BuildResolvedPoolKey(assetPath);
if (_poolsByKey.TryGetValue(poolKey, out RuntimePrefabPool existingPool))
{
return existingPool;
}
var pool = MemoryPool.Acquire<RuntimePrefabPool>();
pool.Initialize(
config,
assetPath,
2026-04-17 21:01:20 +08:00
GetResourceLoader(config.loaderType),
2026-03-26 10:49:41 +08:00
this,
_shutdownTokenSource != null ? _shutdownTokenSource.Token : default);
_poolsByKey.Add(poolKey, pool);
ScheduleCleanup();
2026-03-26 10:49:41 +08:00
return pool;
}
2026-04-17 21:01:20 +08:00
private void PerformCleanup(bool aggressive)
2026-03-26 10:49:41 +08:00
{
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
{
2026-04-17 21:01:20 +08:00
pool.Trim(aggressive ? int.MaxValue : 0, aggressive);
2026-03-26 10:49:41 +08:00
}
}
private void ScheduleCleanup()
{
if (!IsReady)
{
_cleanupScheduled = false;
_nextCleanupTime = float.MaxValue;
enabled = false;
return;
}
float now = Time.time;
2026-04-17 21:01:20 +08:00
bool hasWork = false;
float nextDueTime = now + Mathf.Max(checkInterval, 0.1f);
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
{
2026-04-17 21:01:20 +08:00
if (pool.NeedsTrim(now) || pool.NeedsPrefabUnload(now, false))
{
2026-04-17 21:01:20 +08:00
hasWork = true;
break;
}
}
2026-04-17 21:01:20 +08:00
_nextCleanupTime = hasWork ? nextDueTime : float.MaxValue;
_cleanupScheduled = hasWork;
enabled = hasWork;
}
private void OnLowMemory()
{
if (!IsReady)
{
return;
}
2026-04-17 21:01:20 +08:00
_aggressiveCleanupRequested = true;
PerformCleanup(true);
ScheduleCleanup();
}
2026-03-26 10:49:41 +08:00
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()
{
2026-04-17 21:01:20 +08:00
_entries.Clear();
2026-03-26 10:49:41 +08:00
_resolvedConfigCache.Clear();
IResourceService resourceService = AppServices.Require<IResourceService>();
PoolConfigScriptableObject configAsset = resourceService.LoadAsset<PoolConfigScriptableObject>(poolConfigPath);
2026-03-26 10:49:41 +08:00
2026-04-17 21:01:20 +08:00
if (configAsset == null)
2026-03-26 10:49:41 +08:00
{
return;
}
2026-04-17 21:01:20 +08:00
PoolConfigCatalog catalog = configAsset.BuildCatalog();
_entries.AddRange(catalog.entries);
resourceService.UnloadAsset(configAsset);
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
private ResolvedPoolConfig ResolveConfig(string assetPath, string group)
2026-03-26 10:49:41 +08:00
{
if (string.IsNullOrWhiteSpace(assetPath))
{
return null;
}
2026-04-17 21:01:20 +08:00
string cacheKey = string.IsNullOrWhiteSpace(group) ? assetPath : string.Concat(group, "|", assetPath);
if (_resolvedConfigCache.TryGetValue(cacheKey, out ResolvedPoolConfig cachedConfig))
2026-03-26 10:49:41 +08:00
{
return cachedConfig;
}
2026-04-17 21:01:20 +08:00
for (int i = 0; i < _entries.Count; i++)
2026-03-26 10:49:41 +08:00
{
2026-04-17 21:01:20 +08:00
PoolEntry entry = _entries[i];
if (!entry.Matches(assetPath, group))
2026-03-26 10:49:41 +08:00
{
continue;
}
2026-04-17 21:01:20 +08:00
ResolvedPoolConfig resolved = ResolvedPoolConfig.From(entry);
_resolvedConfigCache[cacheKey] = resolved;
return resolved;
2026-03-26 10:49:41 +08:00
}
2026-04-17 21:01:20 +08:00
_resolvedConfigCache[cacheKey] = null;
return null;
2026-03-26 10:49:41 +08:00
}
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-04-17 21:01:20 +08:00
if (!string.IsNullOrWhiteSpace(group))
2026-03-26 10:49:41 +08:00
{
2026-04-17 21:01:20 +08:00
for (int i = 0; i < _entries.Count; i++)
{
PoolEntry entry = _entries[i];
if (string.Equals(entry.group, group, StringComparison.Ordinal))
{
return GetResourceLoader(entry.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 ReleaseDebugSnapshots()
{
for (int i = 0; i < _debugSnapshots.Count; i++)
{
MemoryPool.Release(_debugSnapshots[i]);
}
_debugSnapshots.Clear();
}
2026-04-17 21:01:20 +08:00
private static string GetInstanceKey(GameObject gameObject)
{
return gameObject.GetInstanceID().ToString();
}
2026-03-26 10:49:41 +08:00
}
}