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;
|
2026-03-26 16:14:05 +08:00
|
|
|
[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;
|
2026-03-31 17:25:20 +08:00
|
|
|
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();
|
2026-03-31 17:25:20 +08:00
|
|
|
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
|
|
|
{
|
2026-03-31 17:25:20 +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;
|
2026-03-31 17:25:20 +08:00
|
|
|
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()
|
|
|
|
|
{
|
2026-03-31 17:25:20 +08:00
|
|
|
if (!IsReady || !_cleanupScheduled)
|
2026-03-26 10:49:41 +08:00
|
|
|
{
|
2026-03-31 17:25:20 +08:00
|
|
|
enabled = false;
|
2026-03-26 10:49:41 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 17:25:20 +08:00
|
|
|
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;
|
2026-03-31 17:25:20 +08:00
|
|
|
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);
|
2026-03-31 17:25:20 +08:00
|
|
|
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 16:14:05 +08:00
|
|
|
|
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();
|
2026-03-31 17:25:20 +08:00
|
|
|
_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;
|
2026-03-31 17:25:20 +08:00
|
|
|
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));
|
2026-03-31 17:25:20 +08:00
|
|
|
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);
|
2026-03-31 17:25:20 +08:00
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 17:25:20 +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);
|
2026-03-31 17:25:20 +08:00
|
|
|
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
|
|
|
|
|
{
|
2026-04-17 21:01:20 +08:00
|
|
|
if (pool.NeedsTrim(now) || pool.NeedsPrefabUnload(now, false))
|
2026-03-31 17:25:20 +08:00
|
|
|
{
|
2026-04-17 21:01:20 +08:00
|
|
|
hasWork = true;
|
|
|
|
|
break;
|
2026-03-31 17:25:20 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 21:01:20 +08:00
|
|
|
_nextCleanupTime = hasWork ? nextDueTime : float.MaxValue;
|
|
|
|
|
_cleanupScheduled = hasWork;
|
|
|
|
|
enabled = hasWork;
|
2026-03-31 17:25:20 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnLowMemory()
|
|
|
|
|
{
|
|
|
|
|
if (!IsReady)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 21:01:20 +08:00
|
|
|
_aggressiveCleanupRequested = true;
|
|
|
|
|
PerformCleanup(true);
|
2026-03-31 17:25:20 +08:00
|
|
|
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();
|
|
|
|
|
|
2026-03-26 16:14:05 +08:00
|
|
|
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);
|
2026-03-26 16:14:05 +08:00
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|