diff --git a/Editor/GameObjectPool.meta b/Editor/GameObjectPool.meta new file mode 100644 index 0000000..2bd3b63 --- /dev/null +++ b/Editor/GameObjectPool.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6571694cccb20a64fb609396b9aada2b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/GameObjectPool/GameObjectPoolEditor.cs b/Editor/GameObjectPool/GameObjectPoolEditor.cs new file mode 100644 index 0000000..e893c44 --- /dev/null +++ b/Editor/GameObjectPool/GameObjectPoolEditor.cs @@ -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 _foldoutState = new Dictionary(); + + 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 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(); + } + } +} diff --git a/Runtime/ABase/GameObjectPool/GameObjectPool.cs.meta b/Editor/GameObjectPool/GameObjectPoolEditor.cs.meta similarity index 83% rename from Runtime/ABase/GameObjectPool/GameObjectPool.cs.meta rename to Editor/GameObjectPool/GameObjectPoolEditor.cs.meta index 79c907a..b5e6e80 100644 --- a/Runtime/ABase/GameObjectPool/GameObjectPool.cs.meta +++ b/Editor/GameObjectPool/GameObjectPoolEditor.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: ce0e8ead006ba324eaf2410a3dd556a5 +guid: 43ecbf81dd8d96d418b2d61e28e453cd MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Editor/GameObjectPool/PoolConfigScriptableObjectEditor.cs b/Editor/GameObjectPool/PoolConfigScriptableObjectEditor.cs new file mode 100644 index 0000000..8421da5 --- /dev/null +++ b/Editor/GameObjectPool/PoolConfigScriptableObjectEditor.cs @@ -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 warnings = BuildWarnings(asset.configs); + for (int i = 0; i < warnings.Count; i++) + { + EditorGUILayout.HelpBox(warnings[i], MessageType.Warning); + } + } + + private static List BuildWarnings(List configs) + { + var warnings = new List(); + + 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; + } + } +} diff --git a/Editor/GameObjectPool/PoolConfigScriptableObjectEditor.cs.meta b/Editor/GameObjectPool/PoolConfigScriptableObjectEditor.cs.meta new file mode 100644 index 0000000..8467ab1 --- /dev/null +++ b/Editor/GameObjectPool/PoolConfigScriptableObjectEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 21b55582105cd3a44bfc4bfb935a35d1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Entry.prefab b/Entry.prefab index c88ef25..f2db106 100644 --- a/Entry.prefab +++ b/Entry.prefab @@ -355,6 +355,7 @@ MonoBehaviour: minUnloadUnusedAssetsInterval: 60 maxUnloadUnusedAssetsInterval: 300 useSystemUnloadUnusedAssets: 1 + minGCCollectInterval: 30 decryptionServices: autoUnloadBundleWhenUnused: 0 _playMode: 0 diff --git a/Runtime/ABase/Base/Service.meta b/Runtime/ABase/Base/Service.meta new file mode 100644 index 0000000..afffe6e --- /dev/null +++ b/Runtime/ABase/Base/Service.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 420d45a81b5c11e478b3a5ebe958914f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core.meta b/Runtime/ABase/Base/Service/Core.meta new file mode 100644 index 0000000..3b38e5f --- /dev/null +++ b/Runtime/ABase/Base/Service/Core.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 48e89f2eceb320547b4986db840128da +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core/AppScope.cs b/Runtime/ABase/Base/Service/Core/AppScope.cs new file mode 100644 index 0000000..740f4b9 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/AppScope.cs @@ -0,0 +1,7 @@ +namespace AlicizaX +{ + /// + /// 框架内置的 App Scope 标记类,生命周期与 ServiceWorld 相同。 + /// + public sealed class AppScope { } +} diff --git a/Runtime/ABase/Base/Service/Core/AppScope.cs.meta b/Runtime/ABase/Base/Service/Core/AppScope.cs.meta new file mode 100644 index 0000000..e95ef57 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/AppScope.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d205821b5dae0264fb6c5d1ea78f9b43 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core/AppServices.cs b/Runtime/ABase/Base/Service/Core/AppServices.cs new file mode 100644 index 0000000..27bedaa --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/AppServices.cs @@ -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(int order = 0) where TScope : class + => World.CreateScope(order); + + public static ServiceScope GetOrCreateScope(int order = 0) where TScope : class + => World.GetOrCreateScope(order); + + public static bool TryGetScope(out ServiceScope scope) where TScope : class + { + if (_world == null) { scope = null; return false; } + return _world.TryGetScope(out scope); + } + + public static bool DestroyScope() where TScope : class + { + if (_world == null) return false; + return _world.DestroyScope(); + } + + // ── Service 查找 ──────────────────────────────────────────────────────── + + public static bool TryGet(out T service) where T : class, IService + { + if (_world == null) { service = null; return false; } + return _world.TryGet(out service); + } + + public static T Require() where T : class, IService + => World.Require(); + + // ── 生命周期 ──────────────────────────────────────────────────────────── + + public static void Shutdown() + { + if (_world == null) return; + _world.Dispose(); + _world = null; + } + } +} diff --git a/Runtime/ABase/Base/Service/Core/AppServices.cs.meta b/Runtime/ABase/Base/Service/Core/AppServices.cs.meta new file mode 100644 index 0000000..5bcacd8 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/AppServices.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7186431b5e61b3c4f9865bd9901ce831 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core/IMonoService.cs b/Runtime/ABase/Base/Service/Core/IMonoService.cs new file mode 100644 index 0000000..3e3cb90 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/IMonoService.cs @@ -0,0 +1,8 @@ +namespace AlicizaX +{ + /// + /// 标记一个 IService 实现来自 MonoBehaviour。 + /// ServiceScope 通过此接口识别 Mono 服务,避免 Core 层依赖 UnityEngine。 + /// + public interface IMonoService : IService { } +} diff --git a/Runtime/ABase/Base/Service/Core/IMonoService.cs.meta b/Runtime/ABase/Base/Service/Core/IMonoService.cs.meta new file mode 100644 index 0000000..381e576 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/IMonoService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 64c7431bc2d38d24594f92006ac398d9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core/IService.cs b/Runtime/ABase/Base/Service/Core/IService.cs new file mode 100644 index 0000000..49b71e9 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/IService.cs @@ -0,0 +1,8 @@ +namespace AlicizaX +{ + public interface IService + { + void Initialize(ServiceContext context); + void Destroy(); + } +} diff --git a/Runtime/ABase/Base/Service/Core/IService.cs.meta b/Runtime/ABase/Base/Service/Core/IService.cs.meta new file mode 100644 index 0000000..e83f7ee --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/IService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1cab09453acbb93498bd781202713656 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core/IServiceOrder.cs b/Runtime/ABase/Base/Service/Core/IServiceOrder.cs new file mode 100644 index 0000000..9993b23 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/IServiceOrder.cs @@ -0,0 +1,7 @@ +namespace AlicizaX +{ + public interface IServiceOrder + { + int Order { get; } + } +} diff --git a/Runtime/ABase/Base/Service/Core/IServiceOrder.cs.meta b/Runtime/ABase/Base/Service/Core/IServiceOrder.cs.meta new file mode 100644 index 0000000..ac24793 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/IServiceOrder.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f66ea6b1b8a144b4184b2b078305f8df +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core/IServiceTicks.cs b/Runtime/ABase/Base/Service/Core/IServiceTicks.cs new file mode 100644 index 0000000..6397efa --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/IServiceTicks.cs @@ -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(); + } +} diff --git a/Runtime/ABase/Base/Service/Core/IServiceTicks.cs.meta b/Runtime/ABase/Base/Service/Core/IServiceTicks.cs.meta new file mode 100644 index 0000000..69a99d4 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/IServiceTicks.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a7ff388c5465f4c439bf85bb76c2d786 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core/ReferenceComparer.cs b/Runtime/ABase/Base/Service/Core/ReferenceComparer.cs new file mode 100644 index 0000000..688f2e2 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/ReferenceComparer.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace AlicizaX +{ + internal sealed class ReferenceComparer : IEqualityComparer + where T : class + { + public static readonly ReferenceComparer Instance = new ReferenceComparer(); + + private ReferenceComparer() { } + + public bool Equals(T x, T y) => ReferenceEquals(x, y); + + public int GetHashCode(T obj) + => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); + } +} diff --git a/Runtime/ABase/Base/Service/Core/ReferenceComparer.cs.meta b/Runtime/ABase/Base/Service/Core/ReferenceComparer.cs.meta new file mode 100644 index 0000000..b457acc --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/ReferenceComparer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f3afc1438ca0eba4892ac93b1165d60d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core/ServiceBase.cs b/Runtime/ABase/Base/Service/Core/ServiceBase.cs new file mode 100644 index 0000000..c35d012 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/ServiceBase.cs @@ -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(); + } +} diff --git a/Runtime/ABase/Base/Service/Core/ServiceBase.cs.meta b/Runtime/ABase/Base/Service/Core/ServiceBase.cs.meta new file mode 100644 index 0000000..d227b38 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/ServiceBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af65a59b2e4b11b419fc09505ecd1c88 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core/ServiceContext.cs b/Runtime/ABase/Base/Service/Core/ServiceContext.cs new file mode 100644 index 0000000..9585391 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/ServiceContext.cs @@ -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() where T : class, IService + => World.Require(Scope); + + public bool TryGet(out T service) where T : class, IService + => World.TryGet(Scope, out service); + + public ServiceScope CreateScope(int order = 0) where TScope : class + => World.CreateScope(order); + + public ServiceScope GetOrCreateScope(int order = 0) where TScope : class + => World.GetOrCreateScope(order); + + public bool TryGetScope(out ServiceScope scope) where TScope : class + => World.TryGetScope(out scope); + } +} diff --git a/Runtime/ABase/Base/Service/Core/ServiceContext.cs.meta b/Runtime/ABase/Base/Service/Core/ServiceContext.cs.meta new file mode 100644 index 0000000..ce8559b --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/ServiceContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fd30fae6aee72a04db7507d194215b40 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core/ServiceContractUtility.cs b/Runtime/ABase/Base/Service/Core/ServiceContractUtility.cs new file mode 100644 index 0000000..dddf4e9 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/ServiceContractUtility.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; + +namespace AlicizaX +{ + internal static class ServiceContractUtility + { + private static readonly HashSet ExcludedContracts = new HashSet + { + typeof(IService), + typeof(IServiceTickable), + typeof(IServiceLateTickable), + typeof(IServiceFixedTickable), + typeof(IServiceGizmoDrawable), + typeof(IServiceOrder), + }; + + public static List Collect(Type serviceType, IReadOnlyList extraContracts) + { + var contracts = new List { serviceType }; + var unique = new HashSet { 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."); + } + } +} diff --git a/Runtime/ABase/Base/Service/Core/ServiceContractUtility.cs.meta b/Runtime/ABase/Base/Service/Core/ServiceContractUtility.cs.meta new file mode 100644 index 0000000..536cdd5 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/ServiceContractUtility.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 95c468fca1e70ab4b931d26d98af3fd9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core/ServiceScope.cs b/Runtime/ABase/Base/Service/Core/ServiceScope.cs new file mode 100644 index 0000000..595dcd1 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/ServiceScope.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections.Generic; + +namespace AlicizaX +{ + public sealed class ServiceScope : IDisposable + { + private readonly Dictionary _servicesByContract = new Dictionary(); + private readonly Dictionary> _contractsByService = new Dictionary>(ReferenceComparer.Instance); + private readonly List _registrationOrder = new List(); + + private readonly List _tickables = new List(); + private readonly List _lateTickables = new List(); + private readonly List _fixedTickables = new List(); + private readonly List _gizmoDrawables = new List(); + + private IServiceTickable[] _tickableSnapshot = Array.Empty(); + private IServiceLateTickable[] _lateTickableSnapshot = Array.Empty(); + private IServiceFixedTickable[] _fixedTickableSnapshot = Array.Empty(); + private IServiceGizmoDrawable[] _gizmoSnapshot = Array.Empty(); + + 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 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() 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(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() 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(); + _tickablesDirty = false; + } + return _tickableSnapshot; + } + + private IServiceLateTickable[] GetLateTickSnapshot() + { + if (_lateTickablesDirty) + { + _lateTickables.Sort(CompareByOrder); + _lateTickableSnapshot = _lateTickables.Count > 0 ? _lateTickables.ToArray() : Array.Empty(); + _lateTickablesDirty = false; + } + return _lateTickableSnapshot; + } + + private IServiceFixedTickable[] GetFixedTickSnapshot() + { + if (_fixedTickablesDirty) + { + _fixedTickables.Sort(CompareByOrder); + _fixedTickableSnapshot = _fixedTickables.Count > 0 ? _fixedTickables.ToArray() : Array.Empty(); + _fixedTickablesDirty = false; + } + return _fixedTickableSnapshot; + } + + private IServiceGizmoDrawable[] GetGizmoSnapshot() + { + if (_gizmoDrawablesDirty) + { + _gizmoDrawables.Sort(CompareByOrder); + _gizmoSnapshot = _gizmoDrawables.Count > 0 ? _gizmoDrawables.ToArray() : Array.Empty(); + _gizmoDrawablesDirty = false; + } + return _gizmoSnapshot; + } + + private static int CompareByOrder(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); + } + } +} diff --git a/Runtime/ABase/Base/Service/Core/ServiceScope.cs.meta b/Runtime/ABase/Base/Service/Core/ServiceScope.cs.meta new file mode 100644 index 0000000..d82248e --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/ServiceScope.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 90a7d784a3aff2f4f832fa67901e6931 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Core/ServiceWorld.cs b/Runtime/ABase/Base/Service/Core/ServiceWorld.cs new file mode 100644 index 0000000..b0afe5c --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/ServiceWorld.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; + +namespace AlicizaX +{ + public sealed class ServiceWorld : IDisposable + { + private readonly List _scopes = new List(); + private readonly Dictionary _scopesByType = new Dictionary(); + + private ServiceScope[] _scopeSnapshot = Array.Empty(); + private bool _scopesDirty; + + public ServiceWorld(int appScopeOrder = -10000) + { + AppScope = CreateScopeInternal(typeof(AppScope), appScopeOrder); + } + + public ServiceScope AppScope { get; } + + // ── Scope 管理(Type-based) ──────────────────────────────────────────── + + public ServiceScope CreateScope(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(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(out ServiceScope scope) where TScope : class + => _scopesByType.TryGetValue(typeof(TScope), out scope); + + public ServiceScope GetScope() where TScope : class + { + if (TryGetScope(out var scope)) return scope; + throw new InvalidOperationException($"Scope {typeof(TScope).Name} does not exist."); + } + + public bool DestroyScope() 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(out T service) where T : class, IService + => TryGet(null, out service); + + public bool TryGet(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() where T : class, IService => Require(null); + + public T Require(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(); + } + + // ── 内部 ──────────────────────────────────────────────────────────────── + + 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(); + _scopesDirty = false; + } + return _scopeSnapshot; + } + + private static int CompareScopeOrder(ServiceScope left, ServiceScope right) + => left.Order.CompareTo(right.Order); + } +} diff --git a/Runtime/ABase/Base/Service/Core/ServiceWorld.cs.meta b/Runtime/ABase/Base/Service/Core/ServiceWorld.cs.meta new file mode 100644 index 0000000..74c2ef3 --- /dev/null +++ b/Runtime/ABase/Base/Service/Core/ServiceWorld.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d48a8032734a55647a565ee72471c5c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Unity.meta b/Runtime/ABase/Base/Service/Unity.meta new file mode 100644 index 0000000..b0c44f0 --- /dev/null +++ b/Runtime/ABase/Base/Service/Unity.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c69106e6f9633554ea75fbc122821e2a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Unity/AppServiceRoot.cs b/Runtime/ABase/Base/Service/Unity/AppServiceRoot.cs new file mode 100644 index 0000000..d25897e --- /dev/null +++ b/Runtime/ABase/Base/Service/Unity/AppServiceRoot.cs @@ -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) { } + } +} diff --git a/Runtime/ABase/Base/Service/Unity/AppServiceRoot.cs.meta b/Runtime/ABase/Base/Service/Unity/AppServiceRoot.cs.meta new file mode 100644 index 0000000..936e56f --- /dev/null +++ b/Runtime/ABase/Base/Service/Unity/AppServiceRoot.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f1e04080395e4e649a43e98b673c5911 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/Base/Service/Unity/MonoServiceBehaviour.cs b/Runtime/ABase/Base/Service/Unity/MonoServiceBehaviour.cs new file mode 100644 index 0000000..6d4e339 --- /dev/null +++ b/Runtime/ABase/Base/Service/Unity/MonoServiceBehaviour.cs @@ -0,0 +1,99 @@ +using UnityEngine; + +namespace AlicizaX +{ + /// + /// Mono 服务基类(不自动注册,适合需要手动控制注册时机的场景)。 + /// + 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() { } + } + + /// + /// Mono 服务基类(自动注册到 )。 + /// + /// 场景服务:_dontDestroyOnLoad = false(默认),销毁时自动注销。
+ /// 跨场景服务:_dontDestroyOnLoad = true,首个实例持久化并注册; + /// 后续场景中出现的重复实例自动销毁自身。 + ///
+ /// + /// 子类通过 执行额外的 Awake 逻辑, + /// 通过 执行注册后的初始化, + /// 通过 执行注销前的清理。 + /// + ///
+ public abstract class MonoServiceBehaviour : 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(); + + // 跨场景重复实例检测:契约已被占用则销毁自身 + if (scope.HasContract(GetType())) + { + Destroy(gameObject); + return; + } + + scope.Register(this); + } + + private void OnDestroy() + { + if (!IsInitialized) return; + if (!AppServices.HasWorld) return; + if (!AppServices.TryGetScope(out var scope)) return; + + scope.Unregister(this); + } + + /// + /// 在 Awake 阶段执行(早于 Start 中的自动注册)。 + /// 适合缓存组件引用等不依赖服务系统的初始化。 + /// + protected virtual void OnAwake() { } + } +} diff --git a/Runtime/ABase/Base/Service/Unity/MonoServiceBehaviour.cs.meta b/Runtime/ABase/Base/Service/Unity/MonoServiceBehaviour.cs.meta new file mode 100644 index 0000000..a16bb63 --- /dev/null +++ b/Runtime/ABase/Base/Service/Unity/MonoServiceBehaviour.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af63e3465b03eaf4ca3a82aebcab6e26 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/GameObjectPool/Data.meta b/Runtime/ABase/GameObjectPool/Data.meta new file mode 100644 index 0000000..624348b --- /dev/null +++ b/Runtime/ABase/GameObjectPool/Data.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fe4fa3a609da4cd188473e4d4914897c +timeCreated: 1774439835 \ No newline at end of file diff --git a/Runtime/ABase/GameObjectPool/Data/PoolConfig.cs b/Runtime/ABase/GameObjectPool/Data/PoolConfig.cs new file mode 100644 index 0000000..5962f07 --- /dev/null +++ b/Runtime/ABase/GameObjectPool/Data/PoolConfig.cs @@ -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 + } + + /// + /// 对象池配置项。 + /// + [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('\\', '/'); + } + } + +} diff --git a/Runtime/ABase/GameObjectPool/Data/PoolConfig.cs.meta b/Runtime/ABase/GameObjectPool/Data/PoolConfig.cs.meta new file mode 100644 index 0000000..917e19f --- /dev/null +++ b/Runtime/ABase/GameObjectPool/Data/PoolConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7de4b0d73b4145b6bd836ec9d2c832d9 +timeCreated: 1774439841 \ No newline at end of file diff --git a/Runtime/ABase/GameObjectPool/Data/PoolConfigScriptableObject.cs b/Runtime/ABase/GameObjectPool/Data/PoolConfigScriptableObject.cs new file mode 100644 index 0000000..f46a03e --- /dev/null +++ b/Runtime/ABase/GameObjectPool/Data/PoolConfigScriptableObject.cs @@ -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 configs = new List(); + + public void Normalize() + { + if (configs == null) + { + configs = new List(); + return; + } + + for (int i = 0; i < configs.Count; i++) + { + configs[i]?.Normalize(); + } + } + +#if UNITY_EDITOR + private void OnValidate() + { + Normalize(); + } +#endif + } + +} diff --git a/Runtime/ABase/GameObjectPool/PoolConfigScriptableObject.cs.meta b/Runtime/ABase/GameObjectPool/Data/PoolConfigScriptableObject.cs.meta similarity index 100% rename from Runtime/ABase/GameObjectPool/PoolConfigScriptableObject.cs.meta rename to Runtime/ABase/GameObjectPool/Data/PoolConfigScriptableObject.cs.meta diff --git a/Runtime/ABase/GameObjectPool/GameObjectPool.cs b/Runtime/ABase/GameObjectPool/GameObjectPool.cs deleted file mode 100644 index c78be3e..0000000 --- a/Runtime/ABase/GameObjectPool/GameObjectPool.cs +++ /dev/null @@ -1,1369 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading; -using AlicizaX; -using AlicizaX.Resource.Runtime; -using Cysharp.Threading.Tasks; -using UnityEngine; - - -namespace AlicizaX -{ - /// - /// 对象池配置项。 - /// - [Serializable] - public class PoolConfig - { - public string asset; - public float time; - public int poolCount; - } - - /// - /// 预制体引用计数信息。 - /// - public class PrefabRefInfo - { - public GameObject Prefab; - public int RefCount; - public float LastAccessTime; - public string AssetPath; - - public PrefabRefInfo(GameObject prefab, string assetPath) - { - this.Prefab = prefab; - this.AssetPath = assetPath; - this.RefCount = 0; - this.LastAccessTime = Time.time; - } - - public void AddRef() - { - RefCount++; - LastAccessTime = Time.time; - } - - public void RemoveRef() - { - if (RefCount > 0) // 防止引用计数变成负数 - { - RefCount--; - if (RefCount > 0) - { - LastAccessTime = Time.time; - } - - Log.Info($"RemoveRef: {AssetPath}, refCount: {RefCount}"); - } - else - { - Log.Warning($"尝试减少已经为0的引用计数: {AssetPath}"); - } - } - - public bool CanUnload(float expireTime) - { - return RefCount <= 0 && expireTime > 0 && (Time.time - LastAccessTime) > expireTime; - } - } - - [Serializable] - public class PooledObject : IMemory - { - public GameObject gameObject; - public string assetPath; - public float lastUsedTime; - public bool isActive; - public string instanceName; - public bool isRefCountReduced; - [NonSerialized] public PoolObjectMonitor monitor; - - public PooledObject() - { - } - - public PooledObject(GameObject go, string path) - { - gameObject = go; - assetPath = path; - lastUsedTime = Time.time; - isActive = false; - instanceName = go.name; - isRefCountReduced = false; - } - - public void Clear() - { - gameObject = null; - assetPath = null; - lastUsedTime = 0f; - isActive = false; - instanceName = null; - isRefCountReduced = false; - monitor = null; - } - - /// - /// 获取过期进度 (0-1),1表示即将过期。 - /// - public float GetExpireProgress(float expireTime) - { - if (expireTime <= 0 || isActive) return 0f; - float timeElapsed = Time.time - lastUsedTime; - return Mathf.Clamp01(timeElapsed / expireTime); - } - - /// - /// 获取剩余时间。 - /// - public float GetRemainingTime(float expireTime) - { - if (expireTime <= 0 || isActive) return -1f; - float timeElapsed = Time.time - lastUsedTime; - return Mathf.Max(0f, expireTime - timeElapsed); - } - } - - /// - /// Inspector显示用的对象信息。 - /// - [Serializable] - public class PoolObjectInfo : IMemory - { - [SerializeField] public string objectName; - [SerializeField] public string assetPath; - [SerializeField] public bool isActive; - [SerializeField] public float lastUsedTime; - [SerializeField] public float remainingTime; - [SerializeField] public float expireProgress; - [SerializeField] public GameObject gameObject; - - public void UpdateFromPooledObject(PooledObject pooledObj, float expireTime) - { - objectName = pooledObj.instanceName; - assetPath = pooledObj.assetPath; - isActive = pooledObj.isActive; - lastUsedTime = pooledObj.lastUsedTime; - remainingTime = pooledObj.GetRemainingTime(expireTime); - expireProgress = pooledObj.GetExpireProgress(expireTime); - gameObject = pooledObj.gameObject; - } - - public void Clear() - { - objectName = null; - assetPath = null; - isActive = false; - lastUsedTime = 0f; - remainingTime = 0f; - expireProgress = 0f; - gameObject = null; - } - } - - /// - /// Inspector显示用的预制体信息. - /// - [Serializable] - public class PrefabRefInfoDisplay : IMemory - { - [SerializeField] public string assetPath; - [SerializeField] public int refCount; - [SerializeField] public float lastAccessTime; - [SerializeField] public GameObject prefab; - - public void UpdateFromPrefabRefInfo(PrefabRefInfo info) - { - assetPath = info.AssetPath; - refCount = info.RefCount; - lastAccessTime = info.LastAccessTime; - prefab = info.Prefab; - } - - public void Clear() - { - assetPath = null; - refCount = 0; - lastAccessTime = 0f; - prefab = null; - } - } - - /// - /// Inspector显示用的池信息。 - /// - [Serializable] - public class ConfigPoolInfo : IMemory - { - [SerializeField] public string configAsset; - [SerializeField] public int maxCount; - [SerializeField] public float expireTime; - [SerializeField] public int totalObjects; - [SerializeField] public int activeObjects; - [SerializeField] public int availableObjects; - [SerializeField] public int loadedPrefabs; - [SerializeField] public List assetPaths = new List(); - [SerializeField] public List objects = new List(); - [SerializeField] public List prefabRefs = new List(); - - public void UpdateFromPool(ConfigPool pool) - { - configAsset = pool.Config.asset; - maxCount = pool.Config.poolCount; - expireTime = pool.Config.time; - totalObjects = pool.AllObjects.Count; - - activeObjects = 0; - foreach (var obj in pool.AllObjects) - { - if (obj.isActive) activeObjects++; - } - - // 计算所有队列中的可用对象总数 - availableObjects = 0; - foreach (var queue in pool.AvailableObjectsByPath.Values) - { - availableObjects += queue.Count; - } - - loadedPrefabs = pool.LoadedPrefabs.Count; - - assetPaths.Clear(); - assetPaths.AddRange(pool.LoadedPrefabs.Keys); - - int objectIndex = 0; - foreach (var pooledObj in pool.AllObjects) - { - if (pooledObj.gameObject != null) - { - PoolObjectInfo info; - if (objectIndex < objects.Count) - { - info = objects[objectIndex]; - } - else - { - info = MemoryPool.Acquire(); - objects.Add(info); - } - - info.UpdateFromPooledObject(pooledObj, pool.Config.time); - objectIndex++; - } - } - - ReleasePoolObjectInfos(objectIndex); - - int prefabIndex = 0; - foreach (var kvp in pool.LoadedPrefabs) - { - PrefabRefInfoDisplay info; - if (prefabIndex < prefabRefs.Count) - { - info = prefabRefs[prefabIndex]; - } - else - { - info = MemoryPool.Acquire(); - prefabRefs.Add(info); - } - - info.UpdateFromPrefabRefInfo(kvp.Value); - prefabIndex++; - } - - ReleasePrefabRefInfos(prefabIndex); - } - - public void Clear() - { - configAsset = null; - maxCount = 0; - expireTime = 0f; - totalObjects = 0; - activeObjects = 0; - availableObjects = 0; - loadedPrefabs = 0; - assetPaths.Clear(); - ReleasePoolObjectInfos(0); - ReleasePrefabRefInfos(0); - } - - private void ReleasePoolObjectInfos(int keepCount) - { - while (objects.Count > keepCount) - { - int lastIndex = objects.Count - 1; - MemoryPool.Release(objects[lastIndex]); - objects.RemoveAt(lastIndex); - } - } - - private void ReleasePrefabRefInfos(int keepCount) - { - while (prefabRefs.Count > keepCount) - { - int lastIndex = prefabRefs.Count - 1; - MemoryPool.Release(prefabRefs[lastIndex]); - prefabRefs.RemoveAt(lastIndex); - } - } - } - - /// - /// 配置组对象池 - 管理一个PoolConfig下的所有资源。 - /// - public class ConfigPool - { - public readonly PoolConfig Config; - - // 按资源路径分组的可用对象队列 - public readonly Dictionary> AvailableObjectsByPath; - public readonly HashSet AllObjects; - public readonly Dictionary LoadedPrefabs; - public readonly Dictionary>> PendingRequests; - public readonly HashSet LoadingAssets; - public readonly Transform PoolRoot; - - private readonly IResourceLoader _resourceLoader; - - // GameObject到PooledObject的快速查找字典 - private readonly Dictionary _gameObjectToPooledObject; - private readonly List _availableObjectPaths; - - // 重用临时队列,避免重复创建。 - private static Queue _tempQueue = new Queue(); - - // 重用过期对象列表,避免重复创建。 - private static readonly List _expiredObjects = new List(); - private static readonly List _expiredPrefabs = new List(); - - public ConfigPool(PoolConfig config, IResourceLoader resourceLoader) - { - _resourceLoader = resourceLoader; - Config = config; - AvailableObjectsByPath = new Dictionary>(); - AllObjects = new HashSet(); - LoadedPrefabs = new Dictionary(); - PendingRequests = new Dictionary>>(); - LoadingAssets = new HashSet(); - _gameObjectToPooledObject = new Dictionary(); - _availableObjectPaths = new List(); - - // 创建池根节点。 - GameObject poolRootGo = new GameObject($"ConfigPool_{config.asset.Replace('/', '_')}"); - PoolRoot = poolRootGo.transform; - PoolRoot.SetParent(GameObjectPool.Instance.poolContainer); - poolRootGo.SetActive(false); - } - - public bool MatchesAsset(string assetPath) - { - return assetPath.StartsWith(Config.asset); - } - - /// - /// 同步获取对象,如果资源未加载则同步加载。 - /// - public GameObject Get(string assetPath) - { - if (!LoadedPrefabs.ContainsKey(assetPath)) - { - if (LoadingAssets.Contains(assetPath)) - { - Log.Warning($"资源 {assetPath} 正在异步加载中,同步获取可能导致重复加载,建议使用异步方法"); - } - - try - { - GameObject prefab = _resourceLoader.LoadPrefab(assetPath); - if (prefab != null) - { - LoadedPrefabs[assetPath] = new PrefabRefInfo(prefab, assetPath); - Log.Info($"同步加载资源成功: {assetPath}"); - } - else - { - Log.Error($"同步加载资源失败: {assetPath}"); - return null; - } - } - catch (Exception e) - { - Log.Error($"同步加载资源异常: {assetPath}, 错误: {e.Message}"); - return null; - } - } - - return GetInternal(assetPath); - } - - /// - /// 异步获取对象。 - /// - public async UniTask GetAsync(string assetPath, CancellationToken cancellationToken = default) - { - if (LoadedPrefabs.ContainsKey(assetPath)) - { - return GetInternal(assetPath); - } - - if (LoadingAssets.Contains(assetPath)) - { - var completionSource = new UniTaskCompletionSource(); - if (!PendingRequests.ContainsKey(assetPath)) - { - PendingRequests[assetPath] = new List>(); - } - - PendingRequests[assetPath].Add(completionSource); - - try - { - return await completionSource.Task.AttachExternalCancellation(cancellationToken); - } - catch (OperationCanceledException) - { - PendingRequests[assetPath].Remove(completionSource); - throw; - } - } - - LoadingAssets.Add(assetPath); - try - { - GameObject prefab = await _resourceLoader.LoadPrefabAsync(assetPath, cancellationToken); - if (prefab != null) - { - LoadedPrefabs[assetPath] = new PrefabRefInfo(prefab, assetPath); - Log.Info($"异步加载资源成功: {assetPath}"); - - if (PendingRequests.ContainsKey(assetPath)) - { - var requests = PendingRequests[assetPath]; - PendingRequests.Remove(assetPath); - - foreach (var request in requests) - { - try - { - var go = GetInternal(assetPath); - request.TrySetResult(go); - } - catch (Exception e) - { - request.TrySetException(e); - } - } - } - - return GetInternal(assetPath); - } - else - { - throw new Exception($"无法异步加载资源: {assetPath}"); - } - } - catch (Exception e) - { - Log.Error($"异步加载资源失败: {assetPath}, 错误: {e.Message}"); - - if (PendingRequests.ContainsKey(assetPath)) - { - var requests = PendingRequests[assetPath]; - PendingRequests.Remove(assetPath); - - foreach (var request in requests) - { - request.TrySetException(e); - } - } - - throw; - } - finally - { - LoadingAssets.Remove(assetPath); - } - } - - /// - /// 创建新的池对象。 - /// - private PooledObject CreatePooledObject(string assetPath) - { - var prefabRefInfo = LoadedPrefabs[assetPath]; - GameObject instantiate = GameObject.Instantiate(prefabRefInfo.Prefab); - - var pooledObj = MemoryPool.Acquire(); - pooledObj.gameObject = instantiate; - pooledObj.assetPath = assetPath; - pooledObj.lastUsedTime = Time.time; - pooledObj.isActive = false; - pooledObj.instanceName = instantiate.name; - pooledObj.isRefCountReduced = false; - - AllObjects.Add(pooledObj); - _gameObjectToPooledObject[instantiate] = pooledObj; - - prefabRefInfo.AddRef(); - - var monitor = instantiate.GetComponent(); - if (monitor == null) - { - monitor = instantiate.AddComponent(); - } - - monitor.Initialize(this, pooledObj); - pooledObj.monitor = monitor; - return pooledObj; - } - - private GameObject GetInternal(string assetPath) - { - PooledObject pooledObj = null; - - // 从按路径分组的队列中获取 - if (AvailableObjectsByPath.TryGetValue(assetPath, out var queue) && queue.Count > 0) - { - // 清理已销毁的对象 - while (queue.Count > 0) - { - var obj = queue.Dequeue(); - if (obj.gameObject == null) - { - // 只有在引用计数未减少时才处理 - if (!obj.isRefCountReduced) - { - OnObjectReallyDestroyed(obj); - } - else - { - // 只需要从集合中移除,不需要减少引用计数 - AllObjects.Remove(obj); - } - - continue; - } - - pooledObj = obj; - break; - } - } - - if (pooledObj == null) - { - if (AllObjects.Count < Config.poolCount) - { - pooledObj = CreatePooledObject(assetPath); - } - else - { - // 使用LINQ找到最旧的未使用对象(更高效) - PooledObject oldestObj = null; - foreach (var obj in AllObjects) - { - if (obj.isActive) - { - continue; - } - - if (oldestObj == null || obj.lastUsedTime < oldestObj.lastUsedTime) - { - oldestObj = obj; - } - } - - if (oldestObj != null) - { - DestroyPooledObject(oldestObj); - pooledObj = CreatePooledObject(assetPath); - } - else - { - Log.Warning($"对象池已满且所有对象都在使用中: {Config.asset},无法创建新对象 {assetPath}"); - return null; - } - } - } - - pooledObj.isActive = true; - pooledObj.lastUsedTime = Time.time; - pooledObj.gameObject.SetActive(true); - - return pooledObj.gameObject; - } - - public void Return(GameObject go) - { - if (!_gameObjectToPooledObject.TryGetValue(go, out var pooledObj)) - { - return; - } - - if (pooledObj != null && pooledObj.isActive) - { - pooledObj.isActive = false; - pooledObj.lastUsedTime = Time.time; - - go.SetActive(false); - go.transform.SetParent(PoolRoot); - go.transform.localPosition = Vector3.zero; - go.transform.localRotation = Quaternion.identity; - go.transform.localScale = Vector3.one; - - // 放入按路径分组的队列 - if (!AvailableObjectsByPath.TryGetValue(pooledObj.assetPath, out var queue)) - { - queue = new Queue(); - AvailableObjectsByPath[pooledObj.assetPath] = queue; - } - - queue.Enqueue(pooledObj); - } - } - - public void OnObjectDestroyed(PooledObject pooledObj) - { - // 从GameObjectPool的字典中移除,防止内存泄漏 - if (pooledObj.gameObject != null) - { - GameObjectPool.Instance.RemoveGameObjectReference(pooledObj.gameObject); - } - - // 防止重复减少引用计数 - if (!pooledObj.isRefCountReduced) - { - OnObjectReallyDestroyed(pooledObj); - MemoryPool.Release(pooledObj); - } - else - { - // DestroyPooledObject 已调用 Detach,此分支理论上不可达 - AllObjects.Remove(pooledObj); - CleanAvailableQueue(pooledObj); - } - } - - private void OnObjectReallyDestroyed(PooledObject pooledObj) - { - // 标记引用计数已减少,防止重复处理 - if (pooledObj.isRefCountReduced) - { - return; - } - - pooledObj.isRefCountReduced = true; - AllObjects.Remove(pooledObj); - - // 从快速查找字典中移除 - if (pooledObj.gameObject != null) - { - _gameObjectToPooledObject.Remove(pooledObj.gameObject); - } - - // 减少预制体引用计数 - if (LoadedPrefabs.TryGetValue(pooledObj.assetPath, out PrefabRefInfo refInfo)) - { - refInfo.RemoveRef(); - } - - CleanAvailableQueue(pooledObj); - } - - // 清理可用队列 - private void CleanAvailableQueue(PooledObject pooledObj) - { - // 从对应路径的队列中移除 - if (AvailableObjectsByPath.TryGetValue(pooledObj.assetPath, out var queue)) - { - _tempQueue.Clear(); - while (queue.Count > 0) - { - var obj = queue.Dequeue(); - if (obj != pooledObj) - { - _tempQueue.Enqueue(obj); - } - } - - // 交换队列 - (queue, _tempQueue) = (_tempQueue, queue); - - // 如果队列为空,从字典中移除 - if (queue.Count == 0) - { - AvailableObjectsByPath.Remove(pooledObj.assetPath); - } - } - } - - private void DestroyPooledObject(PooledObject pooledObj) - { - // 先标记引用计数已减少 - if (pooledObj.isRefCountReduced) - { - return; - } - - // 先处理引用计数 - OnObjectReallyDestroyed(pooledObj); - - if (pooledObj.gameObject != null) - { - pooledObj.monitor?.Detach(); - GameObject.Destroy(pooledObj.gameObject); - } - - MemoryPool.Release(pooledObj); - } - - public void CheckExpiredObjects() - { - if (Config.time <= 0) return; - - float currentTime = Time.time; - - // 重用过期对象列表 - _expiredObjects.Clear(); - - foreach (var obj in AllObjects) - { - if (!obj.isActive && !obj.isRefCountReduced && (currentTime - obj.lastUsedTime) > Config.time) - { - _expiredObjects.Add(obj); - } - } - - foreach (var expiredObj in _expiredObjects) - { - DestroyPooledObject(expiredObj); - } - - // 重建所有路径的可用队列 - _availableObjectPaths.Clear(); - foreach (var assetPath in AvailableObjectsByPath.Keys) - { - _availableObjectPaths.Add(assetPath); - } - - foreach (var assetPath in _availableObjectPaths) - { - if (!AvailableObjectsByPath.TryGetValue(assetPath, out var queue)) - { - continue; - } - - _tempQueue.Clear(); - while (queue.Count > 0) - { - var obj = queue.Dequeue(); - if (AllObjects.Contains(obj) && !obj.isRefCountReduced) - { - _tempQueue.Enqueue(obj); - } - } - - if (_tempQueue.Count > 0) - { - // 交换队列 - (queue, _tempQueue) = (_tempQueue, queue); - } - else - { - // 队列为空,从字典中移除 - AvailableObjectsByPath.Remove(assetPath); - } - } - - CheckExpiredPrefabs(); - } - - private void CheckExpiredPrefabs() - { - if (Config.time <= 0) return; - - // 重用过期预制体列表 - _expiredPrefabs.Clear(); - - foreach (var kvp in LoadedPrefabs) - { - var refInfo = kvp.Value; - if (refInfo.CanUnload(Config.time)) - { - _expiredPrefabs.Add(kvp.Key); - } - } - - foreach (var assetPath in _expiredPrefabs) - { - var refInfo = LoadedPrefabs[assetPath]; - Log.Info($"卸载过期预制体: {assetPath}, 引用计数: {refInfo.RefCount}"); - - _resourceLoader.UnloadAsset(refInfo.Prefab); - LoadedPrefabs.Remove(assetPath); - } - } - - public void Clear() - { - foreach (var obj in AllObjects) - { - if (obj.gameObject != null) - { - obj.monitor?.Detach(); - GameObject.Destroy(obj.gameObject); - } - - MemoryPool.Release(obj); - } - - AllObjects.Clear(); - AvailableObjectsByPath.Clear(); - _gameObjectToPooledObject.Clear(); - - foreach (var kvp in LoadedPrefabs) - { - var refInfo = kvp.Value; - if (refInfo.Prefab != null) - { - Log.Info($"清理时卸载预制体: {kvp.Key}, 引用计数: {refInfo.RefCount}"); - _resourceLoader.UnloadAsset(refInfo.Prefab); - } - } - - LoadedPrefabs.Clear(); - LoadingAssets.Clear(); - - foreach (var requests in PendingRequests.Values) - { - foreach (var request in requests) - { - request.TrySetCanceled(); - } - } - - PendingRequests.Clear(); - - if (PoolRoot != null) - { - GameObject.Destroy(PoolRoot.gameObject); - } - } - } - - /// - /// 对象销毁监听器。 - /// - public class PoolObjectMonitor : MonoBehaviour - { - private ConfigPool _pool; - private PooledObject _pooledObject; - - public void Initialize(ConfigPool pool, PooledObject pooledObject) - { - _pool = pool; - _pooledObject = pooledObject; - } - - public void Detach() - { - _pool = null; - _pooledObject = null; - } - - private void OnDestroy() - { - if (_pool != null && _pooledObject != null) - { - _pool.OnObjectDestroyed(_pooledObject); - } - } - } - - /// - /// 游戏对象池管理器。 - /// - public class GameObjectPool : MonoBehaviour - { - private static GameObjectPool _instance; - - public static GameObjectPool Instance - { - get - { - if (_instance == null) - { - GameObject go = new GameObject("[GameObjectPool]"); - _instance = go.AddComponent(); - DontDestroyOnLoad(go); - } - - return _instance; - } - } - - [Header("检查间隔")] public float checkInterval = 10f; - - [Header("资源加载器")] public bool useEngineResourceLoader = true; - - [Header("配置路径")] public string poolConfigPath = "Assets/Bundles/Configs/ScriptableObject/PoolConfig"; - - [Header("Inspector显示设置")] public bool showDetailedInfo = true; - - [Header("池状态信息")] [SerializeField] private List poolInfos = new List(); - - public Transform poolContainer; - internal IResourceLoader _resourceLoader; - - private List _poolConfigs; - private List _configPools; - private Dictionary _gameObjectToPool; - private Dictionary _configPoolCache; - - // 重用预加载对象列表 - private static readonly List _preloadedObjects = new List(); - - private float _lastCleanupTime; - - private void Awake() - { - if (_instance == null) - { - _instance = this; - DontDestroyOnLoad(gameObject); - Initialize(); - } - else if (_instance != this) - { - Destroy(gameObject); - } - } - - private void Initialize() - { - _resourceLoader = useEngineResourceLoader ? new AlicizaResourceLoader() as IResourceLoader : new DefaultResourceLoader() as IResourceLoader; - - GameObject containerGo = new GameObject("PoolContainer"); - poolContainer = containerGo.transform; - poolContainer.SetParent(transform); - - _configPools = new List(); - _gameObjectToPool = new Dictionary(); - _configPoolCache = new Dictionary(); - - try - { - _poolConfigs = ModuleSystem.GetModule().LoadAsset(poolConfigPath).configs; - _poolConfigs.Sort((a, b) => b.asset.Length.CompareTo(a.asset.Length)); - - foreach (var config in _poolConfigs) - { - var configPool = new ConfigPool(config, _resourceLoader); - _configPools.Add(configPool); - } - } - catch (Exception e) - { - Log.Error($"加载对象池配置失败: {e.Message}"); - _poolConfigs = new List(); - } - - // 初始化清理时间 - _lastCleanupTime = Time.time; - } - - private void Update() - { - if (Time.time - _lastCleanupTime >= checkInterval) - { - PerformCleanup(); - _lastCleanupTime = Time.time; - } - } - - /// - /// 执行对象池清理。 - /// - private void PerformCleanup() - { - if (_configPools == null || _configPools.Count == 0) - { - return; - } - - foreach (var pool in _configPools) - { - pool.CheckExpiredObjects(); - } - } - - /// - /// 手动触发一次清理。 - /// - public void ForceCleanup() - { - PerformCleanup(); - _lastCleanupTime = Time.time; - } - - /// - /// 内部方法:从字典中移除GameObject引用,防止内存泄漏。 - /// - internal void RemoveGameObjectReference(GameObject go) - { - if (go != null) - { - _gameObjectToPool.Remove(go); - } - } - - // Editor专用的刷新。 - private void UpdateInspectorInfo() - { - if (_configPools == null) - { - ReleaseInspectorInfos(0); - return; - } - - int poolIndex = 0; - foreach (var pool in _configPools) - { - ConfigPoolInfo info; - if (poolIndex < poolInfos.Count) - { - info = poolInfos[poolIndex]; - } - else - { - info = MemoryPool.Acquire(); - poolInfos.Add(info); - } - - info.UpdateFromPool(pool); - poolIndex++; - } - - ReleaseInspectorInfos(poolIndex); - } - - public void SetResourceLoader(IResourceLoader resourceLoader) - { - _resourceLoader = resourceLoader; - } - - public GameObject GetGameObject(string assetPath, Transform parent = null) - { - ConfigPool pool = FindConfigPool(assetPath); - GameObject go = null; - - if (pool != null) - { - go = pool.Get(assetPath); - } - else - { - go = _resourceLoader.LoadGameObject(assetPath, parent); - } - - if (go != null && pool != null) - { - _gameObjectToPool[go] = pool; - go.transform.SetParent(parent); - } - - return go; - } - - public async UniTask GetGameObjectAsync(string assetPath, Transform parent = null, CancellationToken cancellationToken = default) - { - ConfigPool pool = FindConfigPool(assetPath); - GameObject go = null; - - if (pool != null) - { - go = await pool.GetAsync(assetPath, cancellationToken); - } - else - { - go = await _resourceLoader.LoadGameObjectAsync(assetPath, parent, cancellationToken); - } - - if (go != null && pool != null) - { - _gameObjectToPool[go] = pool; - go.transform.SetParent(parent); // 设置父节点 - } - - return go; - } - - public void Release(GameObject go) - { - if (go == null) return; - - if (_gameObjectToPool.TryGetValue(go, out ConfigPool pool)) - { - pool.Return(go); - _gameObjectToPool.Remove(go); - } - else - { - Destroy(go); - } - } - - public async UniTask PreloadAsync(string assetPath, int count = 1, CancellationToken cancellationToken = default) - { - ConfigPool pool = FindConfigPool(assetPath); - if (pool == null) - { - Log.Warning($"资源 {assetPath} 没有对应的池配置,无法预加载"); - return; - } - - // 优化:重用预加载对象列表 - _preloadedObjects.Clear(); - for (int i = 0; i < count; i++) - { - GameObject go = await pool.GetAsync(assetPath, cancellationToken); - if (go != null) - { - _preloadedObjects.Add(go); - } - } - - foreach (var go in _preloadedObjects) - { - pool.Return(go); - _gameObjectToPool.Remove(go); - } - } - - public void Preload(string assetPath, int count = 1) - { - ConfigPool pool = FindConfigPool(assetPath); - if (pool == null) - { - Log.Warning($"资源 {assetPath} 没有对应的池配置,无法预加载"); - return; - } - - // 优化:重用预加载对象列表 - _preloadedObjects.Clear(); - for (int i = 0; i < count; i++) - { - GameObject go = pool.Get(assetPath); - if (go != null) - { - _preloadedObjects.Add(go); - } - } - - foreach (var go in _preloadedObjects) - { - pool.Return(go); - _gameObjectToPool.Remove(go); - } - } - - private ConfigPool FindConfigPool(string assetPath) - { - if (string.IsNullOrEmpty(assetPath)) - { - return null; - } - - if (_configPools == null) - { - return null; - } - - if (_configPoolCache == null) - { - _configPoolCache = new Dictionary(); - } - - if (_configPoolCache != null && _configPoolCache.TryGetValue(assetPath, out var cachedPool)) - { - return cachedPool; - } - - foreach (var pool in _configPools) - { - if (pool.MatchesAsset(assetPath)) - { - _configPoolCache[assetPath] = pool; - return pool; - } - } - - _configPoolCache[assetPath] = null; - return null; - } - - /// - /// 手动刷新Inspector信息 - /// - public void RefreshInspectorInfo() - { - UpdateInspectorInfo(); - } - - public void ClearAllPools() - { - if (_configPools != null) - { - foreach (var pool in _configPools) - { - pool.Clear(); - } - } - - if (_gameObjectToPool != null) - { - _gameObjectToPool.Clear(); - } - - if (_configPoolCache != null) - { - _configPoolCache.Clear(); - } - - ReleaseInspectorInfos(0); - } - - private void ReleaseInspectorInfos(int keepCount) - { - while (poolInfos.Count > keepCount) - { - int lastIndex = poolInfos.Count - 1; - MemoryPool.Release(poolInfos[lastIndex]); - poolInfos.RemoveAt(lastIndex); - } - } - - private void OnDestroy() - { - ClearAllPools(); - } - } - - public interface IResourceLoader - { - GameObject LoadPrefab(string location); - UniTask LoadPrefabAsync(string location, CancellationToken cancellationToken = default); - GameObject LoadGameObject(string location, Transform parent = null); - UniTask LoadGameObjectAsync(string location, Transform parent = null, CancellationToken cancellationToken = default); - void UnloadAsset(GameObject gameObject); - } - - public class DefaultResourceLoader : IResourceLoader - { - public GameObject LoadPrefab(string location) - { - return Resources.Load(location); - } - - public async UniTask LoadPrefabAsync(string location, CancellationToken cancellationToken = default) - { - return await Resources.LoadAsync(location).ToUniTask(cancellationToken: cancellationToken) as GameObject; - } - - public GameObject LoadGameObject(string location, Transform parent = null) - { - var prefab = Resources.Load(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 LoadGameObjectAsync(string location, Transform parent = null, CancellationToken cancellationToken = default) - { - var prefab = await Resources.LoadAsync(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; - - private void CheckInit() - { - if (_resourceModule == null) - { - _resourceModule = ModuleSystem.GetModule(); - } - } - - public GameObject LoadPrefab(string location) - { - CheckInit(); - return _resourceModule.LoadAsset(location); - } - - public async UniTask LoadPrefabAsync(string location, CancellationToken cancellationToken = default) - { - CheckInit(); - return await _resourceModule.LoadAssetAsync(location, cancellationToken); - } - - public GameObject LoadGameObject(string location, Transform parent = null) - { - CheckInit(); - return _resourceModule.LoadGameObject(location, parent); - } - - public async UniTask LoadGameObjectAsync(string location, Transform parent = null, CancellationToken cancellationToken = default) - { - CheckInit(); - return await _resourceModule.LoadGameObjectAsync(location, parent, cancellationToken); - } - - public void UnloadAsset(GameObject gameObject) - { - CheckInit(); - _resourceModule.UnloadAsset(gameObject); - } - } - - public static class GameObjectPoolUtil - { - public static GameObject LoadGameObject(string assetPath, Transform parent = null) - { - return GameObjectPool.Instance.GetGameObject(assetPath, parent); - } - - public static async UniTask LoadGameObjectAsync(string assetPath, Transform parent = null, CancellationToken cancellationToken = default) - { - return await GameObjectPool.Instance.GetGameObjectAsync(assetPath, parent, cancellationToken); - } - - public static void Release(GameObject go) - { - GameObjectPool.Instance.Release(go); - } - } -} diff --git a/Runtime/ABase/GameObjectPool/GameObjectPoolEditor.cs b/Runtime/ABase/GameObjectPool/GameObjectPoolEditor.cs deleted file mode 100644 index 9e69cc4..0000000 --- a/Runtime/ABase/GameObjectPool/GameObjectPoolEditor.cs +++ /dev/null @@ -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 diff --git a/Runtime/ABase/GameObjectPool/GameObjectPoolEditor.cs.meta b/Runtime/ABase/GameObjectPool/GameObjectPoolEditor.cs.meta deleted file mode 100644 index 247e064..0000000 --- a/Runtime/ABase/GameObjectPool/GameObjectPoolEditor.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: df436879f8854a95b5a92e8b77772189 -timeCreated: 1773109368 \ No newline at end of file diff --git a/Runtime/ABase/GameObjectPool/GameObjectPoolService.cs b/Runtime/ABase/GameObjectPool/GameObjectPoolService.cs new file mode 100644 index 0000000..42fce8c --- /dev/null +++ b/Runtime/ABase/GameObjectPool/GameObjectPoolService.cs @@ -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 + { + [Header("检查间隔")] + public float checkInterval = 10f; + + [Header("配置路径")] + public string poolConfigPath = "Assets/Bundles/Configs/PoolConfig"; + + [Header("Inspector显示设置")] + public bool showDetailedInfo = true; + + [SerializeField] + internal Transform poolContainer; + + private const PoolResourceLoaderType DefaultDirectLoadResourceLoaderType = PoolResourceLoaderType.AssetBundle; + + private readonly Dictionary _poolsByKey = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _resolvedConfigCache = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _ownersByObject = new Dictionary(); + private readonly Dictionary _resourceLoaders = new Dictionary(); + private readonly List _configs = new List(); + private readonly List _debugSnapshots = new List(); + + private CancellationTokenSource _shutdownTokenSource; + private UniTask _initializeTask; + private bool _initializationCompleted; + private Exception _initializationException; + private float _lastCleanupTime; + + public bool IsReady => _initializationCompleted && _initializationException == null; + + protected override void OnServiceInitialize() + { + _shutdownTokenSource = new CancellationTokenSource(); + EnsureDefaultResourceLoaders(); + EnsurePoolContainer(); + _initializeTask = InitializeAsync(_shutdownTokenSource.Token); + } + + protected override void OnServiceDestroy() + { + _shutdownTokenSource?.Cancel(); + ClearAllPools(); + + if (poolContainer != null) + { + Destroy(poolContainer.gameObject); + poolContainer = null; + } + + _shutdownTokenSource?.Dispose(); + _shutdownTokenSource = null; + } + + private async UniTask InitializeAsync(CancellationToken cancellationToken) + { + try + { + await UniTask.WaitUntil(() => YooAsset.YooAssets.Initialized, cancellationToken: cancellationToken); + LoadConfigs(); + _lastCleanupTime = Time.time; + _initializationCompleted = true; + await PrewarmConfiguredPoolsAsync(cancellationToken); + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + _initializationException = exception; + _initializationCompleted = true; + Log.Error($"GameObjectPool initialization failed: {exception}"); + } + } + + private void Update() + { + if (!IsReady) + { + return; + } + + if (Time.time - _lastCleanupTime < checkInterval) + { + return; + } + + PerformCleanup(); + _lastCleanupTime = Time.time; + } + + public void SetResourceLoader(IResourceLoader resourceLoader) + { + SetResourceLoader(PoolResourceLoaderType.AssetBundle, resourceLoader); + } + + public void SetResourceLoader(PoolResourceLoaderType loaderType, IResourceLoader resourceLoader) + { + if (resourceLoader == null) + { + throw new ArgumentNullException(nameof(resourceLoader)); + } + + _resourceLoaders[loaderType] = resourceLoader; + } + + public GameObject GetGameObject(string assetPath, Transform parent = null) + { + EnsureReadyForSyncUse(); + return GetGameObjectInternal(PoolConfig.NormalizeAssetPath(assetPath), null, parent); + } + + public GameObject GetGameObjectByGroup(string group, string assetPath, Transform parent = null) + { + EnsureReadyForSyncUse(); + return GetGameObjectInternal(PoolConfig.NormalizeAssetPath(assetPath), group, parent); + } + + public async UniTask GetGameObjectAsync( + string assetPath, + Transform parent = null, + CancellationToken cancellationToken = default) + { + await EnsureInitializedAsync(cancellationToken); + return await GetGameObjectInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), null, parent, cancellationToken); + } + + public async UniTask GetGameObjectAsyncByGroup( + string group, + string assetPath, + Transform parent = null, + CancellationToken cancellationToken = default) + { + await EnsureInitializedAsync(cancellationToken); + return await GetGameObjectInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), group, parent, cancellationToken); + } + + public void Release(GameObject gameObject) + { + if (gameObject == null) + { + return; + } + + if (_ownersByObject.TryGetValue(gameObject, out RuntimePrefabPool pool)) + { + pool.Release(gameObject); + return; + } + + Log.Warning($"Trying to release untracked GameObject '{gameObject.name}'. Destroying it."); + Destroy(gameObject); + } + + public void Preload(string assetPath, int count = 1) + { + EnsureReadyForSyncUse(); + PreloadInternal(PoolConfig.NormalizeAssetPath(assetPath), null, count); + } + + public void PreloadByGroup(string group, string assetPath, int count = 1) + { + EnsureReadyForSyncUse(); + PreloadInternal(PoolConfig.NormalizeAssetPath(assetPath), group, count); + } + + public async UniTask PreloadAsync(string assetPath, int count = 1, CancellationToken cancellationToken = default) + { + await EnsureInitializedAsync(cancellationToken); + await PreloadInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), null, count, cancellationToken); + } + + public async UniTask PreloadAsyncByGroup( + string group, + string assetPath, + int count = 1, + CancellationToken cancellationToken = default) + { + await EnsureInitializedAsync(cancellationToken); + await PreloadInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), group, count, cancellationToken); + } + + public void ForceCleanup() + { + if (!IsReady) + { + return; + } + + PerformCleanup(); + _lastCleanupTime = Time.time; + } + + public void ClearAllPools() + { + foreach (RuntimePrefabPool pool in _poolsByKey.Values) + { + pool.Shutdown(); + MemoryPool.Release(pool); + } + + _poolsByKey.Clear(); + _ownersByObject.Clear(); + _resolvedConfigCache.Clear(); + ReleaseDebugSnapshots(); + } + + public List 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 GetGameObjectInternalAsync( + string assetPath, + string group, + Transform parent, + CancellationToken cancellationToken) + { + PoolConfig config = ResolveConfig(assetPath, group); + if (config == null) + { + return await LoadUnpooledAsync(assetPath, group, parent, cancellationToken); + } + + RuntimePrefabPool pool = GetOrCreatePool(config, assetPath); + return await pool.AcquireAsync(parent, cancellationToken); + } + + private void PreloadInternal(string assetPath, string group, int count) + { + if (count <= 0) + { + return; + } + + PoolConfig config = ResolveConfig(assetPath, group); + if (config == null) + { + Log.Warning($"Asset '{assetPath}' has no matching pool config. Preload skipped."); + return; + } + + RuntimePrefabPool pool = GetOrCreatePool(config, assetPath); + pool.Warmup(count); + } + + private async UniTask PreloadInternalAsync( + string assetPath, + string group, + int count, + CancellationToken cancellationToken) + { + if (count <= 0) + { + return; + } + + PoolConfig config = ResolveConfig(assetPath, group); + if (config == null) + { + Log.Warning($"Asset '{assetPath}' has no matching pool config. Preload skipped."); + return; + } + + RuntimePrefabPool pool = GetOrCreatePool(config, assetPath); + await pool.WarmupAsync(count, cancellationToken); + } + + private RuntimePrefabPool GetOrCreatePool(PoolConfig config, string assetPath) + { + EnsurePoolContainer(); + + string poolKey = config.BuildResolvedPoolKey(assetPath); + if (_poolsByKey.TryGetValue(poolKey, out RuntimePrefabPool existingPool)) + { + return existingPool; + } + + var pool = MemoryPool.Acquire(); + pool.Initialize( + config, + assetPath, + GetResourceLoader(config.resourceLoaderType), + this, + _shutdownTokenSource != null ? _shutdownTokenSource.Token : default); + + _poolsByKey.Add(poolKey, pool); + return pool; + } + + private void PerformCleanup() + { + float now = Time.time; + foreach (RuntimePrefabPool pool in _poolsByKey.Values) + { + pool.TrimExpiredInstances(now); + } + } + + private void EnsureDefaultResourceLoaders() + { + if (!_resourceLoaders.ContainsKey(PoolResourceLoaderType.AssetBundle)) + { + _resourceLoaders[PoolResourceLoaderType.AssetBundle] = new 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().LoadAsset(poolConfigPath); + + if (configAsset == null || configAsset.configs == null) + { + return; + } + + for (int i = 0; i < configAsset.configs.Count; i++) + { + PoolConfig config = configAsset.configs[i]; + if (config == null) + { + continue; + } + + config.Normalize(); + if (string.IsNullOrWhiteSpace(config.assetPath)) + { + Log.Warning($"PoolConfig at index {i} has an empty asset path and was ignored."); + continue; + } + + _configs.Add(config); + } + + _configs.Sort(PoolConfig.CompareByPriority); + LogConfigWarnings(); + } + + 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 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(); + } + } +} diff --git a/Runtime/ABase/GameObjectPool/GameObjectPoolService.cs.meta b/Runtime/ABase/GameObjectPool/GameObjectPoolService.cs.meta new file mode 100644 index 0000000..e6f01c2 --- /dev/null +++ b/Runtime/ABase/GameObjectPool/GameObjectPoolService.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 17b192955f7691b4e8283328c926ab1a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/GameObjectPool/IResourceLoader.cs b/Runtime/ABase/GameObjectPool/IResourceLoader.cs new file mode 100644 index 0000000..85a7d36 --- /dev/null +++ b/Runtime/ABase/GameObjectPool/IResourceLoader.cs @@ -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 LoadPrefabAsync(string location, CancellationToken cancellationToken = default); + GameObject LoadGameObject(string location, Transform parent = null); + UniTask 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(location); + } + + public async UniTask LoadPrefabAsync(string location, CancellationToken cancellationToken = default) + { + return await Resources.LoadAsync(location).ToUniTask(cancellationToken: cancellationToken) as GameObject; + } + + public GameObject LoadGameObject(string location, Transform parent = null) + { + var prefab = Resources.Load(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 LoadGameObjectAsync(string location, Transform parent = null, CancellationToken cancellationToken = default) + { + var prefab = await Resources.LoadAsync(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(); + } + + return _resourceModule; + } + } + + + public GameObject LoadPrefab(string location) + { + return ResourceModule.LoadAsset(location); + } + + public async UniTask LoadPrefabAsync(string location, CancellationToken cancellationToken = default) + { + return await ResourceModule.LoadAssetAsync(location, cancellationToken); + } + + public GameObject LoadGameObject(string location, Transform parent = null) + { + return ResourceModule.LoadGameObject(location, parent); + } + + public async UniTask LoadGameObjectAsync(string location, Transform parent = null, CancellationToken cancellationToken = default) + { + return await ResourceModule.LoadGameObjectAsync(location, parent, cancellationToken); + } + + public void UnloadAsset(GameObject gameObject) + { + ResourceModule.UnloadAsset(gameObject); + } + } +} diff --git a/Runtime/ABase/GameObjectPool/IResourceLoader.cs.meta b/Runtime/ABase/GameObjectPool/IResourceLoader.cs.meta new file mode 100644 index 0000000..7e42b61 --- /dev/null +++ b/Runtime/ABase/GameObjectPool/IResourceLoader.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9b48a0c8ea0e494f91746c5c40670feb +timeCreated: 1774439792 \ No newline at end of file diff --git a/Runtime/ABase/GameObjectPool/PoolConfigScriptableObject.cs b/Runtime/ABase/GameObjectPool/PoolConfigScriptableObject.cs deleted file mode 100644 index 7a00c9d..0000000 --- a/Runtime/ABase/GameObjectPool/PoolConfigScriptableObject.cs +++ /dev/null @@ -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 configs; - } - -} diff --git a/Runtime/ABase/GameObjectPool/RuntimePoolModels.cs b/Runtime/ABase/GameObjectPool/RuntimePoolModels.cs new file mode 100644 index 0000000..4b9da0a --- /dev/null +++ b/Runtime/ABase/GameObjectPool/RuntimePoolModels.cs @@ -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 instances = new List(); + + 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 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 _instancesByObject; + private LinkedList _inactiveInstances; + private Transform _root; + + private GameObject _prefab; + private UniTaskCompletionSource _prefabLoadSource; + private int _activeCount; + private float _lastPrefabTouchTime; + + public RuntimePrefabPool() + { + _instancesByObject = new Dictionary(); + _inactiveInstances = new LinkedList(); + } + + 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 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(); + 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(); + instanceSnapshot.instanceName = instance.GameObject != null ? instance.GameObject.name : ""; + 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(_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(); + + 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(); + if (marker == null) + { + marker = gameObject.AddComponent(); + } + + var instance = MemoryPool.Acquire(); + 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(':', '_'); + } + } +} diff --git a/Runtime/ABase/GameObjectPool/RuntimePoolModels.cs.meta b/Runtime/ABase/GameObjectPool/RuntimePoolModels.cs.meta new file mode 100644 index 0000000..8190d38 --- /dev/null +++ b/Runtime/ABase/GameObjectPool/RuntimePoolModels.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 542273e73f9fe8c46854d088f3c6387c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: