opt gameobjectpool

This commit is contained in:
陈思海 2026-03-26 10:49:41 +08:00
parent c6fe229b4d
commit 9756674342
52 changed files with 2847 additions and 1707 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6571694cccb20a64fb609396b9aada2b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,126 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace AlicizaX
{
[CustomEditor(typeof(GameObjectPool))]
public sealed class GameObjectPoolEditor : UnityEditor.Editor
{
private readonly Dictionary<string, bool> _foldoutState = new Dictionary<string, bool>();
public override void OnInspectorGUI()
{
serializedObject.Update();
DrawDefaultInspector();
serializedObject.ApplyModifiedProperties();
var pool = (GameObjectPool)target;
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox("Enter Play Mode to inspect runtime pool state.", MessageType.Info);
return;
}
if (!pool.showDetailedInfo)
{
return;
}
EditorGUILayout.Space();
DrawRuntimeState(pool);
}
public override bool RequiresConstantRepaint()
{
var pool = target as GameObjectPool;
return pool != null && Application.isPlaying && pool.showDetailedInfo;
}
private void DrawRuntimeState(GameObjectPool pool)
{
if (!pool.IsReady)
{
EditorGUILayout.HelpBox("GameObjectPool is initializing.", MessageType.Info);
return;
}
List<GameObjectPoolSnapshot> snapshots = pool.GetDebugSnapshots();
if (snapshots.Count == 0)
{
EditorGUILayout.HelpBox("No runtime pools have been created yet.", MessageType.Info);
return;
}
for (int i = 0; i < snapshots.Count; i++)
{
DrawSnapshot(snapshots[i]);
}
}
private void DrawSnapshot(GameObjectPoolSnapshot snapshot)
{
if (snapshot == null)
{
return;
}
string key = $"{snapshot.group}|{snapshot.assetPath}";
if (!_foldoutState.ContainsKey(key))
{
_foldoutState[key] = false;
}
EditorGUILayout.BeginVertical("box");
_foldoutState[key] = EditorGUILayout.Foldout(
_foldoutState[key],
$"{snapshot.group} | {snapshot.assetPath} ({snapshot.activeCount}/{snapshot.totalCount})",
true);
if (_foldoutState[key])
{
EditorGUILayout.LabelField("Match Mode", snapshot.matchMode.ToString());
EditorGUILayout.LabelField("Loader", snapshot.loaderType.ToString());
EditorGUILayout.LabelField("Capacity", snapshot.capacity.ToString());
EditorGUILayout.LabelField("Inactive", snapshot.inactiveCount.ToString());
EditorGUILayout.LabelField("Idle Timeout", $"{snapshot.instanceIdleTimeout:F1}s");
EditorGUILayout.LabelField("Prefab Unload Delay", $"{snapshot.prefabUnloadDelay:F1}s");
EditorGUILayout.LabelField("Prefab Loaded", snapshot.prefabLoaded ? "Yes" : "No");
if (snapshot.instances.Count > 0)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField("Instances", EditorStyles.boldLabel);
for (int i = 0; i < snapshot.instances.Count; i++)
{
DrawInstance(snapshot.instances[i]);
}
}
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
}
private static void DrawInstance(GameObjectPoolInstanceSnapshot instance)
{
if (instance == null)
{
return;
}
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField(instance.instanceName, EditorStyles.boldLabel);
EditorGUILayout.LabelField("State", instance.isActive ? "Active" : "Inactive");
if (!instance.isActive)
{
EditorGUILayout.LabelField("Idle", $"{instance.idleDuration:F1}s");
}
EditorGUILayout.EndVertical();
EditorGUILayout.ObjectField(instance.gameObject, typeof(GameObject), true, GUILayout.Width(120));
EditorGUILayout.EndHorizontal();
}
}
}

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: ce0e8ead006ba324eaf2410a3dd556a5
guid: 43ecbf81dd8d96d418b2d61e28e453cd
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@ -0,0 +1,180 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace AlicizaX
{
[CustomEditor(typeof(PoolConfigScriptableObject))]
public sealed class PoolConfigScriptableObjectEditor : UnityEditor.Editor
{
private const float VerticalSpacing = 4f;
private ReorderableList _configList;
private SerializedProperty _configsProperty;
private void OnEnable()
{
_configsProperty = serializedObject.FindProperty("configs");
_configList = new ReorderableList(serializedObject, _configsProperty, true, true, true, true)
{
drawHeaderCallback = rect => EditorGUI.LabelField(rect, "Pool Configs"),
drawElementCallback = DrawElement,
elementHeightCallback = GetElementHeight
};
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.HelpBox(
"每条配置定义一条匹配规则;真正的池按具体 assetPath 实例化,不再共享一个目录级总容量。",
MessageType.Info);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Normalize"))
{
serializedObject.ApplyModifiedProperties();
NormalizeAndSort(shouldSort: false);
serializedObject.Update();
}
if (GUILayout.Button("Normalize And Sort"))
{
serializedObject.ApplyModifiedProperties();
NormalizeAndSort(shouldSort: true);
serializedObject.Update();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
_configList.DoLayoutList();
serializedObject.ApplyModifiedProperties();
DrawValidation();
}
private void DrawElement(Rect rect, int index, bool isActive, bool isFocused)
{
SerializedProperty element = _configsProperty.GetArrayElementAtIndex(index);
rect.y += 2f;
float lineHeight = EditorGUIUtility.singleLineHeight;
float wideFieldWidth = rect.width * 0.6f;
float narrowFieldWidth = rect.width - wideFieldWidth - 6f;
SerializedProperty group = element.FindPropertyRelative("group");
SerializedProperty assetPath = element.FindPropertyRelative("assetPath");
SerializedProperty matchMode = element.FindPropertyRelative("matchMode");
SerializedProperty loaderType = element.FindPropertyRelative("resourceLoaderType");
SerializedProperty capacity = element.FindPropertyRelative("capacity");
SerializedProperty prewarmCount = element.FindPropertyRelative("prewarmCount");
SerializedProperty idleTimeout = element.FindPropertyRelative("instanceIdleTimeout");
SerializedProperty unloadDelay = element.FindPropertyRelative("prefabUnloadDelay");
SerializedProperty preloadOnInitialize = element.FindPropertyRelative("preloadOnInitialize");
Rect line1 = new Rect(rect.x, rect.y, rect.width, lineHeight);
Rect line2 = new Rect(rect.x, line1.yMax + VerticalSpacing, rect.width, lineHeight);
Rect line3Left = new Rect(rect.x, line2.yMax + VerticalSpacing, wideFieldWidth, lineHeight);
Rect line3Right = new Rect(line3Left.xMax + 6f, line3Left.y, narrowFieldWidth, lineHeight);
Rect line4Left = new Rect(rect.x, line3Left.yMax + VerticalSpacing, wideFieldWidth, lineHeight);
Rect line4Right = new Rect(line4Left.xMax + 6f, line4Left.y, narrowFieldWidth, lineHeight);
Rect line5Left = new Rect(rect.x, line4Left.yMax + VerticalSpacing, wideFieldWidth, lineHeight);
Rect line5Right = new Rect(line5Left.xMax + 6f, line5Left.y, narrowFieldWidth, lineHeight);
EditorGUI.PropertyField(line1, assetPath);
EditorGUI.PropertyField(line2, group);
EditorGUI.PropertyField(line3Left, matchMode);
EditorGUI.PropertyField(line3Right, loaderType);
EditorGUI.PropertyField(line4Left, capacity);
EditorGUI.PropertyField(line4Right, prewarmCount);
EditorGUI.PropertyField(line5Left, idleTimeout);
EditorGUI.PropertyField(line5Right, unloadDelay);
Rect line6 = new Rect(rect.x, line5Left.yMax + VerticalSpacing, rect.width, lineHeight);
EditorGUI.PropertyField(line6, preloadOnInitialize);
}
private float GetElementHeight(int index)
{
float lineHeight = EditorGUIUtility.singleLineHeight;
return lineHeight * 6f + VerticalSpacing * 7f;
}
private void NormalizeAndSort(bool shouldSort)
{
var asset = (PoolConfigScriptableObject)target;
asset.Normalize();
if (shouldSort)
{
asset.configs.Sort(PoolConfig.CompareByPriority);
}
EditorUtility.SetDirty(asset);
}
private void DrawValidation()
{
var asset = (PoolConfigScriptableObject)target;
if (asset.configs == null || asset.configs.Count == 0)
{
return;
}
List<string> warnings = BuildWarnings(asset.configs);
for (int i = 0; i < warnings.Count; i++)
{
EditorGUILayout.HelpBox(warnings[i], MessageType.Warning);
}
}
private static List<string> BuildWarnings(List<PoolConfig> configs)
{
var warnings = new List<string>();
for (int i = 0; i < configs.Count; i++)
{
PoolConfig config = configs[i];
if (config == null)
{
warnings.Add($"Element {i} is null.");
continue;
}
if (string.IsNullOrWhiteSpace(config.assetPath))
{
warnings.Add($"Element {i} has an empty asset path.");
}
if (config.matchMode == PoolMatchMode.Prefix && config.preloadOnInitialize)
{
warnings.Add($"Element {i} uses Prefix matching and preloadOnInitialize. Prefix rules cannot infer a concrete asset to prewarm.");
}
for (int j = i + 1; j < configs.Count; j++)
{
PoolConfig other = configs[j];
if (other == null)
{
continue;
}
bool duplicate =
string.Equals(config.group, other.group, System.StringComparison.Ordinal) &&
config.matchMode == other.matchMode &&
string.Equals(config.assetPath, other.assetPath, System.StringComparison.Ordinal);
if (duplicate)
{
warnings.Add($"Duplicate rule detected between elements {i} and {j}: {config.group}:{config.assetPath}.");
}
}
}
return warnings;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 21b55582105cd3a44bfc4bfb935a35d1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -355,6 +355,7 @@ MonoBehaviour:
minUnloadUnusedAssetsInterval: 60
maxUnloadUnusedAssetsInterval: 300
useSystemUnloadUnusedAssets: 1
minGCCollectInterval: 30
decryptionServices:
autoUnloadBundleWhenUnused: 0
_playMode: 0

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 420d45a81b5c11e478b3a5ebe958914f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 48e89f2eceb320547b4986db840128da
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,7 @@
namespace AlicizaX
{
/// <summary>
/// 框架内置的 App Scope 标记类,生命周期与 ServiceWorld 相同。
/// </summary>
public sealed class AppScope { }
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d205821b5dae0264fb6c5d1ea78f9b43
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,70 @@
using System;
namespace AlicizaX
{
public static class AppServices
{
private static ServiceWorld _world;
public static bool HasWorld => _world != null;
public static ServiceWorld World
{
get
{
if (_world == null)
throw new InvalidOperationException("ServiceWorld has not been created yet.");
return _world;
}
}
public static ServiceScope App => World.AppScope;
public static ServiceWorld EnsureWorld(int appScopeOrder = -10000)
{
if (_world == null)
_world = new ServiceWorld(appScopeOrder);
return _world;
}
// ── Scope 管理 ──────────────────────────────────────────────────────────
public static ServiceScope CreateScope<TScope>(int order = 0) where TScope : class
=> World.CreateScope<TScope>(order);
public static ServiceScope GetOrCreateScope<TScope>(int order = 0) where TScope : class
=> World.GetOrCreateScope<TScope>(order);
public static bool TryGetScope<TScope>(out ServiceScope scope) where TScope : class
{
if (_world == null) { scope = null; return false; }
return _world.TryGetScope<TScope>(out scope);
}
public static bool DestroyScope<TScope>() where TScope : class
{
if (_world == null) return false;
return _world.DestroyScope<TScope>();
}
// ── Service 查找 ────────────────────────────────────────────────────────
public static bool TryGet<T>(out T service) where T : class, IService
{
if (_world == null) { service = null; return false; }
return _world.TryGet(out service);
}
public static T Require<T>() where T : class, IService
=> World.Require<T>();
// ── 生命周期 ────────────────────────────────────────────────────────────
public static void Shutdown()
{
if (_world == null) return;
_world.Dispose();
_world = null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7186431b5e61b3c4f9865bd9901ce831
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
namespace AlicizaX
{
/// <summary>
/// 标记一个 IService 实现来自 MonoBehaviour。
/// ServiceScope 通过此接口识别 Mono 服务,避免 Core 层依赖 UnityEngine。
/// </summary>
public interface IMonoService : IService { }
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 64c7431bc2d38d24594f92006ac398d9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
namespace AlicizaX
{
public interface IService
{
void Initialize(ServiceContext context);
void Destroy();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1cab09453acbb93498bd781202713656
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,7 @@
namespace AlicizaX
{
public interface IServiceOrder
{
int Order { get; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f66ea6b1b8a144b4184b2b078305f8df
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,22 @@
namespace AlicizaX
{
public interface IServiceTickable
{
void Tick(float deltaTime);
}
public interface IServiceLateTickable
{
void LateTick(float deltaTime);
}
public interface IServiceFixedTickable
{
void FixedTick(float fixedDeltaTime);
}
public interface IServiceGizmoDrawable
{
void DrawGizmos();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a7ff388c5465f4c439bf85bb76c2d786
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace AlicizaX
{
internal sealed class ReferenceComparer<T> : IEqualityComparer<T>
where T : class
{
public static readonly ReferenceComparer<T> Instance = new ReferenceComparer<T>();
private ReferenceComparer() { }
public bool Equals(T x, T y) => ReferenceEquals(x, y);
public int GetHashCode(T obj)
=> System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3afc1438ca0eba4892ac93b1165d60d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,36 @@
namespace AlicizaX
{
public abstract class ServiceBase : IService
{
public ServiceContext Context { get; private set; }
public bool IsInitialized { get; private set; }
protected ServiceWorld World => Context.World;
protected ServiceScope Scope => Context.Scope;
void IService.Initialize(ServiceContext context)
{
if (IsInitialized)
throw new System.InvalidOperationException($"{GetType().FullName} is already initialized.");
Context = context;
IsInitialized = true;
OnInitialize();
}
void IService.Destroy()
{
if (!IsInitialized) return;
OnDestroyService();
IsInitialized = false;
Context = default;
}
protected abstract void OnInitialize();
protected abstract void OnDestroyService();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: af65a59b2e4b11b419fc09505ecd1c88
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,32 @@
namespace AlicizaX
{
public readonly struct ServiceContext
{
public ServiceContext(ServiceWorld world, ServiceScope scope)
{
World = world;
Scope = scope;
}
public ServiceWorld World { get; }
public ServiceScope Scope { get; }
public ServiceScope AppScope => World.AppScope;
public T Require<T>() where T : class, IService
=> World.Require<T>(Scope);
public bool TryGet<T>(out T service) where T : class, IService
=> World.TryGet(Scope, out service);
public ServiceScope CreateScope<TScope>(int order = 0) where TScope : class
=> World.CreateScope<TScope>(order);
public ServiceScope GetOrCreateScope<TScope>(int order = 0) where TScope : class
=> World.GetOrCreateScope<TScope>(order);
public bool TryGetScope<TScope>(out ServiceScope scope) where TScope : class
=> World.TryGetScope<TScope>(out scope);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fd30fae6aee72a04db7507d194215b40
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
namespace AlicizaX
{
internal static class ServiceContractUtility
{
private static readonly HashSet<Type> ExcludedContracts = new HashSet<Type>
{
typeof(IService),
typeof(IServiceTickable),
typeof(IServiceLateTickable),
typeof(IServiceFixedTickable),
typeof(IServiceGizmoDrawable),
typeof(IServiceOrder),
};
public static List<Type> Collect(Type serviceType, IReadOnlyList<Type> extraContracts)
{
var contracts = new List<Type> { serviceType };
var unique = new HashSet<Type> { serviceType };
var interfaces = serviceType.GetInterfaces();
for (var i = 0; i < interfaces.Length; i++)
{
var contract = interfaces[i];
if (!typeof(IService).IsAssignableFrom(contract)) continue;
if (ExcludedContracts.Contains(contract)) continue;
if (unique.Add(contract)) contracts.Add(contract);
}
if (extraContracts == null) return contracts;
for (var i = 0; i < extraContracts.Count; i++)
{
var extraContract = extraContracts[i];
ValidateExtraContract(serviceType, extraContract);
if (unique.Add(extraContract)) contracts.Add(extraContract);
}
return contracts;
}
private static void ValidateExtraContract(Type serviceType, Type contract)
{
if (contract == null)
throw new ArgumentNullException(nameof(contract));
if (!typeof(IService).IsAssignableFrom(contract))
throw new InvalidOperationException($"{contract.FullName} must inherit {nameof(IService)}.");
if (!contract.IsAssignableFrom(serviceType))
throw new InvalidOperationException($"{serviceType.FullName} does not implement {contract.FullName}.");
if (ExcludedContracts.Contains(contract))
throw new InvalidOperationException($"{contract.FullName} cannot be used as a service contract.");
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 95c468fca1e70ab4b931d26d98af3fd9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,260 @@
using System;
using System.Collections.Generic;
namespace AlicizaX
{
public sealed class ServiceScope : IDisposable
{
private readonly Dictionary<Type, IService> _servicesByContract = new Dictionary<Type, IService>();
private readonly Dictionary<IService, List<Type>> _contractsByService = new Dictionary<IService, List<Type>>(ReferenceComparer<IService>.Instance);
private readonly List<IService> _registrationOrder = new List<IService>();
private readonly List<IServiceTickable> _tickables = new List<IServiceTickable>();
private readonly List<IServiceLateTickable> _lateTickables = new List<IServiceLateTickable>();
private readonly List<IServiceFixedTickable> _fixedTickables = new List<IServiceFixedTickable>();
private readonly List<IServiceGizmoDrawable> _gizmoDrawables = new List<IServiceGizmoDrawable>();
private IServiceTickable[] _tickableSnapshot = Array.Empty<IServiceTickable>();
private IServiceLateTickable[] _lateTickableSnapshot = Array.Empty<IServiceLateTickable>();
private IServiceFixedTickable[] _fixedTickableSnapshot = Array.Empty<IServiceFixedTickable>();
private IServiceGizmoDrawable[] _gizmoSnapshot = Array.Empty<IServiceGizmoDrawable>();
private bool _tickablesDirty;
private bool _lateTickablesDirty;
private bool _fixedTickablesDirty;
private bool _gizmoDrawablesDirty;
internal ServiceScope(ServiceWorld world, string name, int order)
{
World = world ?? throw new ArgumentNullException(nameof(world));
Name = string.IsNullOrWhiteSpace(name) ? throw new ArgumentException("Scope name cannot be empty.", nameof(name)) : name;
Order = order;
}
public ServiceWorld World { get; }
public string Name { get; }
public int Order { get; }
public bool IsDisposed { get; private set; }
public T Register<T>(T service, params Type[] extraContracts)
where T : class, IService
{
EnsureNotDisposed();
if (service == null)
throw new ArgumentNullException(nameof(service));
ValidateService(service);
if (_contractsByService.ContainsKey(service))
throw new InvalidOperationException($"Service {service.GetType().FullName} is already registered in scope {Name}.");
var contracts = ServiceContractUtility.Collect(service.GetType(), extraContracts);
for (var i = 0; i < contracts.Count; i++)
{
var contract = contracts[i];
if (_servicesByContract.TryGetValue(contract, out var existing))
{
throw new InvalidOperationException(
$"Scope {Name} already contains contract {contract.FullName} bound to {existing.GetType().FullName}.");
}
}
_contractsByService.Add(service, contracts);
_registrationOrder.Add(service);
for (var i = 0; i < contracts.Count; i++)
_servicesByContract.Add(contracts[i], service);
try
{
service.Initialize(new ServiceContext(World, this));
AddToLifecycleLists(service);
}
catch
{
RemoveBindings(service);
throw;
}
return service;
}
public bool Unregister<T>() where T : class, IService
{
if (!_servicesByContract.TryGetValue(typeof(T), out var service))
return false;
return Unregister(service);
}
public bool Unregister(IService service)
{
if (service == null || !_contractsByService.ContainsKey(service))
return false;
RemoveFromLifecycleLists(service);
RemoveBindings(service);
service.Destroy();
return true;
}
public bool TryGet<T>(out T service) where T : class, IService
{
if (_servicesByContract.TryGetValue(typeof(T), out var raw))
{
service = raw as T;
return service != null;
}
service = null;
return false;
}
public T Require<T>() where T : class, IService
{
if (TryGet(out T service)) return service;
throw new InvalidOperationException($"Scope {Name} does not contain service {typeof(T).FullName}.");
}
public bool HasContract(Type contractType)
=> _servicesByContract.ContainsKey(contractType);
internal void Tick(float deltaTime)
{
var snapshot = GetTickSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].Tick(deltaTime);
}
internal void LateTick(float deltaTime)
{
var snapshot = GetLateTickSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].LateTick(deltaTime);
}
internal void FixedTick(float fixedDeltaTime)
{
var snapshot = GetFixedTickSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].FixedTick(fixedDeltaTime);
}
internal void DrawGizmos()
{
var snapshot = GetGizmoSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].DrawGizmos();
}
public void Dispose()
{
if (IsDisposed) return;
var snapshot = _registrationOrder.ToArray();
for (var i = snapshot.Length - 1; i >= 0; i--)
{
var service = snapshot[i];
if (!_contractsByService.ContainsKey(service)) continue;
RemoveFromLifecycleLists(service);
RemoveBindings(service);
service.Destroy();
}
IsDisposed = true;
}
private void EnsureNotDisposed()
{
if (IsDisposed) throw new ObjectDisposedException(Name);
}
private static void ValidateService(IService service)
{
if (service is IMonoService &&
(service is IServiceTickable ||
service is IServiceLateTickable ||
service is IServiceFixedTickable ||
service is IServiceGizmoDrawable))
{
throw new InvalidOperationException(
$"Mono service {service.GetType().FullName} cannot implement tick lifecycle interfaces.");
}
}
private void AddToLifecycleLists(IService service)
{
if (service is IServiceTickable tickable) { _tickables.Add(tickable); _tickablesDirty = true; }
if (service is IServiceLateTickable late) { _lateTickables.Add(late); _lateTickablesDirty = true; }
if (service is IServiceFixedTickable fixed_) { _fixedTickables.Add(fixed_); _fixedTickablesDirty = true; }
if (service is IServiceGizmoDrawable gizmo) { _gizmoDrawables.Add(gizmo); _gizmoDrawablesDirty = true; }
}
private void RemoveFromLifecycleLists(IService service)
{
if (service is IServiceTickable tickable && _tickables.Remove(tickable)) _tickablesDirty = true;
if (service is IServiceLateTickable late && _lateTickables.Remove(late)) _lateTickablesDirty = true;
if (service is IServiceFixedTickable fixed_ && _fixedTickables.Remove(fixed_)) _fixedTickablesDirty = true;
if (service is IServiceGizmoDrawable gizmo && _gizmoDrawables.Remove(gizmo)) _gizmoDrawablesDirty = true;
}
private void RemoveBindings(IService service)
{
if (_contractsByService.TryGetValue(service, out var contracts))
{
for (var i = 0; i < contracts.Count; i++)
_servicesByContract.Remove(contracts[i]);
}
_contractsByService.Remove(service);
_registrationOrder.Remove(service);
}
private IServiceTickable[] GetTickSnapshot()
{
if (_tickablesDirty)
{
_tickables.Sort(CompareByOrder);
_tickableSnapshot = _tickables.Count > 0 ? _tickables.ToArray() : Array.Empty<IServiceTickable>();
_tickablesDirty = false;
}
return _tickableSnapshot;
}
private IServiceLateTickable[] GetLateTickSnapshot()
{
if (_lateTickablesDirty)
{
_lateTickables.Sort(CompareByOrder);
_lateTickableSnapshot = _lateTickables.Count > 0 ? _lateTickables.ToArray() : Array.Empty<IServiceLateTickable>();
_lateTickablesDirty = false;
}
return _lateTickableSnapshot;
}
private IServiceFixedTickable[] GetFixedTickSnapshot()
{
if (_fixedTickablesDirty)
{
_fixedTickables.Sort(CompareByOrder);
_fixedTickableSnapshot = _fixedTickables.Count > 0 ? _fixedTickables.ToArray() : Array.Empty<IServiceFixedTickable>();
_fixedTickablesDirty = false;
}
return _fixedTickableSnapshot;
}
private IServiceGizmoDrawable[] GetGizmoSnapshot()
{
if (_gizmoDrawablesDirty)
{
_gizmoDrawables.Sort(CompareByOrder);
_gizmoSnapshot = _gizmoDrawables.Count > 0 ? _gizmoDrawables.ToArray() : Array.Empty<IServiceGizmoDrawable>();
_gizmoDrawablesDirty = false;
}
return _gizmoSnapshot;
}
private static int CompareByOrder<T>(T a, T b)
{
var left = a is IServiceOrder oa ? oa.Order : 0;
var right = b is IServiceOrder ob ? ob.Order : 0;
return left.CompareTo(right);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 90a7d784a3aff2f4f832fa67901e6931
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
namespace AlicizaX
{
public sealed class ServiceWorld : IDisposable
{
private readonly List<ServiceScope> _scopes = new List<ServiceScope>();
private readonly Dictionary<Type, ServiceScope> _scopesByType = new Dictionary<Type, ServiceScope>();
private ServiceScope[] _scopeSnapshot = Array.Empty<ServiceScope>();
private bool _scopesDirty;
public ServiceWorld(int appScopeOrder = -10000)
{
AppScope = CreateScopeInternal(typeof(AppScope), appScopeOrder);
}
public ServiceScope AppScope { get; }
// ── Scope 管理Type-based ────────────────────────────────────────────
public ServiceScope CreateScope<TScope>(int order = 0) where TScope : class
{
var type = typeof(TScope);
if (_scopesByType.ContainsKey(type))
throw new InvalidOperationException($"Scope {type.Name} already exists.");
return CreateScopeInternal(type, order);
}
public ServiceScope GetOrCreateScope<TScope>(int order = 0) where TScope : class
{
var type = typeof(TScope);
if (_scopesByType.TryGetValue(type, out var existing))
return existing;
return CreateScopeInternal(type, order);
}
public bool TryGetScope<TScope>(out ServiceScope scope) where TScope : class
=> _scopesByType.TryGetValue(typeof(TScope), out scope);
public ServiceScope GetScope<TScope>() where TScope : class
{
if (TryGetScope<TScope>(out var scope)) return scope;
throw new InvalidOperationException($"Scope {typeof(TScope).Name} does not exist.");
}
public bool DestroyScope<TScope>() where TScope : class
{
if (typeof(TScope) == typeof(AppScope))
throw new InvalidOperationException("AppScope can only be destroyed when the world is disposed.");
var type = typeof(TScope);
if (!_scopesByType.TryGetValue(type, out var scope))
return false;
_scopesByType.Remove(type);
_scopes.Remove(scope);
_scopesDirty = true;
scope.Dispose();
return true;
}
// ── Service 查找 ────────────────────────────────────────────────────────
public bool TryGet<T>(out T service) where T : class, IService
=> TryGet(null, out service);
public bool TryGet<T>(ServiceScope preferredScope, out T service) where T : class, IService
{
if (preferredScope != null && !preferredScope.IsDisposed && preferredScope.TryGet(out service))
return true;
var snapshot = GetScopeSnapshot();
for (var i = snapshot.Length - 1; i >= 0; i--)
{
var scope = snapshot[i];
if (ReferenceEquals(scope, preferredScope)) continue;
if (scope.TryGet(out service)) return true;
}
service = null;
return false;
}
public T Require<T>() where T : class, IService => Require<T>(null);
public T Require<T>(ServiceScope preferredScope) where T : class, IService
{
if (TryGet(preferredScope, out T service)) return service;
throw new InvalidOperationException($"Service {typeof(T).FullName} was not found in any active scope.");
}
// ── Tick ────────────────────────────────────────────────────────────────
public void Tick(float deltaTime)
{
var snapshot = GetScopeSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].Tick(deltaTime);
}
public void LateTick(float deltaTime)
{
var snapshot = GetScopeSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].LateTick(deltaTime);
}
public void FixedTick(float fixedDeltaTime)
{
var snapshot = GetScopeSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].FixedTick(fixedDeltaTime);
}
public void DrawGizmos()
{
var snapshot = GetScopeSnapshot();
for (var i = 0; i < snapshot.Length; i++) snapshot[i].DrawGizmos();
}
// ── Dispose ─────────────────────────────────────────────────────────────
public void Dispose()
{
var snapshot = _scopes.ToArray();
for (var i = snapshot.Length - 1; i >= 0; i--)
snapshot[i].Dispose();
_scopes.Clear();
_scopesByType.Clear();
_scopeSnapshot = Array.Empty<ServiceScope>();
}
// ── 内部 ────────────────────────────────────────────────────────────────
private ServiceScope CreateScopeInternal(Type scopeType, int order)
{
var scope = new ServiceScope(this, scopeType.Name, order);
_scopes.Add(scope);
_scopesByType.Add(scopeType, scope);
_scopes.Sort(CompareScopeOrder);
_scopesDirty = true;
return scope;
}
private ServiceScope[] GetScopeSnapshot()
{
if (_scopesDirty)
{
_scopeSnapshot = _scopes.Count > 0 ? _scopes.ToArray() : Array.Empty<ServiceScope>();
_scopesDirty = false;
}
return _scopeSnapshot;
}
private static int CompareScopeOrder(ServiceScope left, ServiceScope right)
=> left.Order.CompareTo(right.Order);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d48a8032734a55647a565ee72471c5c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c69106e6f9633554ea75fbc122821e2a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,68 @@
using UnityEngine;
namespace AlicizaX
{
[DefaultExecutionOrder(-32000)]
[DisallowMultipleComponent]
public class AppServiceRoot : MonoBehaviour
{
private static AppServiceRoot s_activeRoot;
[SerializeField] private bool _dontDestroyOnLoad = true;
[SerializeField] private int _appScopeOrder = -10000;
private bool _ownsWorld;
protected virtual void Awake()
{
if (s_activeRoot != null && s_activeRoot != this)
{
enabled = false;
return;
}
s_activeRoot = this;
if (_dontDestroyOnLoad)
DontDestroyOnLoad(gameObject);
var createdWorld = !AppServices.HasWorld;
var world = AppServices.EnsureWorld(_appScopeOrder);
_ownsWorld = createdWorld;
if (createdWorld)
RegisterAppServices(world.AppScope);
}
protected virtual void Update()
{
if (AppServices.HasWorld) AppServices.World.Tick(Time.deltaTime);
}
protected virtual void LateUpdate()
{
if (AppServices.HasWorld) AppServices.World.LateTick(Time.deltaTime);
}
protected virtual void FixedUpdate()
{
if (AppServices.HasWorld) AppServices.World.FixedTick(Time.fixedDeltaTime);
}
protected virtual void OnDrawGizmos()
{
if (AppServices.HasWorld) AppServices.World.DrawGizmos();
}
protected virtual void OnDestroy()
{
if (s_activeRoot == this)
s_activeRoot = null;
if (_ownsWorld && AppServices.HasWorld)
AppServices.Shutdown();
}
protected virtual void RegisterAppServices(ServiceScope appScope) { }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f1e04080395e4e649a43e98b673c5911
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,99 @@
using UnityEngine;
namespace AlicizaX
{
/// <summary>
/// Mono 服务基类(不自动注册,适合需要手动控制注册时机的场景)。
/// </summary>
public abstract class MonoServiceBehaviour : MonoBehaviour, IMonoService
{
public ServiceContext Context { get; private set; }
public bool IsInitialized { get; private set; }
protected ServiceWorld World => Context.World;
protected ServiceScope Scope => Context.Scope;
void IService.Initialize(ServiceContext context)
{
if (IsInitialized)
throw new System.InvalidOperationException($"{GetType().FullName} is already initialized.");
Context = context;
IsInitialized = true;
OnServiceInitialize();
}
void IService.Destroy()
{
if (!IsInitialized) return;
OnServiceDestroy();
IsInitialized = false;
Context = default;
}
protected virtual void OnServiceInitialize() { }
protected virtual void OnServiceDestroy() { }
}
/// <summary>
/// Mono 服务基类(自动注册到 <typeparamref name="TScope"/>)。
/// <para>
/// 场景服务:<c>_dontDestroyOnLoad = false</c>(默认),销毁时自动注销。<br/>
/// 跨场景服务:<c>_dontDestroyOnLoad = true</c>,首个实例持久化并注册;
/// 后续场景中出现的重复实例自动销毁自身。
/// </para>
/// <para>
/// 子类通过 <see cref="OnAwake"/> 执行额外的 Awake 逻辑,
/// 通过 <see cref="OnServiceInitialize"/> 执行注册后的初始化,
/// 通过 <see cref="OnServiceDestroy"/> 执行注销前的清理。
/// </para>
/// </summary>
public abstract class MonoServiceBehaviour<TScope> : MonoServiceBehaviour
where TScope : class
{
[SerializeField] private bool _dontDestroyOnLoad = false;
// 注意:使用 Start 而非 Awake 注册,确保 GameServiceRoot.Awake创建 World必然先于此执行。
// DefaultExecutionOrder 会影响所有生命周期(含 Awake用 Start 可彻底规避执行顺序陷阱。
private void Awake()
{
if (_dontDestroyOnLoad)
DontDestroyOnLoad(gameObject);
OnAwake();
}
private void Start()
{
var scope = AppServices.GetOrCreateScope<TScope>();
// 跨场景重复实例检测:契约已被占用则销毁自身
if (scope.HasContract(GetType()))
{
Destroy(gameObject);
return;
}
scope.Register(this);
}
private void OnDestroy()
{
if (!IsInitialized) return;
if (!AppServices.HasWorld) return;
if (!AppServices.TryGetScope<TScope>(out var scope)) return;
scope.Unregister(this);
}
/// <summary>
/// 在 Awake 阶段执行(早于 Start 中的自动注册)。
/// 适合缓存组件引用等不依赖服务系统的初始化。
/// </summary>
protected virtual void OnAwake() { }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: af63e3465b03eaf4ca3a82aebcab6e26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fe4fa3a609da4cd188473e4d4914897c
timeCreated: 1774439835

View File

@ -0,0 +1,133 @@
using System;
using UnityEngine;
using UnityEngine.Serialization;
namespace AlicizaX
{
public enum PoolResourceLoaderType
{
AssetBundle = 0,
Resources = 1
}
public enum PoolMatchMode
{
Exact = 0,
Prefix = 1
}
/// <summary>
/// 对象池配置项。
/// </summary>
[Serializable]
public sealed class PoolConfig
{
public const string DefaultGroup = "Default";
public const int DefaultCapacity = 8;
public const float DefaultInstanceIdleTimeout = 30f;
public const float DefaultPrefabUnloadDelay = 60f;
public string group = DefaultGroup;
[FormerlySerializedAs("asset")]
public string assetPath;
public PoolMatchMode matchMode = PoolMatchMode.Exact;
public PoolResourceLoaderType resourceLoaderType = PoolResourceLoaderType.AssetBundle;
[FormerlySerializedAs("time")]
[Min(0f)]
public float instanceIdleTimeout = DefaultInstanceIdleTimeout;
[Min(0f)]
public float prefabUnloadDelay = DefaultPrefabUnloadDelay;
[FormerlySerializedAs("poolCount")]
[Min(1)]
public int capacity = DefaultCapacity;
[Min(0)]
public int prewarmCount;
public bool preloadOnInitialize;
public void Normalize()
{
group = string.IsNullOrWhiteSpace(group) ? DefaultGroup : group.Trim();
assetPath = NormalizeAssetPath(assetPath);
capacity = Mathf.Max(1, capacity);
prewarmCount = Mathf.Clamp(prewarmCount, 0, capacity);
instanceIdleTimeout = Mathf.Max(0f, instanceIdleTimeout);
prefabUnloadDelay = Mathf.Max(0f, prefabUnloadDelay);
}
public bool Matches(string requestedAssetPath, string requestedGroup = null)
{
if (string.IsNullOrWhiteSpace(assetPath) || string.IsNullOrWhiteSpace(requestedAssetPath))
{
return false;
}
if (!string.IsNullOrWhiteSpace(requestedGroup) &&
!string.Equals(group, requestedGroup, StringComparison.Ordinal))
{
return false;
}
return matchMode switch
{
PoolMatchMode.Exact => string.Equals(requestedAssetPath, assetPath, StringComparison.Ordinal),
PoolMatchMode.Prefix => requestedAssetPath.StartsWith(assetPath, StringComparison.Ordinal),
_ => false
};
}
public string BuildResolvedPoolKey(string resolvedAssetPath)
{
return $"{group}|{(int)matchMode}|{assetPath}|{(int)resourceLoaderType}|{resolvedAssetPath}";
}
public static int CompareByPriority(PoolConfig left, PoolConfig right)
{
if (ReferenceEquals(left, right))
{
return 0;
}
if (left == null)
{
return 1;
}
if (right == null)
{
return -1;
}
int modeCompare = left.matchMode.CompareTo(right.matchMode);
if (modeCompare != 0)
{
return modeCompare;
}
int pathLengthCompare = right.assetPath.Length.CompareTo(left.assetPath.Length);
if (pathLengthCompare != 0)
{
return pathLengthCompare;
}
return string.Compare(left.group, right.group, StringComparison.Ordinal);
}
public static string NormalizeAssetPath(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
return string.Empty;
}
return value.Trim().Replace('\\', '/');
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7de4b0d73b4145b6bd836ec9d2c832d9
timeCreated: 1774439841

View File

@ -0,0 +1,33 @@
using System.Collections.Generic;
using UnityEngine;
namespace AlicizaX
{
[CreateAssetMenu(fileName = "PoolConfig", menuName = "GameplaySystem/PoolConfig", order = 10)]
public class PoolConfigScriptableObject : ScriptableObject
{
public List<PoolConfig> configs = new List<PoolConfig>();
public void Normalize()
{
if (configs == null)
{
configs = new List<PoolConfig>();
return;
}
for (int i = 0; i < configs.Count; i++)
{
configs[i]?.Normalize();
}
}
#if UNITY_EDITOR
private void OnValidate()
{
Normalize();
}
#endif
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,322 +0,0 @@
using UnityEditor;
using UnityEngine;
#if UNITY_EDITOR
namespace AlicizaX
{
[CustomEditor(typeof(GameObjectPool))]
public class GameObjectPoolEditor : UnityEditor.Editor
{
private bool[] _poolFoldouts;
private bool[] _prefabFoldouts;
private float _lastRefreshTime;
private const float AUTO_REFRESH_INTERVAL = 0.1f;
// 缓存序列化属性,避免重复查找
private SerializedProperty _poolInfosProperty;
private void OnEnable()
{
_poolInfosProperty = serializedObject.FindProperty("poolInfos");
_lastRefreshTime = Time.time;
}
public override void OnInspectorGUI()
{
var pool = (GameObjectPool)target;
// 更新序列化对象
serializedObject.Update();
// 绘制默认Inspector
DrawDefaultInspector();
EditorGUILayout.Space();
// 手动刷新按钮
if (GUILayout.Button("刷新池状态信息"))
{
RefreshPoolInfo(pool);
}
// 检查是否需要自动刷新
bool shouldAutoRefresh = EditorApplication.isPlaying && pool.showDetailedInfo &&
Selection.activeGameObject == pool.gameObject &&
Time.time - _lastRefreshTime > AUTO_REFRESH_INTERVAL;
if (shouldAutoRefresh)
{
RefreshPoolInfo(pool);
}
if (!pool.showDetailedInfo)
{
serializedObject.ApplyModifiedProperties();
return;
}
EditorGUILayout.Space();
EditorGUILayout.LabelField("对象池详细信息", EditorStyles.boldLabel);
// 重新获取属性以确保数据是最新的
_poolInfosProperty = serializedObject.FindProperty("poolInfos");
if (_poolInfosProperty != null && _poolInfosProperty.arraySize > 0)
{
DrawPoolInfos();
}
else
{
EditorGUILayout.HelpBox("暂无池信息,请等待系统初始化或点击刷新按钮", MessageType.Info);
}
// 显示自动刷新状态
if (Selection.activeGameObject == pool.gameObject)
{
EditorGUILayout.HelpBox("Inspector正在自动刷新 (仅在选中时)", MessageType.Info);
}
// 应用修改的属性
serializedObject.ApplyModifiedProperties();
}
private void RefreshPoolInfo(GameObjectPool pool)
{
pool.RefreshInspectorInfo();
_lastRefreshTime = Time.time;
serializedObject.Update(); // 立即更新序列化对象
// 标记需要重绘
if (Selection.activeGameObject == pool.gameObject)
{
EditorUtility.SetDirty(pool);
Repaint();
}
}
private void DrawPoolInfos()
{
int poolCount = _poolInfosProperty.arraySize;
// 确保折叠状态数组大小正确
if (_poolFoldouts == null || _poolFoldouts.Length != poolCount)
{
bool[] oldPoolFoldouts = _poolFoldouts;
bool[] oldPrefabFoldouts = _prefabFoldouts;
_poolFoldouts = new bool[poolCount];
_prefabFoldouts = new bool[poolCount];
// 保持之前的折叠状态
if (oldPoolFoldouts != null)
{
for (int i = 0; i < Mathf.Min(oldPoolFoldouts.Length, poolCount); i++)
{
_poolFoldouts[i] = oldPoolFoldouts[i];
if (oldPrefabFoldouts != null && i < oldPrefabFoldouts.Length)
{
_prefabFoldouts[i] = oldPrefabFoldouts[i];
}
}
}
}
for (int i = 0; i < poolCount; i++)
{
DrawPoolInfo(i);
}
}
private void DrawPoolInfo(int poolIndex)
{
var poolInfo = _poolInfosProperty.GetArrayElementAtIndex(poolIndex);
if (poolInfo == null) return;
var configAssetProp = poolInfo.FindPropertyRelative("configAsset");
var totalObjectsProp = poolInfo.FindPropertyRelative("totalObjects");
var maxCountProp = poolInfo.FindPropertyRelative("maxCount");
var activeObjectsProp = poolInfo.FindPropertyRelative("activeObjects");
if (configAssetProp == null || totalObjectsProp == null || maxCountProp == null || activeObjectsProp == null)
return;
string configAsset = configAssetProp.stringValue;
int totalObjects = totalObjectsProp.intValue;
int maxCount = maxCountProp.intValue;
int activeObjects = activeObjectsProp.intValue;
EditorGUILayout.BeginVertical("box");
// 使用Rect布局来精确控制Foldout的大小
Rect rect = EditorGUILayout.GetControlRect();
Rect foldoutRect = new Rect(rect.x, rect.y, 15, rect.height);
Rect progressRect = new Rect(rect.x + 20, rect.y, rect.width - 120, rect.height);
Rect labelRect = new Rect(rect.x + rect.width - 95, rect.y, 95, rect.height);
// 绘制折叠按钮
_poolFoldouts[poolIndex] = EditorGUI.Foldout(foldoutRect, _poolFoldouts[poolIndex], GUIContent.none);
// 使用率进度条
float usage = maxCount > 0 ? (float)totalObjects / maxCount : 0f;
EditorGUI.ProgressBar(progressRect, usage, $"{configAsset} ({totalObjects}/{maxCount})");
// 活跃对象数
EditorGUI.LabelField(labelRect, $"活跃:{activeObjects}", EditorStyles.miniLabel);
if (_poolFoldouts[poolIndex])
{
EditorGUI.indentLevel++;
DrawPoolDetails(poolInfo, poolIndex);
EditorGUI.indentLevel--;
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
}
private void DrawPoolDetails(SerializedProperty poolInfo, int poolIndex)
{
var configAssetProp = poolInfo.FindPropertyRelative("configAsset");
var maxCountProp = poolInfo.FindPropertyRelative("maxCount");
var expireTimeProp = poolInfo.FindPropertyRelative("expireTime");
var loadedPrefabsProp = poolInfo.FindPropertyRelative("loadedPrefabs");
if (configAssetProp != null)
EditorGUILayout.LabelField($"配置路径: {configAssetProp.stringValue}");
if (maxCountProp != null)
EditorGUILayout.LabelField($"最大数量: {maxCountProp.intValue}");
if (expireTimeProp != null)
EditorGUILayout.LabelField($"过期时间: {expireTimeProp.floatValue}s");
if (loadedPrefabsProp != null)
EditorGUILayout.LabelField($"已加载预制体: {loadedPrefabsProp.intValue}");
EditorGUILayout.Space();
// 绘制预制体引用信息
DrawPrefabRefs(poolInfo, poolIndex);
// 绘制对象详细信息
DrawObjectDetails(poolInfo);
}
private void DrawPrefabRefs(SerializedProperty poolInfo, int poolIndex)
{
var prefabRefsProp = poolInfo.FindPropertyRelative("prefabRefs");
if (prefabRefsProp == null || prefabRefsProp.arraySize <= 0) return;
// 使用简单的Foldout不指定宽度
_prefabFoldouts[poolIndex] = EditorGUILayout.Foldout(_prefabFoldouts[poolIndex], "预制体引用信息:");
if (_prefabFoldouts[poolIndex])
{
EditorGUI.indentLevel++;
for (int j = 0; j < prefabRefsProp.arraySize; j++)
{
DrawPrefabRefInfo(prefabRefsProp.GetArrayElementAtIndex(j));
}
EditorGUI.indentLevel--;
}
EditorGUILayout.Space();
}
private void DrawPrefabRefInfo(SerializedProperty prefabRef)
{
if (prefabRef == null) return;
var assetPathProp = prefabRef.FindPropertyRelative("assetPath");
var refCountProp = prefabRef.FindPropertyRelative("refCount");
var lastAccessTimeProp = prefabRef.FindPropertyRelative("lastAccessTime");
var prefabObjProp = prefabRef.FindPropertyRelative("prefab");
EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.BeginVertical();
if (assetPathProp != null)
EditorGUILayout.LabelField($"{System.IO.Path.GetFileName(assetPathProp.stringValue)}", EditorStyles.boldLabel);
if (refCountProp != null)
EditorGUILayout.LabelField($"引用计数: {refCountProp.intValue}", EditorStyles.miniLabel);
if (lastAccessTimeProp != null)
EditorGUILayout.LabelField($"最后访问: {(Time.time - lastAccessTimeProp.floatValue):F1}秒前", EditorStyles.miniLabel);
EditorGUILayout.EndVertical();
if (prefabObjProp != null)
EditorGUILayout.ObjectField(prefabObjProp.objectReferenceValue, typeof(GameObject), false, GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
}
private void DrawObjectDetails(SerializedProperty poolInfo)
{
var objectsProp = poolInfo.FindPropertyRelative("objects");
if (objectsProp == null || objectsProp.arraySize <= 0) return;
EditorGUILayout.LabelField("对象详情:", EditorStyles.boldLabel);
for (int j = 0; j < objectsProp.arraySize; j++)
{
DrawObjectInfo(objectsProp.GetArrayElementAtIndex(j));
}
}
private void DrawObjectInfo(SerializedProperty obj)
{
if (obj == null) return;
var objNameProp = obj.FindPropertyRelative("objectName");
var objAssetPathProp = obj.FindPropertyRelative("assetPath");
var isActiveProp = obj.FindPropertyRelative("isActive");
var remainingTimeProp = obj.FindPropertyRelative("remainingTime");
var expireProgressProp = obj.FindPropertyRelative("expireProgress");
var gameObjectProp = obj.FindPropertyRelative("gameObject");
EditorGUILayout.BeginHorizontal("box");
// 状态颜色指示器
bool isActive = isActiveProp?.boolValue ?? false;
var statusColor = isActive ? Color.green : Color.yellow;
var prevColor = GUI.color;
GUI.color = statusColor;
EditorGUILayout.LabelField("●", GUILayout.Width(15));
GUI.color = prevColor;
EditorGUILayout.BeginVertical();
// 对象名称和路径
string objName = objNameProp?.stringValue ?? "Unknown";
string objAssetPath = objAssetPathProp?.stringValue ?? "";
EditorGUILayout.LabelField($"{objName} ({System.IO.Path.GetFileName(objAssetPath)})", EditorStyles.boldLabel);
EditorGUILayout.LabelField($"状态: {(isActive ? "" : "")}", EditorStyles.miniLabel);
// 过期进度条
if (!isActive && remainingTimeProp != null && expireProgressProp != null)
{
float remainingTime = remainingTimeProp.floatValue;
float expireProgress = expireProgressProp.floatValue;
if (remainingTime >= 0)
{
Rect expireRect = GUILayoutUtility.GetRect(100, 16, GUILayout.ExpandWidth(true), GUILayout.Height(16));
EditorGUI.ProgressBar(expireRect, expireProgress, $"释放倒计时: {remainingTime:F1}s");
}
}
EditorGUILayout.EndVertical();
// GameObject引用
if (gameObjectProp != null)
EditorGUILayout.ObjectField(gameObjectProp.objectReferenceValue, typeof(GameObject), true, GUILayout.Width(100));
EditorGUILayout.EndHorizontal();
}
public override bool RequiresConstantRepaint()
{
// 只有在选中对象池时才需要持续重绘
var pool = target as GameObjectPool;
return pool != null && pool.showDetailedInfo && Selection.activeGameObject == pool.gameObject;
}
}
}
#endif

View File

@ -1,3 +0,0 @@
fileFormatVersion: 2
guid: df436879f8854a95b5a92e8b77772189
timeCreated: 1773109368

View File

@ -0,0 +1,606 @@
using System;
using System.Collections.Generic;
using System.Threading;
using AlicizaX.Resource.Runtime;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace AlicizaX
{
public sealed class GameObjectPool : MonoServiceBehaviour<GameObjectPool>
{
[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<string, RuntimePrefabPool> _poolsByKey = new Dictionary<string, RuntimePrefabPool>(StringComparer.Ordinal);
private readonly Dictionary<string, PoolConfig> _resolvedConfigCache = new Dictionary<string, PoolConfig>(StringComparer.Ordinal);
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;
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<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()
{
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
{
pool.Shutdown();
MemoryPool.Release(pool);
}
_poolsByKey.Clear();
_ownersByObject.Clear();
_resolvedConfigCache.Clear();
ReleaseDebugSnapshots();
}
public List<GameObjectPoolSnapshot> GetDebugSnapshots()
{
ReleaseDebugSnapshots();
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
{
_debugSnapshots.Add(pool.CreateSnapshot());
}
_debugSnapshots.Sort((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);
if (groupCompare != 0)
{
return groupCompare;
}
return string.Compare(left.assetPath, right.assetPath, StringComparison.Ordinal);
});
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)
{
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))
{
_resourceLoaders[PoolResourceLoaderType.AssetBundle] = new AlicizaResourceLoader();
}
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();
PoolConfigScriptableObject configAsset =
ModuleSystem.GetModule<IResourceModule>().LoadAsset<PoolConfigScriptableObject>(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();
}
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;
}
string cacheKey = $"{group ?? string.Empty}|{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<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)
{
if (!string.IsNullOrWhiteSpace(group))
{
for (int i = 0; i < _configs.Count; i++)
{
PoolConfig config = _configs[i];
if (string.Equals(config.group, group, StringComparison.Ordinal))
{
return GetResourceLoader(config.resourceLoaderType);
}
}
}
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()
{
for (int i = 0; i < _configs.Count; i++)
{
PoolConfig left = _configs[i];
for (int j = i + 1; j < _configs.Count; j++)
{
PoolConfig right = _configs[j];
if (!string.Equals(left.group, right.group, StringComparison.Ordinal))
{
continue;
}
if (left.matchMode != right.matchMode)
{
continue;
}
if (!string.Equals(left.assetPath, right.assetPath, StringComparison.Ordinal))
{
continue;
}
Log.Warning($"Duplicate pool config detected: '{left.group}:{left.assetPath}'.");
}
}
}
private void ReleaseDebugSnapshots()
{
for (int i = 0; i < _debugSnapshots.Count; i++)
{
MemoryPool.Release(_debugSnapshots[i]);
}
_debugSnapshots.Clear();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 17b192955f7691b4e8283328c926ab1a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,106 @@
using System.Threading;
using AlicizaX.Resource.Runtime;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace AlicizaX
{
public interface IResourceLoader
{
GameObject LoadPrefab(string location);
UniTask<GameObject> LoadPrefabAsync(string location, CancellationToken cancellationToken = default);
GameObject LoadGameObject(string location, Transform parent = null);
UniTask<GameObject> LoadGameObjectAsync(string location, Transform parent = null, CancellationToken cancellationToken = default);
void UnloadAsset(GameObject gameObject);
}
public class UnityResourcesLoader : IResourceLoader
{
public GameObject LoadPrefab(string location)
{
return Resources.Load<GameObject>(location);
}
public async UniTask<GameObject> LoadPrefabAsync(string location, CancellationToken cancellationToken = default)
{
return await Resources.LoadAsync<GameObject>(location).ToUniTask(cancellationToken: cancellationToken) as GameObject;
}
public GameObject LoadGameObject(string location, Transform parent = null)
{
var prefab = Resources.Load<GameObject>(location);
if (prefab == null) return null;
var instance = GameObject.Instantiate(prefab);
if (instance != null && parent != null)
{
instance.transform.SetParent(parent);
}
return instance;
}
public async UniTask<GameObject> LoadGameObjectAsync(string location, Transform parent = null, CancellationToken cancellationToken = default)
{
var prefab = await Resources.LoadAsync<GameObject>(location).ToUniTask(cancellationToken: cancellationToken) as GameObject;
if (prefab == null) return null;
var instance = GameObject.Instantiate(prefab);
if (instance != null && parent != null)
{
instance.transform.SetParent(parent);
}
return instance;
}
public void UnloadAsset(GameObject gameObject)
{
Resources.UnloadAsset(gameObject);
}
}
public class AlicizaResourceLoader : IResourceLoader
{
private IResourceModule _resourceModule;
IResourceModule ResourceModule
{
get
{
if (_resourceModule == null)
{
_resourceModule = ModuleSystem.GetModule<IResourceModule>();
}
return _resourceModule;
}
}
public GameObject LoadPrefab(string location)
{
return ResourceModule.LoadAsset<GameObject>(location);
}
public async UniTask<GameObject> LoadPrefabAsync(string location, CancellationToken cancellationToken = default)
{
return await ResourceModule.LoadAssetAsync<GameObject>(location, cancellationToken);
}
public GameObject LoadGameObject(string location, Transform parent = null)
{
return ResourceModule.LoadGameObject(location, parent);
}
public async UniTask<GameObject> LoadGameObjectAsync(string location, Transform parent = null, CancellationToken cancellationToken = default)
{
return await ResourceModule.LoadGameObjectAsync(location, parent, cancellationToken);
}
public void UnloadAsset(GameObject gameObject)
{
ResourceModule.UnloadAsset(gameObject);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9b48a0c8ea0e494f91746c5c40670feb
timeCreated: 1774439792

View File

@ -1,12 +0,0 @@
using System.Collections.Generic;
using UnityEngine;
namespace AlicizaX
{
[CreateAssetMenu(fileName = "PoolConfig", menuName = "GameplaySystem/PoolConfig", order = 10)]
public class PoolConfigScriptableObject : ScriptableObject
{
public List<PoolConfig> configs;
}
}

View File

@ -0,0 +1,583 @@
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace AlicizaX
{
[Serializable]
public sealed class GameObjectPoolInstanceSnapshot : IMemory
{
public string instanceName;
public bool isActive;
public float idleDuration;
public GameObject gameObject;
public void Clear()
{
instanceName = null;
isActive = false;
idleDuration = 0f;
gameObject = null;
}
}
[Serializable]
public sealed class GameObjectPoolSnapshot : IMemory
{
public string group;
public string assetPath;
public PoolMatchMode matchMode;
public PoolResourceLoaderType loaderType;
public int capacity;
public int totalCount;
public int activeCount;
public int inactiveCount;
public float instanceIdleTimeout;
public float prefabUnloadDelay;
public bool prefabLoaded;
public float prefabIdleDuration;
public List<GameObjectPoolInstanceSnapshot> instances = new List<GameObjectPoolInstanceSnapshot>();
public void Clear()
{
group = null;
assetPath = null;
matchMode = default;
loaderType = default;
capacity = 0;
totalCount = 0;
activeCount = 0;
inactiveCount = 0;
instanceIdleTimeout = 0f;
prefabUnloadDelay = 0f;
prefabLoaded = false;
prefabIdleDuration = 0f;
for (int i = 0; i < instances.Count; i++)
{
MemoryPool.Release(instances[i]);
}
instances.Clear();
}
}
[DisallowMultipleComponent]
public sealed class PooledGameObjectMarker : MonoBehaviour
{
private RuntimePrefabPool _owner;
private RuntimePooledInstance _instance;
internal void Bind(RuntimePrefabPool owner, RuntimePooledInstance instance)
{
_owner = owner;
_instance = instance;
}
public void Detach()
{
_owner = null;
_instance = null;
}
private void OnDestroy()
{
if (_owner != null && _instance != null)
{
_owner.NotifyInstanceDestroyed(_instance);
}
}
}
internal sealed class RuntimePooledInstance : IMemory
{
public GameObject GameObject { get; private set; }
public PooledGameObjectMarker Marker { get; private set; }
public LinkedListNode<RuntimePooledInstance> InactiveNode { get; set; }
public bool IsActive { get; set; }
public float LastReleaseTime { get; set; }
public void Initialize(GameObject gameObject, PooledGameObjectMarker marker)
{
GameObject = gameObject;
Marker = marker;
}
public void Clear()
{
GameObject = null;
Marker = null;
InactiveNode = null;
IsActive = false;
LastReleaseTime = 0f;
}
}
internal sealed class RuntimePrefabPool : IMemory
{
private PoolConfig _config;
private string _assetPath;
private IResourceLoader _loader;
private GameObjectPool _service;
private CancellationToken _shutdownToken;
private Dictionary<GameObject, RuntimePooledInstance> _instancesByObject;
private LinkedList<RuntimePooledInstance> _inactiveInstances;
private Transform _root;
private GameObject _prefab;
private UniTaskCompletionSource<GameObject> _prefabLoadSource;
private int _activeCount;
private float _lastPrefabTouchTime;
public RuntimePrefabPool()
{
_instancesByObject = new Dictionary<GameObject, RuntimePooledInstance>();
_inactiveInstances = new LinkedList<RuntimePooledInstance>();
}
public void Initialize(
PoolConfig config,
string assetPath,
IResourceLoader loader,
GameObjectPool service,
CancellationToken shutdownToken)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_assetPath = assetPath ?? throw new ArgumentNullException(nameof(assetPath));
_loader = loader ?? throw new ArgumentNullException(nameof(loader));
_service = service ?? throw new ArgumentNullException(nameof(service));
_shutdownToken = shutdownToken;
_lastPrefabTouchTime = Time.time;
_instancesByObject.Clear();
_inactiveInstances.Clear();
_prefab = null;
_prefabLoadSource = null;
_activeCount = 0;
GameObject rootObject = new GameObject($"Pool_{SanitizeName(config.group)}_{SanitizeName(assetPath)}");
_root = rootObject.transform;
_root.SetParent(service.poolContainer, false);
rootObject.SetActive(false);
}
public string AssetPath => _assetPath;
public PoolConfig Config => _config;
public int TotalCount => _instancesByObject.Count;
public int ActiveCount => _activeCount;
public int InactiveCount => _inactiveInstances.Count;
public bool IsPrefabLoaded => _prefab != null;
public float PrefabIdleDuration => _prefab == null ? 0f : Mathf.Max(0f, Time.time - _lastPrefabTouchTime);
public GameObject Acquire(Transform parent)
{
EnsurePrefabLoaded();
return AcquirePrepared(parent);
}
public async UniTask<GameObject> AcquireAsync(Transform parent, CancellationToken cancellationToken)
{
await EnsurePrefabLoadedAsync(cancellationToken);
return AcquirePrepared(parent);
}
public void Warmup(int count)
{
if (count <= 0)
{
return;
}
EnsurePrefabLoaded();
WarmupPrepared(count);
}
public async UniTask WarmupAsync(int count, CancellationToken cancellationToken)
{
if (count <= 0)
{
return;
}
await EnsurePrefabLoadedAsync(cancellationToken);
WarmupPrepared(count);
}
public bool Release(GameObject gameObject)
{
if (gameObject == null || !_instancesByObject.TryGetValue(gameObject, out RuntimePooledInstance instance))
{
return false;
}
if (!instance.IsActive)
{
return true;
}
instance.IsActive = false;
instance.LastReleaseTime = Time.time;
_activeCount = Mathf.Max(0, _activeCount - 1);
Transform transform = gameObject.transform;
transform.SetParent(_root, false);
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
transform.localScale = Vector3.one;
gameObject.SetActive(false);
instance.InactiveNode = _inactiveInstances.AddLast(instance);
TouchPrefab();
return true;
}
public void NotifyInstanceDestroyed(RuntimePooledInstance instance)
{
if (instance == null)
{
return;
}
RemoveInstance(instance);
MemoryPool.Release(instance);
}
public void TrimExpiredInstances(float now)
{
if (_config.instanceIdleTimeout > 0f)
{
while (_inactiveInstances.First != null)
{
RuntimePooledInstance candidate = _inactiveInstances.First.Value;
if (now - candidate.LastReleaseTime < _config.instanceIdleTimeout)
{
break;
}
DestroyInstance(candidate);
}
}
if (_prefab != null &&
_instancesByObject.Count == 0 &&
_config.prefabUnloadDelay > 0f &&
now - _lastPrefabTouchTime >= _config.prefabUnloadDelay)
{
_loader.UnloadAsset(_prefab);
_prefab = null;
}
}
public GameObjectPoolSnapshot CreateSnapshot()
{
var snapshot = MemoryPool.Acquire<GameObjectPoolSnapshot>();
snapshot.group = _config.group;
snapshot.assetPath = _assetPath;
snapshot.matchMode = _config.matchMode;
snapshot.loaderType = _config.resourceLoaderType;
snapshot.capacity = _config.capacity;
snapshot.totalCount = _instancesByObject.Count;
snapshot.activeCount = _activeCount;
snapshot.inactiveCount = _inactiveInstances.Count;
snapshot.instanceIdleTimeout = _config.instanceIdleTimeout;
snapshot.prefabUnloadDelay = _config.prefabUnloadDelay;
snapshot.prefabLoaded = _prefab != null;
snapshot.prefabIdleDuration = PrefabIdleDuration;
foreach (RuntimePooledInstance instance in _instancesByObject.Values)
{
var instanceSnapshot = MemoryPool.Acquire<GameObjectPoolInstanceSnapshot>();
instanceSnapshot.instanceName = instance.GameObject != null ? instance.GameObject.name : "<destroyed>";
instanceSnapshot.isActive = instance.IsActive;
instanceSnapshot.idleDuration = instance.IsActive ? 0f : Mathf.Max(0f, Time.time - instance.LastReleaseTime);
instanceSnapshot.gameObject = instance.GameObject;
snapshot.instances.Add(instanceSnapshot);
}
snapshot.instances.Sort((left, right) =>
{
if (left == null && right == null)
{
return 0;
}
if (left == null)
{
return 1;
}
if (right == null)
{
return -1;
}
if (left.isActive != right.isActive)
{
return left.isActive ? -1 : 1;
}
return string.Compare(left.instanceName, right.instanceName, StringComparison.Ordinal);
});
return snapshot;
}
public void Shutdown()
{
var instances = new List<RuntimePooledInstance>(_instancesByObject.Values);
foreach (RuntimePooledInstance instance in instances)
{
DestroyInstance(instance);
}
_inactiveInstances.Clear();
_instancesByObject.Clear();
_activeCount = 0;
if (_prefab != null)
{
_loader.UnloadAsset(_prefab);
_prefab = null;
}
if (_root != null)
{
GameObject.Destroy(_root.gameObject);
}
_root = null;
}
private void EnsurePrefabLoaded()
{
if (_prefab != null)
{
TouchPrefab();
return;
}
if (_prefabLoadSource != null)
{
throw new InvalidOperationException(
$"Pool asset '{_assetPath}' is being loaded asynchronously. Use the async acquire API.");
}
_prefab = _loader.LoadPrefab(_assetPath);
if (_prefab == null)
{
throw new InvalidOperationException($"Failed to load pooled prefab '{_assetPath}'.");
}
TouchPrefab();
}
private async UniTask EnsurePrefabLoadedAsync(CancellationToken cancellationToken)
{
if (_prefab != null)
{
TouchPrefab();
return;
}
if (_prefabLoadSource != null)
{
await _prefabLoadSource.Task.AttachExternalCancellation(cancellationToken);
return;
}
_prefabLoadSource = new UniTaskCompletionSource<GameObject>();
try
{
GameObject prefab = await _loader.LoadPrefabAsync(_assetPath, _shutdownToken);
if (prefab == null)
{
throw new InvalidOperationException($"Failed to load pooled prefab '{_assetPath}'.");
}
_prefab = prefab;
TouchPrefab();
_prefabLoadSource.TrySetResult(prefab);
}
catch (Exception exception)
{
_prefabLoadSource.TrySetException(exception);
throw;
}
finally
{
_prefabLoadSource = null;
}
}
private void WarmupPrepared(int requestedCount)
{
int targetCount = Mathf.Clamp(requestedCount, 0, _config.capacity);
if (targetCount <= _instancesByObject.Count)
{
return;
}
int toCreate = targetCount - _instancesByObject.Count;
for (int i = 0; i < toCreate; i++)
{
RuntimePooledInstance instance = CreateInstance();
ParkInactive(instance);
}
}
private GameObject AcquirePrepared(Transform parent)
{
RuntimePooledInstance instance = null;
if (_inactiveInstances.Last != null)
{
instance = _inactiveInstances.Last.Value;
RemoveInactiveNode(instance);
}
else if (_instancesByObject.Count < _config.capacity)
{
instance = CreateInstance();
}
if (instance == null)
{
Log.Warning($"Pool exhausted for '{_assetPath}'. Capacity: {_config.capacity}");
return null;
}
instance.IsActive = true;
_activeCount++;
TouchPrefab();
GameObject gameObject = instance.GameObject;
if (parent != null)
{
gameObject.transform.SetParent(parent);
}
gameObject.SetActive(true);
return gameObject;
}
private RuntimePooledInstance CreateInstance()
{
GameObject gameObject = GameObject.Instantiate(_prefab);
gameObject.SetActive(false);
gameObject.transform.SetParent(_root, false);
PooledGameObjectMarker marker = gameObject.GetComponent<PooledGameObjectMarker>();
if (marker == null)
{
marker = gameObject.AddComponent<PooledGameObjectMarker>();
}
var instance = MemoryPool.Acquire<RuntimePooledInstance>();
instance.Initialize(gameObject, marker);
marker.Bind(this, instance);
_instancesByObject.Add(gameObject, instance);
_service.RegisterOwnedObject(gameObject, this);
TouchPrefab();
return instance;
}
private void ParkInactive(RuntimePooledInstance instance)
{
if (instance == null)
{
return;
}
instance.IsActive = false;
instance.LastReleaseTime = Time.time;
Transform transform = instance.GameObject.transform;
transform.SetParent(_root, false);
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
transform.localScale = Vector3.one;
instance.GameObject.SetActive(false);
instance.InactiveNode = _inactiveInstances.AddLast(instance);
}
private void DestroyInstance(RuntimePooledInstance instance)
{
if (instance == null)
{
return;
}
RemoveInstance(instance);
if (instance.GameObject != null)
{
instance.Marker?.Detach();
GameObject.Destroy(instance.GameObject);
}
MemoryPool.Release(instance);
}
private void RemoveInstance(RuntimePooledInstance instance)
{
GameObject gameObject = instance?.GameObject;
if (gameObject == null)
{
return;
}
RemoveInactiveNode(instance);
if (instance.IsActive)
{
instance.IsActive = false;
_activeCount = Mathf.Max(0, _activeCount - 1);
}
_instancesByObject.Remove(gameObject);
_service.UnregisterOwnedObject(gameObject);
TouchPrefab();
}
private void RemoveInactiveNode(RuntimePooledInstance instance)
{
if (instance?.InactiveNode == null)
{
return;
}
_inactiveInstances.Remove(instance.InactiveNode);
instance.InactiveNode = null;
}
private void TouchPrefab()
{
_lastPrefabTouchTime = Time.time;
}
public void Clear()
{
_config = null;
_assetPath = null;
_loader = null;
_service = null;
_shutdownToken = default;
_instancesByObject.Clear();
_inactiveInstances.Clear();
_root = null;
_prefab = null;
_prefabLoadSource = null;
_activeCount = 0;
_lastPrefabTouchTime = 0f;
}
private static string SanitizeName(string value)
{
return string.IsNullOrWhiteSpace(value)
? "Unnamed"
: value.Replace('/', '_').Replace('\\', '_').Replace(':', '_');
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 542273e73f9fe8c46854d088f3c6387c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: