diff --git a/Assets/PerfectWorld/Scene/Bootstrap.unity b/Assets/PerfectWorld/Scene/Bootstrap.unity index 73241b4ef4..ab49322771 100644 --- a/Assets/PerfectWorld/Scene/Bootstrap.unity +++ b/Assets/PerfectWorld/Scene/Bootstrap.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f809da92f7a74929fe4911b38ecddcf5bfe0fa9f667ad04f6c927511246975ae -size 308267 +oid sha256:b4fd514f16e0e1d3b0903d7e8ba0ba96ba7a526a14f48d12174c552f9ff0b284 +size 310807 diff --git a/Assets/PerfectWorld/Scripts/Move/CECPlayer.cs b/Assets/PerfectWorld/Scripts/Move/CECPlayer.cs index c71f88878e..4b1787a645 100644 --- a/Assets/PerfectWorld/Scripts/Move/CECPlayer.cs +++ b/Assets/PerfectWorld/Scripts/Move/CECPlayer.cs @@ -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; diff --git a/Assets/PerfectWorld/Scripts/NPC/CECNPC.cs b/Assets/PerfectWorld/Scripts/NPC/CECNPC.cs index 6f3a7c0370..39aca84030 100644 --- a/Assets/PerfectWorld/Scripts/NPC/CECNPC.cs +++ b/Assets/PerfectWorld/Scripts/NPC/CECNPC.cs @@ -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?.InitNPCEventDoneHandler(m_NPCInfo); diff --git a/Assets/PerfectWorld/Scripts/NPC/NPCBuilder.cs b/Assets/PerfectWorld/Scripts/NPC/NPCBuilder.cs index d9cdedabef..4b8bd0afe8 100644 --- a/Assets/PerfectWorld/Scripts/NPC/NPCBuilder.cs +++ b/Assets/PerfectWorld/Scripts/NPC/NPCBuilder.cs @@ -29,7 +29,8 @@ public class NPCBuilder : MonoSingleton public async Task 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 diff --git a/Assets/PerfectWorld/Scripts/Objet/CECMatter.cs b/Assets/PerfectWorld/Scripts/Objet/CECMatter.cs index dbdb6bf855..e3a79ecc6d 100644 --- a/Assets/PerfectWorld/Scripts/Objet/CECMatter.cs +++ b/Assets/PerfectWorld/Scripts/Objet/CECMatter.cs @@ -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); diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/Addresable.meta b/Assets/PerfectWorld/Scripts/PoolingManager/Addresable.meta new file mode 100644 index 0000000000..6ddfdb0443 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/PoolingManager/Addresable.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a576b0dd1ca5e0c439a1fb60f057d339 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/IPoolable.cs b/Assets/PerfectWorld/Scripts/PoolingManager/Addresable/IPoolable.cs similarity index 100% rename from Assets/PerfectWorld/Scripts/PoolingManager/IPoolable.cs rename to Assets/PerfectWorld/Scripts/PoolingManager/Addresable/IPoolable.cs diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/IPoolable.cs.meta b/Assets/PerfectWorld/Scripts/PoolingManager/Addresable/IPoolable.cs.meta similarity index 100% rename from Assets/PerfectWorld/Scripts/PoolingManager/IPoolable.cs.meta rename to Assets/PerfectWorld/Scripts/PoolingManager/Addresable/IPoolable.cs.meta diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/ObjectPool.cs b/Assets/PerfectWorld/Scripts/PoolingManager/Addresable/ObjectPool.cs similarity index 100% rename from Assets/PerfectWorld/Scripts/PoolingManager/ObjectPool.cs rename to Assets/PerfectWorld/Scripts/PoolingManager/Addresable/ObjectPool.cs diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/ObjectPool.cs.meta b/Assets/PerfectWorld/Scripts/PoolingManager/Addresable/ObjectPool.cs.meta similarity index 100% rename from Assets/PerfectWorld/Scripts/PoolingManager/ObjectPool.cs.meta rename to Assets/PerfectWorld/Scripts/PoolingManager/Addresable/ObjectPool.cs.meta diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/PoolManager.cs b/Assets/PerfectWorld/Scripts/PoolingManager/Addresable/PoolManager.cs similarity index 100% rename from Assets/PerfectWorld/Scripts/PoolingManager/PoolManager.cs rename to Assets/PerfectWorld/Scripts/PoolingManager/Addresable/PoolManager.cs diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/PoolManager.cs.meta b/Assets/PerfectWorld/Scripts/PoolingManager/Addresable/PoolManager.cs.meta similarity index 100% rename from Assets/PerfectWorld/Scripts/PoolingManager/PoolManager.cs.meta rename to Assets/PerfectWorld/Scripts/PoolingManager/Addresable/PoolManager.cs.meta diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/Prefabs.meta b/Assets/PerfectWorld/Scripts/PoolingManager/Prefabs.meta new file mode 100644 index 0000000000..02854fd6b9 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/PoolingManager/Prefabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9c4e2151efbb02540962f141c9a987cb +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/Prefabs/PrefabPoolManager.cs b/Assets/PerfectWorld/Scripts/PoolingManager/Prefabs/PrefabPoolManager.cs new file mode 100644 index 0000000000..0945caf5ed --- /dev/null +++ b/Assets/PerfectWorld/Scripts/PoolingManager/Prefabs/PrefabPoolManager.cs @@ -0,0 +1,208 @@ +using System.Collections.Generic; +using System.Linq; +using UnityEngine; +using UnityEngine.Pool; + +/// +/// Centralized prefab pooling manager for reusable GameObject instances. +/// +/// +namespace BrewMonster.Scripts +{ + public class PrefabPoolManager : MonoBehaviour + { + private static PrefabPoolManager instance; + + private readonly Dictionary poolsByPrefab = new(); + private readonly Dictionary allInstances = new(); + private readonly Dictionary activeInstances = new(); + + public static PrefabPoolManager Instance + { + get + { + if (instance == null) + { + instance = FindAnyObjectByType(); + if (instance == null) + { + var managerObject = new GameObject(nameof(PrefabPoolManager)); + instance = managerObject.AddComponent(); + } + } + + 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( + () => 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(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 Pool; + } + } +} diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/Prefabs/PrefabPoolManager.cs.meta b/Assets/PerfectWorld/Scripts/PoolingManager/Prefabs/PrefabPoolManager.cs.meta new file mode 100644 index 0000000000..c07d2cccbb --- /dev/null +++ b/Assets/PerfectWorld/Scripts/PoolingManager/Prefabs/PrefabPoolManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 868f373ec1cb5324f889aab8b44c8a76 \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/_Doc/PrefabPoolManager .md b/Assets/PerfectWorld/Scripts/_Doc/PrefabPoolManager .md new file mode 100644 index 0000000000..9c9401c19c --- /dev/null +++ b/Assets/PerfectWorld/Scripts/_Doc/PrefabPoolManager .md @@ -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); \ No newline at end of file diff --git a/Assets/PerfectWorld/Scripts/_Doc/PrefabPoolManager .md.meta b/Assets/PerfectWorld/Scripts/_Doc/PrefabPoolManager .md.meta new file mode 100644 index 0000000000..0951cdedb2 --- /dev/null +++ b/Assets/PerfectWorld/Scripts/_Doc/PrefabPoolManager .md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2a0e8e7ca64846747807f6c55aa465d5 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/EC_GameRun.cs b/Assets/Scripts/EC_GameRun.cs index 0d28ce30f7..23a4339bcd 100644 --- a/Assets/Scripts/EC_GameRun.cs +++ b/Assets/Scripts/EC_GameRun.cs @@ -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(AddressResourceConfig.PlayerPrefab); _monsterPrefab = Resources.Load(AddressResourceConfig.MonsterPrefab); + PrefabPoolManager.Instance.InitPool(_monsterPrefab, defaultCapacity: 200, maxSize: 250); _npcServerPrefab = Resources.Load(AddressResourceConfig.NpcServerPrefab); + PrefabPoolManager.Instance.InitPool(_npcServerPrefab, defaultCapacity: 25, maxSize: 100); _petServerPrefab = Resources.Load(AddressResourceConfig.PetServerPrefab); + PrefabPoolManager.Instance.InitPool(_petServerPrefab); _petMountServerPrefab = Resources.Load(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(); + return PrefabPoolManager.Instance.Spawn(_monsterPrefab, Vector3.zero, Quaternion.identity, ObjectSpawner.Instance.transform) .GetComponent(); } public RoleInfo GetSelectedRoleInfo() @@ -742,13 +748,16 @@ public partial class CECGameRun : ITickable public CECPet GetPet() { - return ObjectSpawner.Instance.InstantiateObject(_petServerPrefab, setThisAsParent: true) - .GetComponent(); + //return ObjectSpawner.Instance.InstantiateObject(_petServerPrefab, setThisAsParent: true) + // .GetComponent(); + return PrefabPoolManager.Instance.Spawn(_petServerPrefab, Vector3.zero, Quaternion.identity, ObjectSpawner.Instance.transform) + .GetComponent(); } 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); } /// diff --git a/Documentation/PrefabsPoolingManager.md b/Documentation/PrefabsPoolingManager.md new file mode 100644 index 0000000000..9c6a06824d --- /dev/null +++ b/Documentation/PrefabsPoolingManager.md @@ -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` 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.