From d1a526094e42593366af14f18513681821d90307 Mon Sep 17 00:00:00 2001 From: ud18010 Date: Thu, 30 Jul 2026 20:57:59 +0800 Subject: [PATCH] Add resource pool manager --- .../EditMode/ResourcePoolManagerTests.cs | 82 +++++ .../EditMode/ResourcePoolManagerTests.cs.meta | 11 + .../ActorBehaviorPresentation.cs | 55 ++-- .../xmain/Manager/ResourcePoolManager.cs | 287 ++++++++++++++++++ .../xmain/Manager/ResourcePoolManager.cs.meta | 11 + 5 files changed, 426 insertions(+), 20 deletions(-) create mode 100644 Client/Assets/Script/Tests/EditMode/ResourcePoolManagerTests.cs create mode 100644 Client/Assets/Script/Tests/EditMode/ResourcePoolManagerTests.cs.meta create mode 100644 Client/Assets/Script/xmain/Manager/ResourcePoolManager.cs create mode 100644 Client/Assets/Script/xmain/Manager/ResourcePoolManager.cs.meta diff --git a/Client/Assets/Script/Tests/EditMode/ResourcePoolManagerTests.cs b/Client/Assets/Script/Tests/EditMode/ResourcePoolManagerTests.cs new file mode 100644 index 00000000..d623d205 --- /dev/null +++ b/Client/Assets/Script/Tests/EditMode/ResourcePoolManagerTests.cs @@ -0,0 +1,82 @@ +using NUnit.Framework; +using UnityEngine; +using XGame; + +public sealed class ResourcePoolManagerTests +{ + private const string EffectPath = "Assets/Game/Art/Effect/Prefab/test.prefab"; + private GameObject _mainBefore; + private GameObject _cacheBefore; + private GameObject _parent; + private GameObject _instance; + + [SetUp] + public void SetUp() + { + _mainBefore = GameObject.Find("Main"); + _cacheBefore = _mainBefore != null + ? _mainBefore.transform.Find("Cache")?.gameObject + : null; + } + + [TearDown] + public void TearDown() + { + ResourcePoolManager.ClearAll(); + + if (_parent != null) + { + Object.DestroyImmediate(_parent); + _parent = null; + } + + if (_instance != null) + { + Object.DestroyImmediate(_instance); + _instance = null; + } + + GameObject main = GameObject.Find("Main"); + if (_mainBefore == null && main != null) + { + Object.DestroyImmediate(main); + } + else if (_mainBefore != null && _cacheBefore == null) + { + Transform createdCache = _mainBefore.transform.Find("Cache"); + if (createdCache != null) + { + Object.DestroyImmediate(createdCache.gameObject); + } + } + + _mainBefore = null; + _cacheBefore = null; + } + + [Test] + public void ReleaseAndTryGet_ReusesInactiveInstanceForSamePath() + { + _parent = new GameObject("parent"); + _instance = new GameObject("pooled-effect"); + _instance.transform.SetParent(_parent.transform); + _instance.transform.localPosition = new Vector3(1f, 2f, 3f); + _instance.SetActive(true); + + ResourcePoolManager.Release(EffectPath, _instance); + + Assert.IsFalse(_instance.activeSelf); + Assert.IsNotNull(_instance.transform.parent); + Assert.That(_instance.transform.parent.name, Is.EqualTo("Cache")); + Assert.IsNotNull(_instance.transform.parent.parent); + Assert.That(_instance.transform.parent.parent.name, Is.EqualTo("Main")); + Assert.That(ResourcePoolManager.GetPooledCount(EffectPath), Is.EqualTo(1)); + + GameObject reused = ResourcePoolManager.TryGet(EffectPath, _parent.transform, Vector3.one, Quaternion.identity); + + Assert.AreSame(_instance, reused); + Assert.IsTrue(reused.activeSelf); + Assert.AreSame(_parent.transform, reused.transform.parent); + Assert.That(reused.transform.localPosition, Is.EqualTo(Vector3.one)); + } +} diff --git a/Client/Assets/Script/Tests/EditMode/ResourcePoolManagerTests.cs.meta b/Client/Assets/Script/Tests/EditMode/ResourcePoolManagerTests.cs.meta new file mode 100644 index 00000000..08d93251 --- /dev/null +++ b/Client/Assets/Script/Tests/EditMode/ResourcePoolManagerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2c82760d7f3940d48af3d0a2b49c68a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Assets/Script/xmain/ActorBehavior/ActorBehaviorPresentation.cs b/Client/Assets/Script/xmain/ActorBehavior/ActorBehaviorPresentation.cs index e7a576ab..165b138b 100644 --- a/Client/Assets/Script/xmain/ActorBehavior/ActorBehaviorPresentation.cs +++ b/Client/Assets/Script/xmain/ActorBehavior/ActorBehaviorPresentation.cs @@ -133,7 +133,7 @@ namespace XGame.ActorBehavior target.Version = NextActorVersion(actorId); for (int i = target.SpawnedEffects.Count - 1; i >= 0; i--) { - DestroyUnityObject(target.SpawnedEffects[i]); + ReleaseEffectInstance(target.SpawnedEffects[i]); } target.SpawnedEffects.Clear(); } @@ -154,24 +154,18 @@ namespace XGame.ActorBehavior ActorPresentationTarget startTarget = null; bool actorBoundEffect = !hasExplicitPosition && actors.TryGetValue(actorId, out startTarget); int startVersion = actorBoundEffect && startTarget != null ? startTarget.Version : 0; - UObject loaded = null; - yield return XResLoader.coLoadRes(prefabPath, typeof(GameObject), obj => loaded = obj); - - GameObject prefab = loaded as GameObject; - if (prefab == null) - { - Debug.LogWarning("[ActorBehaviorPresentation] effect prefab load failed: " + prefabPath); - yield break; - } + GameObject instance = null; + yield return LoadEffectInstance(actorId, prefabPath, position, hasExplicitPosition, startTarget, obj => instance = obj); if (actorBoundEffect && (!actors.TryGetValue(actorId, out ActorPresentationTarget currentTarget) || currentTarget.Version != startVersion)) { + ReleaseEffectInstance(instance); yield break; } - GameObject instance = InstantiateEffect(actorId, prefab, position, hasExplicitPosition); if (instance == null) { + Debug.LogWarning("[ActorBehaviorPresentation] effect prefab load failed: " + prefabPath); yield break; } @@ -185,7 +179,7 @@ namespace XGame.ActorBehavior } else if (defaultEffectLifetime > 0f) { - Destroy(instance, defaultEffectLifetime); + StartCoroutine(ReleaseEffectAfterLifetime(instance, defaultEffectLifetime)); } } @@ -196,27 +190,35 @@ namespace XGame.ActorBehavior { target.SpawnedEffects.Remove(instance); } - DestroyUnityObject(instance); + ReleaseEffectInstance(instance); } - private GameObject InstantiateEffect(int actorId, GameObject prefab, BehaviorVector3 position, bool hasExplicitPosition) + private IEnumerator ReleaseEffectAfterLifetime(GameObject instance, float lifetime) { - actors.TryGetValue(actorId, out ActorPresentationTarget target); + yield return new WaitForSeconds(lifetime); + ReleaseEffectInstance(instance); + } + + private IEnumerator LoadEffectInstance(int actorId, string prefabPath, BehaviorVector3 position, bool hasExplicitPosition, ActorPresentationTarget startTarget, System.Action action) + { + ActorPresentationTarget target = startTarget; + if (target == null) + { + actors.TryGetValue(actorId, out target); + } if (target != null && !hasExplicitPosition && attachActorEffects) { Transform parent = target.EffectRoot != null ? target.EffectRoot : target.ActorTransform; - GameObject instance = Instantiate(prefab, parent); - instance.transform.localPosition = Vector3.zero; - instance.transform.localRotation = Quaternion.identity; - return instance; + yield return ResourcePoolManager.coLoadGameObject(prefabPath, parent, Vector3.zero, Quaternion.identity, action); + yield break; } Vector3 worldPosition = hasExplicitPosition ? ToUnity(position) : GetActorPosition(target); Quaternion worldRotation = target != null && target.ActorTransform != null ? target.ActorTransform.rotation : Quaternion.identity; - return Instantiate(prefab, worldPosition, worldRotation); + yield return ResourcePoolManager.coLoadGameObjectWorld(prefabPath, worldPosition, worldRotation, action); } private int NextActorVersion(int actorId) @@ -264,6 +266,19 @@ namespace XGame.ActorBehavior } } + private static void ReleaseEffectInstance(GameObject instance) + { + if (instance == null) + { + return; + } + + if (!ResourcePoolManager.Release(instance)) + { + DestroyUnityObject(instance); + } + } + private sealed class ActorPresentationTarget { public readonly Animator Animator; diff --git a/Client/Assets/Script/xmain/Manager/ResourcePoolManager.cs b/Client/Assets/Script/xmain/Manager/ResourcePoolManager.cs new file mode 100644 index 00000000..324d9aaa --- /dev/null +++ b/Client/Assets/Script/xmain/Manager/ResourcePoolManager.cs @@ -0,0 +1,287 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UObject = UnityEngine.Object; + +namespace XGame +{ + public static class ResourcePoolManager + { + private const string MainName = "Main"; + private const string PoolRootName = "Cache"; + + private static readonly Dictionary> Pools = new Dictionary>(); + private static readonly HashSet PooledObjects = new HashSet(); + + public static IEnumerator coLoadGameObject(string path, Transform parent, Vector3 localPosition, Quaternion localRotation, Action action) + { + GameObject instance = TryGet(path, parent, localPosition, localRotation); + if (instance != null) + { + action?.Invoke(instance); + yield break; + } + + UObject loaded = null; + yield return XResLoader.coLoadRes(path, typeof(GameObject), obj => loaded = obj); + + GameObject prefab = loaded as GameObject; + if (prefab == null) + { + action?.Invoke(null); + yield break; + } + + instance = UObject.Instantiate(prefab, parent); + instance.transform.localPosition = localPosition; + instance.transform.localRotation = localRotation; + Mark(instance, path); + RestartEffects(instance); + action?.Invoke(instance); + } + + public static IEnumerator coLoadGameObjectWorld(string path, Vector3 position, Quaternion rotation, Action action) + { + GameObject instance = TryGetWorld(path, position, rotation); + if (instance != null) + { + action?.Invoke(instance); + yield break; + } + + UObject loaded = null; + yield return XResLoader.coLoadRes(path, typeof(GameObject), obj => loaded = obj); + + GameObject prefab = loaded as GameObject; + if (prefab == null) + { + action?.Invoke(null); + yield break; + } + + instance = UObject.Instantiate(prefab, position, rotation); + Mark(instance, path); + RestartEffects(instance); + action?.Invoke(instance); + } + + public static GameObject TryGet(string path, Transform parent, Vector3 localPosition, Quaternion localRotation) + { + GameObject instance = Pop(path); + if (instance == null) + { + return null; + } + + Transform transform = instance.transform; + transform.SetParent(parent, false); + transform.localPosition = localPosition; + transform.localRotation = localRotation; + instance.SetActive(true); + RestartEffects(instance); + return instance; + } + + public static GameObject TryGetWorld(string path, Vector3 position, Quaternion rotation) + { + GameObject instance = Pop(path); + if (instance == null) + { + return null; + } + + Transform transform = instance.transform; + transform.SetParent(null, false); + transform.SetPositionAndRotation(position, rotation); + instance.SetActive(true); + RestartEffects(instance); + return instance; + } + + public static bool Release(GameObject instance) + { + if (instance == null) + { + return false; + } + + PooledResourceMarker marker = instance.GetComponent(); + return marker != null && Release(marker.Path, instance); + } + + public static bool Release(string path, GameObject instance) + { + if (string.IsNullOrEmpty(path) || instance == null) + { + return false; + } + + string key = NormalizePath(path); + if (PooledObjects.Contains(instance)) + { + return true; + } + + Mark(instance, key); + StopEffects(instance); + instance.SetActive(false); + instance.transform.SetParent(EnsurePoolRoot(), false); + + Stack pool; + if (!Pools.TryGetValue(key, out pool)) + { + pool = new Stack(); + Pools.Add(key, pool); + } + + pool.Push(instance); + PooledObjects.Add(instance); + return true; + } + + public static int GetPooledCount(string path) + { + Stack pool; + return Pools.TryGetValue(NormalizePath(path), out pool) ? pool.Count : 0; + } + + public static void Clear(string path) + { + string key = NormalizePath(path); + Stack pool; + if (!Pools.TryGetValue(key, out pool)) + { + return; + } + + DestroyPool(pool); + Pools.Remove(key); + } + + public static void ClearAll() + { + foreach (Stack pool in Pools.Values) + { + DestroyPool(pool); + } + + Pools.Clear(); + PooledObjects.Clear(); + } + + private static GameObject Pop(string path) + { + Stack pool; + string key = NormalizePath(path); + if (!Pools.TryGetValue(key, out pool)) + { + return null; + } + + while (pool.Count > 0) + { + GameObject instance = pool.Pop(); + PooledObjects.Remove(instance); + if (instance != null) + { + Mark(instance, key); + return instance; + } + } + + Pools.Remove(key); + return null; + } + + private static void Mark(GameObject instance, string path) + { + PooledResourceMarker marker = instance.GetComponent(); + if (marker == null) + { + marker = instance.AddComponent(); + } + + marker.Path = NormalizePath(path); + } + + private static Transform EnsurePoolRoot() + { + GameObject main = GameObject.Find(MainName); + if (main == null) + { + main = new GameObject(MainName); + } + + Transform poolRoot = main.transform.Find(PoolRootName); + if (poolRoot != null) + { + return poolRoot; + } + + GameObject cacheObject = new GameObject(PoolRootName); + Transform cacheTransform = cacheObject.transform; + cacheTransform.SetParent(main.transform, false); + cacheTransform.localPosition = Vector3.zero; + cacheTransform.localRotation = Quaternion.identity; + cacheTransform.localScale = Vector3.one; + return cacheTransform; + } + + private static void DestroyPool(Stack pool) + { + while (pool.Count > 0) + { + GameObject instance = pool.Pop(); + PooledObjects.Remove(instance); + DestroyUnityObject(instance); + } + } + + private static void StopEffects(GameObject instance) + { + ParticleSystem[] particles = instance.GetComponentsInChildren(true); + for (int i = 0; i < particles.Length; i++) + { + particles[i].Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); + } + } + + private static void RestartEffects(GameObject instance) + { + ParticleSystem[] particles = instance.GetComponentsInChildren(true); + for (int i = 0; i < particles.Length; i++) + { + particles[i].Clear(true); + particles[i].Play(true); + } + } + + private static string NormalizePath(string path) + { + return string.IsNullOrEmpty(path) ? string.Empty : path.Replace('\\', '/'); + } + + private static void DestroyUnityObject(UObject obj) + { + if (obj == null) + { + return; + } + + if (Application.isPlaying) + { + UObject.Destroy(obj); + } + else + { + UObject.DestroyImmediate(obj); + } + } + + private sealed class PooledResourceMarker : MonoBehaviour + { + public string Path; + } + } +} diff --git a/Client/Assets/Script/xmain/Manager/ResourcePoolManager.cs.meta b/Client/Assets/Script/xmain/Manager/ResourcePoolManager.cs.meta new file mode 100644 index 00000000..0bd84756 --- /dev/null +++ b/Client/Assets/Script/xmain/Manager/ResourcePoolManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7426300ebda743f0bc14267a6403a810 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: