Merge pull request 'add feature prefabs pooling system' (#408) from feature/pooling_system into develop

Reviewed-on: https://git.pthub.vn/Unity/perfect-world-unity/pulls/408
This commit is contained in:
tungdv
2026-05-06 11:04:10 +00:00
23 changed files with 478 additions and 26 deletions
@@ -0,0 +1,46 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &333304658587404370
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4428566463457521840}
- component: {fileID: 2568441579902397308}
m_Layer: 0
m_Name: AddresablePoolingManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &4428566463457521840
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 333304658587404370}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &2568441579902397308
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 333304658587404370}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: b8e790a183861dd40bd580421c793038, type: 3}
m_Name:
m_EditorClassIdentifier:
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6c3420e247bbb884aa01fd5c833ca4a1
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,46 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &4019969352985339485
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6138598088942571876}
- component: {fileID: 6270892122129443994}
m_Layer: 0
m_Name: PrefabPoolingManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &6138598088942571876
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4019969352985339485}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &6270892122129443994
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4019969352985339485}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 868f373ec1cb5324f889aab8b44c8a76, type: 3}
m_Name:
m_EditorClassIdentifier:
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fe15f23447bb69545934398608d8bd11
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2dfe17492882fb88b93875b027ade9cef638e3bdadaf3a7ccbbcf48dd7ec6b67
size 308617
oid sha256:98acfc6d78af21396b1c3dcadcb4387954e34ff5863aa8a628f4011b26888168
size 313916
+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; // Æï³Ë×´Ì¬Íæ¼ÒÐÕÃûµÄµ÷Õû
// ÒÀ¸½ÀàÐÍ
@@ -3289,6 +3290,7 @@ namespace BrewMonster
{
GameObject.Destroy(m_pPetModel);
m_pPetModel = null;
PoolManager.Instance.Despawn(m_PetModelVisual);
}
if (bResetData)
@@ -3444,21 +3446,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.