Add resource pool manager

This commit is contained in:
ud18010
2026-07-30 20:57:59 +08:00
parent 576aa6c14e
commit d1a526094e
5 changed files with 426 additions and 20 deletions
@@ -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));
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c82760d7f3940d48af3d0a2b49c68a1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -133,7 +133,7 @@ namespace XGame.ActorBehavior
target.Version = NextActorVersion(actorId); target.Version = NextActorVersion(actorId);
for (int i = target.SpawnedEffects.Count - 1; i >= 0; i--) for (int i = target.SpawnedEffects.Count - 1; i >= 0; i--)
{ {
DestroyUnityObject(target.SpawnedEffects[i]); ReleaseEffectInstance(target.SpawnedEffects[i]);
} }
target.SpawnedEffects.Clear(); target.SpawnedEffects.Clear();
} }
@@ -154,24 +154,18 @@ namespace XGame.ActorBehavior
ActorPresentationTarget startTarget = null; ActorPresentationTarget startTarget = null;
bool actorBoundEffect = !hasExplicitPosition && actors.TryGetValue(actorId, out startTarget); bool actorBoundEffect = !hasExplicitPosition && actors.TryGetValue(actorId, out startTarget);
int startVersion = actorBoundEffect && startTarget != null ? startTarget.Version : 0; int startVersion = actorBoundEffect && startTarget != null ? startTarget.Version : 0;
UObject loaded = null; GameObject instance = null;
yield return XResLoader.coLoadRes(prefabPath, typeof(GameObject), obj => loaded = obj); yield return LoadEffectInstance(actorId, prefabPath, position, hasExplicitPosition, startTarget, obj => instance = obj);
GameObject prefab = loaded as GameObject;
if (prefab == null)
{
Debug.LogWarning("[ActorBehaviorPresentation] effect prefab load failed: " + prefabPath);
yield break;
}
if (actorBoundEffect && (!actors.TryGetValue(actorId, out ActorPresentationTarget currentTarget) || currentTarget.Version != startVersion)) if (actorBoundEffect && (!actors.TryGetValue(actorId, out ActorPresentationTarget currentTarget) || currentTarget.Version != startVersion))
{ {
ReleaseEffectInstance(instance);
yield break; yield break;
} }
GameObject instance = InstantiateEffect(actorId, prefab, position, hasExplicitPosition);
if (instance == null) if (instance == null)
{ {
Debug.LogWarning("[ActorBehaviorPresentation] effect prefab load failed: " + prefabPath);
yield break; yield break;
} }
@@ -185,7 +179,7 @@ namespace XGame.ActorBehavior
} }
else if (defaultEffectLifetime > 0f) else if (defaultEffectLifetime > 0f)
{ {
Destroy(instance, defaultEffectLifetime); StartCoroutine(ReleaseEffectAfterLifetime(instance, defaultEffectLifetime));
} }
} }
@@ -196,27 +190,35 @@ namespace XGame.ActorBehavior
{ {
target.SpawnedEffects.Remove(instance); 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<GameObject> action)
{
ActorPresentationTarget target = startTarget;
if (target == null)
{
actors.TryGetValue(actorId, out target);
}
if (target != null && !hasExplicitPosition && attachActorEffects) if (target != null && !hasExplicitPosition && attachActorEffects)
{ {
Transform parent = target.EffectRoot != null ? target.EffectRoot : target.ActorTransform; Transform parent = target.EffectRoot != null ? target.EffectRoot : target.ActorTransform;
GameObject instance = Instantiate(prefab, parent); yield return ResourcePoolManager.coLoadGameObject(prefabPath, parent, Vector3.zero, Quaternion.identity, action);
instance.transform.localPosition = Vector3.zero; yield break;
instance.transform.localRotation = Quaternion.identity;
return instance;
} }
Vector3 worldPosition = hasExplicitPosition ? ToUnity(position) : GetActorPosition(target); Vector3 worldPosition = hasExplicitPosition ? ToUnity(position) : GetActorPosition(target);
Quaternion worldRotation = target != null && target.ActorTransform != null Quaternion worldRotation = target != null && target.ActorTransform != null
? target.ActorTransform.rotation ? target.ActorTransform.rotation
: Quaternion.identity; : Quaternion.identity;
return Instantiate(prefab, worldPosition, worldRotation); yield return ResourcePoolManager.coLoadGameObjectWorld(prefabPath, worldPosition, worldRotation, action);
} }
private int NextActorVersion(int actorId) 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 private sealed class ActorPresentationTarget
{ {
public readonly Animator Animator; public readonly Animator Animator;
@@ -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<string, Stack<GameObject>> Pools = new Dictionary<string, Stack<GameObject>>();
private static readonly HashSet<GameObject> PooledObjects = new HashSet<GameObject>();
public static IEnumerator coLoadGameObject(string path, Transform parent, Vector3 localPosition, Quaternion localRotation, Action<GameObject> 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<GameObject> 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<PooledResourceMarker>();
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<GameObject> pool;
if (!Pools.TryGetValue(key, out pool))
{
pool = new Stack<GameObject>();
Pools.Add(key, pool);
}
pool.Push(instance);
PooledObjects.Add(instance);
return true;
}
public static int GetPooledCount(string path)
{
Stack<GameObject> pool;
return Pools.TryGetValue(NormalizePath(path), out pool) ? pool.Count : 0;
}
public static void Clear(string path)
{
string key = NormalizePath(path);
Stack<GameObject> pool;
if (!Pools.TryGetValue(key, out pool))
{
return;
}
DestroyPool(pool);
Pools.Remove(key);
}
public static void ClearAll()
{
foreach (Stack<GameObject> pool in Pools.Values)
{
DestroyPool(pool);
}
Pools.Clear();
PooledObjects.Clear();
}
private static GameObject Pop(string path)
{
Stack<GameObject> 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<PooledResourceMarker>();
if (marker == null)
{
marker = instance.AddComponent<PooledResourceMarker>();
}
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<GameObject> pool)
{
while (pool.Count > 0)
{
GameObject instance = pool.Pop();
PooledObjects.Remove(instance);
DestroyUnityObject(instance);
}
}
private static void StopEffects(GameObject instance)
{
ParticleSystem[] particles = instance.GetComponentsInChildren<ParticleSystem>(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<ParticleSystem>(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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7426300ebda743f0bc14267a6403a810
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: