feat: Add prefabs pooling manager.

fix: update spawn NPC.
This commit is contained in:
Tungdv
2026-05-06 15:33:28 +07:00
parent f9eb2c04c4
commit df23868b40
19 changed files with 372 additions and 26 deletions
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f809da92f7a74929fe4911b38ecddcf5bfe0fa9f667ad04f6c927511246975ae
size 308267
oid sha256:b4fd514f16e0e1d3b0903d7e8ba0ba96ba7a526a14f48d12174c552f9ff0b284
size 310807
+11 -9
View File
@@ -111,6 +111,7 @@ namespace BrewMonster
public RIDINGPET m_RidingPet; // Riding pet information
public GameObject m_pPetModel = null; // Pet model
public GameObject m_PetModelVisual = null;
public RIDINGPET m_CandPet;// ID of candidate pet
A3DVECTOR3 m_vNamePos; // Æï³Ë×´Ì¬Íæ¼ÒÐÕÃûµÄµ÷Õû
// ÒÀ¸½ÀàÐÍ
@@ -3146,6 +3147,7 @@ namespace BrewMonster
{
GameObject.Destroy(m_pPetModel);
m_pPetModel = null;
PoolManager.Instance.Despawn(m_PetModelVisual);
}
if (bResetData)
@@ -3301,21 +3303,21 @@ namespace BrewMonster
}
try
{
var model = await AddressableManager.Instance.LoadPrefabAsync(AFile.NormalizePath(szPetPath.ToLower(), true));
if(model == null)
m_PetModelVisual = await PoolManager.Instance.SpawnAsync(AFile.NormalizePath(szPetPath.ToLower(), true), Vector3.zero, Quaternion.identity, 15f);
if(m_PetModelVisual == null)
{
model = GameObject.CreatePrimitive(PrimitiveType.Capsule);
m_PetModelVisual = GameObject.CreatePrimitive(PrimitiveType.Capsule);
}
var obModel = GameObject.Instantiate(model);
obModel.transform.SetParent(pPetModel.transform);
AddressableManager.Instance.ReleaseAsset(szPetPath);
//var obModel = GameObject.Instantiate(model);
m_PetModelVisual.transform.SetParent(pPetModel.transform);
//AddressableManager.Instance.ReleaseAsset(szPetPath);
}
catch
{
var model = GameObject.CreatePrimitive(PrimitiveType.Capsule);
var obModel = GameObject.Instantiate(model);
obModel.transform.SetParent(pPetModel.transform);
AddressableManager.Instance.ReleaseAsset(szPetPath);
m_PetModelVisual = GameObject.Instantiate(model);
m_PetModelVisual.transform.SetParent(pPetModel.transform);
//AddressableManager.Instance.ReleaseAsset(szPetPath);
//return null;
}
return pPetModel;
+13 -10
View File
@@ -53,6 +53,7 @@ public class CECNPC : CECObject
[SerializeField] protected CharacterController _characterController;
[SerializeField] protected bool isDebug;
[SerializeField] protected NPCVisual npcVisual;
GameObject m_modelVisual = null;
protected static CECStringTab m_ActionNames;
/* public string NameNPC => m_strName;
@@ -579,7 +580,8 @@ public class CECNPC : CECObject
public void DestroySelf()
{
Destroy(gameObject);
PrefabPoolManager.Instance.Despawn(gameObject);
//Destroy(gameObject);
}
public float GetTransparentLimit()
{
@@ -656,7 +658,7 @@ public class CECNPC : CECObject
m_aIconStates.clear();*/
m_pNPCModelPolicy = null;
PoolManager.Instance.Despawn(m_modelVisual);
/*if (m_pPateName)
{
delete m_pPateName;
@@ -996,26 +998,27 @@ public class CECNPC : CECObject
{
return;
}
GameObject model = null;
try
{
szModelFile = AFile.NormalizePath(szModelFile.ToLower(), true);
model = await NPCBuilder.Instance.GetModelByPath(szModelFile);
if (model == null)
m_modelVisual = await NPCBuilder.Instance.GetModelByPath(szModelFile);
if (m_modelVisual == null)
{
model = GameObject.CreatePrimitive(PrimitiveType.Capsule);
model.name = szModelFile;
m_modelVisual = GameObject.CreatePrimitive(PrimitiveType.Capsule);
m_modelVisual.name = szModelFile;
BMLogger.LogWarning($" CECNPC.QueueLoadNPCModel model == null szModelFile= {szModelFile} ");
}
}
catch
{
model = GameObject.CreatePrimitive(PrimitiveType.Capsule);
m_modelVisual = GameObject.CreatePrimitive(PrimitiveType.Capsule);
BMLogger.LogWarning($" CECNPC.QueueLoadNPCModel model == null szModelFile= {szModelFile} ");
}
var monsterModel = Instantiate(model, transform);
monsterModel.SetActive(true);
//var monsterModel = Instantiate(model, transform);
m_modelVisual.transform.SetParent(transform, false);
m_modelVisual.SetActive(true);
var npcVisual = GetComponent<NPCVisual>();
npcVisual?.InitNPCEventDoneHandler(m_NPCInfo);
@@ -29,7 +29,8 @@ public class NPCBuilder : MonoSingleton<NPCBuilder>
public async Task<GameObject> GetModelByPath(string path)
{
return await AddressableManager.Instance.LoadPrefabAsync(AFile.NormalizePath(path));
//return await AddressableManager.Instance.LoadPrefabAsync(AFile.NormalizePath(path));
return await PoolManager.Instance.SpawnAsync(AFile.NormalizePath(path), Vector3.zero, Quaternion.identity, memoryReleaseTTL: 15f, autoDespawnTime: 0f);
}
#if UNITY_EDITOR
@@ -166,6 +166,7 @@ namespace PerfectWorld.Scripts
if (matterObject != null)
{
//var matterObject = Instantiate(matterPrefab);
matterObject.transform.SetParent(ObjectSpawner.Instance.transform);
matterObject.name = $"Matter {matterObject.name} {matterInfo.tid} {matterInfo.mid}";
matterObject.transform.position = new Vector3(Info.pos.x, Info.pos.y, Info.pos.z);
matterObject.transform.localScale = new Vector3(1f, 1f, 1f);
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a576b0dd1ca5e0c439a1fb60f057d339
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9c4e2151efbb02540962f141c9a987cb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,208 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Pool;
/// <summary>
/// Centralized prefab pooling manager for reusable GameObject instances.
/// </summary>
///
namespace BrewMonster.Scripts
{
public class PrefabPoolManager : MonoBehaviour
{
private static PrefabPoolManager instance;
private readonly Dictionary<GameObject, PoolRecord> poolsByPrefab = new();
private readonly Dictionary<GameObject, PoolRecord> allInstances = new();
private readonly Dictionary<GameObject, PoolRecord> activeInstances = new();
public static PrefabPoolManager Instance
{
get
{
if (instance == null)
{
instance = FindAnyObjectByType<PrefabPoolManager>();
if (instance == null)
{
var managerObject = new GameObject(nameof(PrefabPoolManager));
instance = managerObject.AddComponent<PrefabPoolManager>();
}
}
return instance;
}
}
public void InitPool(GameObject prefab, int defaultCapacity = 10, int maxSize = 50)
{
if (prefab == null)
{
Debug.LogError("[PrefabPoolManager] Cannot initialize a pool with a null prefab.");
return;
}
if (poolsByPrefab.ContainsKey(prefab))
return;
defaultCapacity = Mathf.Max(0, defaultCapacity);
maxSize = Mathf.Max(1, maxSize);
if (defaultCapacity > maxSize)
defaultCapacity = maxSize;
var record = new PoolRecord
{
Prefab = prefab,
Container = CreatePoolContainer(prefab)
};
record.Pool = new ObjectPool<GameObject>(
() => CreateInstance(record),
obj => OnGetFromPool(record, obj),
obj => OnReleaseToPool(record, obj),
OnDestroyPooledObject,
collectionCheck: Application.isEditor,
defaultCapacity: defaultCapacity,
maxSize: maxSize);
poolsByPrefab.Add(prefab, record);
Prewarm(record, defaultCapacity);
}
public GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent = null)
{
if (prefab == null)
{
Debug.LogError("[PrefabPoolManager] Cannot spawn a null prefab.");
return null;
}
if (!poolsByPrefab.TryGetValue(prefab, out PoolRecord record))
{
InitPool(prefab);
record = poolsByPrefab[prefab];
}
GameObject instanceObject = record.Pool.Get();
instanceObject.transform.SetPositionAndRotation(position, rotation);
if (parent != null)
{
instanceObject.transform.SetParent(parent);
}
return instanceObject;
}
public void Despawn(GameObject obj)
{
if (obj == null)
return;
if (!activeInstances.TryGetValue(obj, out PoolRecord record))
{
if (allInstances.ContainsKey(obj))
{
Debug.LogWarning($"[PrefabPoolManager] Object '{obj.name}' has already been returned to the pool.");
return;
}
Debug.LogWarning($"[PrefabPoolManager] Object '{obj.name}' was not spawned by PrefabPoolManager. Destroying it instead.");
Destroy(obj);
return;
}
record.Pool.Release(obj);
}
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
//DontDestroyOnLoad(gameObject);
}
private void OnDestroy()
{
if (instance != this)
return;
foreach (PoolRecord record in poolsByPrefab.Values)
{
record.Pool.Clear();
}
poolsByPrefab.Clear();
allInstances.Clear();
activeInstances.Clear();
instance = null;
}
private Transform CreatePoolContainer(GameObject prefab)
{
var container = new GameObject($"{prefab.name}_Pool").transform;
container.SetParent(transform);
return container;
}
private GameObject CreateInstance(PoolRecord record)
{
GameObject instanceObject = Instantiate(record.Prefab, record.Container);
instanceObject.name = record.Prefab.name;
instanceObject.SetActive(false);
allInstances[instanceObject] = record;
return instanceObject;
}
private void OnGetFromPool(PoolRecord record, GameObject obj)
{
activeInstances[obj] = record;
obj.transform.SetParent(null);
obj.SetActive(true);
}
private void OnReleaseToPool(PoolRecord record, GameObject obj)
{
activeInstances.Remove(obj);
obj.SetActive(false);
obj.transform.SetParent(record.Container);
}
private void OnDestroyPooledObject(GameObject obj)
{
activeInstances.Remove(obj);
allInstances.Remove(obj);
if (obj != null)
Destroy(obj);
}
private static void Prewarm(PoolRecord record, int count)
{
if (count <= 0)
return;
var warmedObjects = new List<GameObject>(count);
for (int i = 0; i < count; i++)
{
warmedObjects.Add(record.Pool.Get());
}
for (int i = 0; i < warmedObjects.Count; i++)
{
record.Pool.Release(warmedObjects[i]);
}
}
internal sealed class PoolRecord
{
public GameObject Prefab;
public Transform Container;
public ObjectPool<GameObject> Pool;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 868f373ec1cb5324f889aab8b44c8a76
@@ -0,0 +1,23 @@
# 🤖 AI SYSTEM INSTRUCTION: PrefabPoolManager Usage Rules
## 1. System Context
- **Environment:** Unity 3D (C#)
- **Target Platform:** Mobile (Android, iOS) - Requires strict optimization.
- **Concept:** The project uses a centralized Object Pooling system called `PrefabPoolManager` (built on `UnityEngine.Pool.ObjectPool`).
- **AI Task:** When writing scripts that involve creating or destroying GameObjects, the AI **MUST** use this manager instead of Unity's default methods.
---
## 2. Available API (Do NOT implement this, just use it)
The `PrefabPoolManager` is a Singleton accessible via `PrefabPoolManager.Instance`. It provides the following methods:
```csharp
// Pre-warms a pool (Optional, used during loading)
public void InitPool(GameObject prefab, int defaultCapacity = 10, int maxSize = 50);
// Spawns an object from the pool
public GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation);
// Returns an object to the pool
public void Despawn(GameObject obj);
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2a0e8e7ca64846747807f6c55aa465d5
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+13 -4
View File
@@ -146,9 +146,13 @@ public partial class CECGameRun : ITickable
BMLogger.LogWarning("CECGameRun::LoadPrefabs, Loading prefabs from Resources. Consider using Addressables for better performance and memory management.");
_playerPrefab = Resources.Load<GameObject>(AddressResourceConfig.PlayerPrefab);
_monsterPrefab = Resources.Load<GameObject>(AddressResourceConfig.MonsterPrefab);
PrefabPoolManager.Instance.InitPool(_monsterPrefab, defaultCapacity: 200, maxSize: 250);
_npcServerPrefab = Resources.Load<GameObject>(AddressResourceConfig.NpcServerPrefab);
PrefabPoolManager.Instance.InitPool(_npcServerPrefab, defaultCapacity: 25, maxSize: 100);
_petServerPrefab = Resources.Load<GameObject>(AddressResourceConfig.PetServerPrefab);
PrefabPoolManager.Instance.InitPool(_petServerPrefab);
_petMountServerPrefab = Resources.Load<GameObject>(AddressResourceConfig.PetMountServerPrefab);
PrefabPoolManager.Instance.InitPool(_petMountServerPrefab);
#if UNITY_EDITOR
if (_playerPrefab == null)
{
@@ -394,7 +398,9 @@ public partial class CECGameRun : ITickable
}
public CECMonster GetMonster()
{
return ObjectSpawner.Instance.InstantiateObject(_monsterPrefab, setThisAsParent: true)
//return ObjectSpawner.Instance.InstantiateObject(_monsterPrefab, setThisAsParent: true)
// .GetComponent<CECMonster>();
return PrefabPoolManager.Instance.Spawn(_monsterPrefab, Vector3.zero, Quaternion.identity, ObjectSpawner.Instance.transform)
.GetComponent<CECMonster>();
}
public RoleInfo GetSelectedRoleInfo()
@@ -742,13 +748,16 @@ public partial class CECGameRun : ITickable
public CECPet GetPet()
{
return ObjectSpawner.Instance.InstantiateObject(_petServerPrefab, setThisAsParent: true)
.GetComponent<CECPet>();
//return ObjectSpawner.Instance.InstantiateObject(_petServerPrefab, setThisAsParent: true)
// .GetComponent<CECPet>();
return PrefabPoolManager.Instance.Spawn(_petServerPrefab, Vector3.zero, Quaternion.identity, ObjectSpawner.Instance.transform)
.GetComponent<CECPet>();
}
public GameObject GetPetMount()
{
return ObjectSpawner.Instance.InstantiateObject(_petMountServerPrefab, setThisAsParent: true);
//return ObjectSpawner.Instance.InstantiateObject(_petMountServerPrefab, setThisAsParent: true);
return PrefabPoolManager.Instance.Spawn(_petMountServerPrefab, Vector3.zero, Quaternion.identity, ObjectSpawner.Instance.transform);
}
/// <summary>
+74
View File
@@ -0,0 +1,74 @@
# Prefabs Pooling Manager
## Purpose
`PrefabPoolManager` centralizes prefab spawning and despawning through `UnityEngine.Pool.ObjectPool`.
Any gameplay code that repeatedly creates or removes prefab instances should use this manager instead of direct `Instantiate` and `Destroy` calls.
## Runtime Flow
1. A caller accesses `PrefabPoolManager.Instance`.
2. If no manager exists in the scene, the singleton creates a `PrefabPoolManager` GameObject and marks it with `DontDestroyOnLoad`.
3. The caller may pre-warm a prefab pool by calling:
```csharp
PrefabPoolManager.Instance.InitPool(prefab, defaultCapacity, maxSize);
```
4. `InitPool` creates one `ObjectPool<GameObject>` for the prefab and preloads inactive instances under a prefab-specific container.
5. The caller spawns instances by calling:
```csharp
GameObject obj = PrefabPoolManager.Instance.Spawn(prefab, position, rotation);
```
6. `Spawn` initializes a pool automatically if the prefab has not been registered yet, gets an object from the pool, detaches it from the pool container, sets its transform, and activates it.
7. The caller returns instances by calling:
```csharp
PrefabPoolManager.Instance.Despawn(obj);
```
8. `Despawn` releases the instance back to its original pool, disables it, and reparents it under the prefab pool container.
9. If the pool exceeds `maxSize`, Unity's `ObjectPool` destroys extra released instances.
## API
```csharp
public void InitPool(GameObject prefab, int defaultCapacity = 10, int maxSize = 50);
public GameObject Spawn(GameObject prefab, Vector3 position, Quaternion rotation);
public void Despawn(GameObject obj);
```
## Usage Example
```csharp
public class ProjectileSpawner : MonoBehaviour
{
[SerializeField] private GameObject projectilePrefab;
private void Start()
{
PrefabPoolManager.Instance.InitPool(projectilePrefab, 20, 100);
}
public void Fire(Vector3 position, Quaternion rotation)
{
GameObject projectile = PrefabPoolManager.Instance.Spawn(projectilePrefab, position, rotation);
projectile.SetActive(true);
}
public void Release(GameObject projectile)
{
PrefabPoolManager.Instance.Despawn(projectile);
}
}
```
## Notes
- Use `InitPool` during loading screens or scene setup for frequently spawned prefabs.
- Use `Spawn` instead of `Instantiate`.
- Use `Despawn` instead of `Destroy` for objects created through this manager.
- Each prefab has its own independent pool and capacity limits.
- The system is intended for mobile targets, where avoiding frequent allocations helps reduce frame spikes and garbage collection pressure.