From 5d4b37582969d5f508fa167770fccc348b3bd79b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A3=AE2003?= <2384017988@qq.com> Date: Mon, 13 Jul 2026 04:48:18 +0800 Subject: [PATCH] 1 --- Assets/Framework/Pool/BulletPool.cs | 92 ++++++---- Assets/Framework/Pool/EffectPool.cs | 171 ++++++++++++++++++ Assets/Framework/Pool/EffectPool.cs.meta | 11 ++ Assets/Framework/Pool/Pool.cs | 8 +- Assets/MyGame/Scenes/UIScene.unity | 2 +- .../Scripts/Items/Bullits/BullitMove.cs | 4 +- .../Scripts/Manager/CreateFishManager.cs | 5 + Assets/MyGame/Scripts/Turret/SitPosData.cs | 4 + Assets/MyGame/aa/Game/DefaultBossControl.cs | 3 +- .../aa/Game/FLZY/BOSS/JL/FLZY_JL_Boss.cs | 3 +- .../aa/Game/FLZY/BOSS/ZDX/FLZY_ZDX_BOSS.cs | 3 +- .../aa/UI/Game/GameMainPanel_FishBase.cs | 9 +- 12 files changed, 269 insertions(+), 46 deletions(-) create mode 100644 Assets/Framework/Pool/EffectPool.cs create mode 100644 Assets/Framework/Pool/EffectPool.cs.meta diff --git a/Assets/Framework/Pool/BulletPool.cs b/Assets/Framework/Pool/BulletPool.cs index a03eea56d..54cae1167 100644 --- a/Assets/Framework/Pool/BulletPool.cs +++ b/Assets/Framework/Pool/BulletPool.cs @@ -2,16 +2,24 @@ using System.Collections.Generic; using UnityEngine; /// -/// 子弹专用对象池 — 按 prefab 实例ID 分桶,与鱼的 Pool 完全隔离 +/// 子弹专用对象池 — 按 prefab.GetInstanceID() 分桶,与鱼的 Pool 完全隔离 /// 用法: /// var go = BulletPool.Instance.Get(prefab, pos, rot, parent); -/// BulletPool.Instance.Return(go); // 代替 Destroy +/// BulletPool.Instance.Return(go); // 代替 Destroy(单个回收) +/// BulletPool.Instance.ClearAll(); // 换炮台/切场景调用:销毁所有活跃子弹 + 清池 +/// prefabID 直接用 prefab.GetInstanceID()(Unity 内部 hash,全局唯一) +/// 活跃子弹→prefabID 的映射用 Dictionary 存储,不挂组件不改 name /// public class BulletPool : Singleton { // prefab.GetInstanceID() → 该 prefab 的池 private Dictionary dic = new Dictionary(); + // 活跃子弹映射:子弹GameObject → prefab.GetInstanceID() + // key 用 GameObject 本身(Unity 重载了 GetHashCode/Equals,按实例区分) + // value 是 prefabID,回收时凭此找到对应池 + private Dictionary activeMap = new Dictionary(); + // 每个池子的最大缓存数(超出直接 Destroy,防止内存膨胀) private const int MaxCachePerPrefab = 200; @@ -23,15 +31,23 @@ public class BulletPool : Singleton { if (prefab == null) return null; - int id = prefab.GetInstanceID(); - GameObject obj; + int prefabID = prefab.GetInstanceID(); + GameObject obj = null; - if (dic.TryGetValue(id, out var data) && data.poolList.Count > 0) + if (dic.TryGetValue(prefabID, out var data)) + { + // ⭐ 循环跳过已销毁的坏对象(ClearAll 用 Destroy 延迟销毁,时序竞争中可能残留) + while (data.poolList.Count > 0) + { + obj = data.GetObj(); + if (obj != null) break; // Unity 重载 ==,能识别已 Destroy 的假 null + obj = null; // 坏对象丢弃,继续取下一个 + } + } + + if (obj != null) { - obj = data.GetObj(); // ⭐ 必须先 SetParent 再设 position:模仿 Instantiate(prefab, pos, rot, parent) - // 若先设 position 再 SetParent(parent, false),localPosition 会保持, - // 换 parent 后世界位置偏移 → 子弹从错误位置飞出 obj.transform.SetParent(parent, false); obj.transform.position = position; obj.transform.rotation = rotation; @@ -43,12 +59,8 @@ public class BulletPool : Singleton obj.SetActive(true); } - // 确保挂上 tracker 记录 prefabID(回收时凭此找到对应池) - var tracker = obj.GetComponent(); - if (tracker == null) - tracker = obj.AddComponent(); - tracker.prefabID = id; - + // 记录映射:子弹实例 → prefabID(回收时凭此找到对应池) + activeMap[obj] = prefabID; return obj; } @@ -63,18 +75,16 @@ public class BulletPool : Singleton { if (obj == null) return; - var tracker = obj.GetComponent(); - int id = tracker != null ? tracker.prefabID : 0; - - // 取不到 id → 兜底 Destroy(非池内对象) - if (id == 0) + // 取不到映射 → 非 pool 对象 → 兜底 Destroy + if (!activeMap.TryGetValue(obj, out int prefabID) || prefabID == 0) { GameObject.Destroy(obj); return; } + activeMap.Remove(obj); - if (!dic.TryGetValue(id, out var data)) - dic[id] = data = new BulletPoolData(EnsurePoolRoot().transform, id); + if (!dic.TryGetValue(prefabID, out var data)) + dic[prefabID] = data = new BulletPoolData(EnsurePoolRoot().transform, prefabID); if (data.poolList.Count >= MaxCachePerPrefab) { @@ -93,25 +103,30 @@ public class BulletPool : Singleton public void Preload(GameObject prefab, int count) { if (prefab == null || count <= 0) return; - int id = prefab.GetInstanceID(); + int prefabID = prefab.GetInstanceID(); - if (!dic.TryGetValue(id, out var data)) - dic[id] = data = new BulletPoolData(EnsurePoolRoot().transform, id); + if (!dic.TryGetValue(prefabID, out var data)) + dic[prefabID] = data = new BulletPoolData(EnsurePoolRoot().transform, prefabID); for (int i = 0; i < count; i++) { var obj = GameObject.Instantiate(prefab, EnsurePoolRoot().transform); - var tracker = obj.GetComponent(); - if (tracker == null) - tracker = obj.AddComponent(); - tracker.prefabID = id; + // 预加载的对象直接入池,不进 activeMap(它们没被"借出") data.PushObj(obj); } } - /// 清理所有子弹池(切场景时调用) - public void Clear() + /// 销毁所有活跃子弹 + 清空所有池(换炮台/切场景调用) + public void ClearAll() { + // 销毁所有借出未归还的子弹 + foreach (var kv in activeMap.Keys) + { + if (kv != null) GameObject.Destroy(kv); + } + activeMap.Clear(); + + // 清空所有池 foreach (var kv in dic.Values) kv.Clear(); dic.Clear(); @@ -123,6 +138,17 @@ public class BulletPool : Singleton } } + /// 只销毁活跃子弹,保留池缓存(换炮台时调用:清屏但池能复用) + public void ClearActive() + { + foreach (var kv in activeMap.Keys) + { + if (kv != null) GameObject.Destroy(kv); + } + activeMap.Clear(); + // ⭐ 不清 dic,池里的缓存保留,同 prefab 下次能复用 + } + private GameObject EnsurePoolRoot() { if (poolRoot == null) @@ -134,12 +160,6 @@ public class BulletPool : Singleton } } -/// 挂在池内对象上,记录所属 prefabID(回收时凭此找到对应池) -public class BulletPoolTracker : MonoBehaviour -{ - public int prefabID; -} - /// 单个 prefab 的子弹池数据 public class BulletPoolData { diff --git a/Assets/Framework/Pool/EffectPool.cs b/Assets/Framework/Pool/EffectPool.cs new file mode 100644 index 000000000..870e37111 --- /dev/null +++ b/Assets/Framework/Pool/EffectPool.cs @@ -0,0 +1,171 @@ +using System.Collections.Generic; +using UnityEngine; + +/// +/// 特效专用对象池 — 按 prefab.GetInstanceID() 分桶 +/// 粒子播放完自动回池(通过 EffectAutoReturn 组件检测) +/// 用法: +/// var go = EffectPool.Instance.Get(prefab, pos, rot, parent); +/// // 无需手动回收,EffectAutoReturn 会在粒子播完后自动回池 +/// EffectPool.Instance.ClearAll(); // 切场景时彻底清理 +/// +public class EffectPool : Singleton +{ + // prefab.GetInstanceID() → 该 prefab 的池 + private Dictionary dic = new Dictionary(); + + // 活跃特效映射:特效GameObject → prefab.GetInstanceID() + private Dictionary activeMap = new Dictionary(); + + private const int MaxCachePerPrefab = 100; + + private GameObject poolRoot; + + /// 从池中取一个特效(池空则 Instantiate),取出后自动播放并在播完时回池 + public GameObject Get(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent) + { + if (prefab == null) return null; + + int prefabID = prefab.GetInstanceID(); + GameObject obj = null; + + if (dic.TryGetValue(prefabID, out var data)) + { + // 循环跳过已销毁的坏对象 + while (data.poolList.Count > 0) + { + obj = data.GetObj(); + if (obj != null) break; + obj = null; + } + } + + if (obj != null) + { + obj.transform.SetParent(parent, false); + obj.transform.position = position; + obj.transform.rotation = rotation; + obj.SetActive(true); + } + else + { + obj = GameObject.Instantiate(prefab, position, rotation, parent); + obj.SetActive(true); + } + + activeMap[obj] = prefabID; + + // 挂上自动回收组件(已挂则复用) + var autoReturn = obj.GetComponent(); + if (autoReturn == null) + autoReturn = obj.AddComponent(); + autoReturn.prefabID = prefabID; + autoReturn.Play(); + + return obj; + } + + /// 回收特效到池(通常由 EffectAutoReturn 自动调用) + public void Return(GameObject obj) + { + if (obj == null) return; + + if (!activeMap.TryGetValue(obj, out int prefabID) || prefabID == 0) + { + GameObject.Destroy(obj); + return; + } + activeMap.Remove(obj); + + if (!dic.TryGetValue(prefabID, out var data)) + dic[prefabID] = data = new BulletPoolData(EnsurePoolRoot().transform, prefabID); + + if (data.poolList.Count >= MaxCachePerPrefab) + { + GameObject.Destroy(obj); + return; + } + + data.PushObj(obj); + } + + /// 销毁所有活跃特效 + 清空所有池(切场景调用) + public void ClearAll() + { + foreach (var kv in activeMap.Keys) + { + if (kv != null) GameObject.Destroy(kv); + } + activeMap.Clear(); + + foreach (var kv in dic.Values) + kv.Clear(); + dic.Clear(); + + if (poolRoot != null) + { + GameObject.Destroy(poolRoot); + poolRoot = null; + } + } + + private GameObject EnsurePoolRoot() + { + if (poolRoot == null) + { + poolRoot = new GameObject("[EffectPool]"); + poolRoot.SetActive(false); + } + return poolRoot; + } +} + +/// 挂在池内特效上,粒子播放完自动回池 +public class EffectAutoReturn : MonoBehaviour +{ + [HideInInspector] public int prefabID; + + private ParticleSystem[] particles; + private float checkTimer; + private const float MinPlayTime = 0.3f; // 至少播 0.3s 再检测,避免第一帧 IsAlive=false 误判 + + /// 重新播放所有粒子 + public void Play() + { + if (particles == null || particles.Length == 0) + particles = GetComponentsInChildren(true); + + checkTimer = 0f; + foreach (var p in particles) + { + if (p != null) + { + p.Clear(true); + p.Play(true); + } + } + } + + void Update() + { + checkTimer += Time.deltaTime; + if (checkTimer < MinPlayTime) return; + + // 所有粒子都播完 → 回池 + if (particles == null || particles.Length == 0) + { + EffectPool.Instance.Return(gameObject); + return; + } + + bool anyAlive = false; + foreach (var p in particles) + { + if (p != null && p.IsAlive(true)) { anyAlive = true; break; } + } + if (!anyAlive) + { + EffectPool.Instance.Return(gameObject); + } + } +} diff --git a/Assets/Framework/Pool/EffectPool.cs.meta b/Assets/Framework/Pool/EffectPool.cs.meta new file mode 100644 index 000000000..eecda9e23 --- /dev/null +++ b/Assets/Framework/Pool/EffectPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e095098a1433eb4408b49fa20449bcb3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Framework/Pool/Pool.cs b/Assets/Framework/Pool/Pool.cs index a199af11b..ef89533b1 100644 --- a/Assets/Framework/Pool/Pool.cs +++ b/Assets/Framework/Pool/Pool.cs @@ -98,7 +98,13 @@ public class Pool : Singleton //{ // rotation = temp.transform.rotation; //} - obj = Object.Instantiate(FishResourcesData.GetFishPrefab(id), position, rotation,parent) as GameObject;//没有就读取一个,不用存入字典 + var prefab = FishResourcesData.GetFishPrefab(id); + if (prefab == null) + { + Debug.LogError($"[Pool] 找不到鱼 prefab, fishNumber={id}"); + return null; + } + obj = Object.Instantiate(prefab, position, rotation,parent) as GameObject;//没有就读取一个,不用存入字典 obj.SetActive(true); obj.name = id.ToString(); diff --git a/Assets/MyGame/Scenes/UIScene.unity b/Assets/MyGame/Scenes/UIScene.unity index 263213e84..228a21b2e 100644 --- a/Assets/MyGame/Scenes/UIScene.unity +++ b/Assets/MyGame/Scenes/UIScene.unity @@ -10153,7 +10153,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: afce420be78fe7e45bf80ca49529e276, type: 3} m_Name: m_EditorClassIdentifier: - enableBot: 0 + enableBot: 1 gameConfig: id: isDelete: 0 diff --git a/Assets/MyGame/Scripts/Items/Bullits/BullitMove.cs b/Assets/MyGame/Scripts/Items/Bullits/BullitMove.cs index 3c2e8e069..8e0956168 100644 --- a/Assets/MyGame/Scripts/Items/Bullits/BullitMove.cs +++ b/Assets/MyGame/Scripts/Items/Bullits/BullitMove.cs @@ -149,7 +149,7 @@ public class BullitMove : MonoBehaviour isTriggerFirst = false; if (effectPrefab != null) { - Instantiate(effectPrefab, transform.position, Quaternion.identity, transform.parent); + EffectPool.Instance.Get(effectPrefab, transform.position, Quaternion.identity, transform.parent); } if (userid == GameManager.Instance.user.id) @@ -215,7 +215,7 @@ public class BullitMove : MonoBehaviour { if (effectPrefab != null) { - Instantiate(effectPrefab, transform.position, Quaternion.identity, transform.parent); + EffectPool.Instance.Get(effectPrefab, transform.position, Quaternion.identity, transform.parent); } if (userid == GameManager.Instance.user.id) diff --git a/Assets/MyGame/Scripts/Manager/CreateFishManager.cs b/Assets/MyGame/Scripts/Manager/CreateFishManager.cs index 7c6a7daa1..2fe55d8db 100644 --- a/Assets/MyGame/Scripts/Manager/CreateFishManager.cs +++ b/Assets/MyGame/Scripts/Manager/CreateFishManager.cs @@ -1108,6 +1108,11 @@ public class CreateFishManager : MonoSingleton try { var fish = Pool.Instance.Get(int.Parse(createFishData.fishNumber), new Vector3(9999, 9999, 0), Quaternion.identity, fishRoot); + if (fish == null) + { + Debug.LogError($"[CreateFishManager] Pool.Get 返回 null,跳过此鱼: {createFishData.fishNumber}"); + return false; + } var mc = fish.GetComponent() ?? fish.AddComponent(); mc.monsterData.fishTypeConfig = FishDatas[createFishData.fishNumber]; mc.guid = createFishData.guid; diff --git a/Assets/MyGame/Scripts/Turret/SitPosData.cs b/Assets/MyGame/Scripts/Turret/SitPosData.cs index 4012c825d..743145842 100644 --- a/Assets/MyGame/Scripts/Turret/SitPosData.cs +++ b/Assets/MyGame/Scripts/Turret/SitPosData.cs @@ -172,6 +172,10 @@ public class SitPosData : MonoBehaviour /// public void LoadTurret(int turretNumber) { + // ⭐ 换炮台时只清活跃子弹(清屏),保留池缓存(同 prefab 能复用) + // 用 ClearActive 而非 ClearAll:ClearAll 会清池导致永远不复用 + BulletPool.Instance.ClearActive(); + if (currentTurret != null) { Destroy(currentTurret.gameObject); diff --git a/Assets/MyGame/aa/Game/DefaultBossControl.cs b/Assets/MyGame/aa/Game/DefaultBossControl.cs index 7c589bf93..0652ebc10 100644 --- a/Assets/MyGame/aa/Game/DefaultBossControl.cs +++ b/Assets/MyGame/aa/Game/DefaultBossControl.cs @@ -50,7 +50,8 @@ public class DefaultBossControl : MonsterControl_BossBase { //GetComponent().enabled = false; - currEffect_DieAward.SetActive(true); + if (currEffect_DieAward != null) + currEffect_DieAward.SetActive(true); } public override void OnMoveUpdata() { diff --git a/Assets/MyGame/aa/Game/FLZY/BOSS/JL/FLZY_JL_Boss.cs b/Assets/MyGame/aa/Game/FLZY/BOSS/JL/FLZY_JL_Boss.cs index 3d44111a4..6b43a54af 100644 --- a/Assets/MyGame/aa/Game/FLZY/BOSS/JL/FLZY_JL_Boss.cs +++ b/Assets/MyGame/aa/Game/FLZY/BOSS/JL/FLZY_JL_Boss.cs @@ -37,7 +37,8 @@ public class FLZY_JL_Boss : MonsterControl_BossBase { GetComponent().enabled = false; - currEffect_DieAward.SetActive(true); + if (currEffect_DieAward != null) + currEffect_DieAward.SetActive(true); } public override void DieFish(RoomKillNotice obj = null) diff --git a/Assets/MyGame/aa/Game/FLZY/BOSS/ZDX/FLZY_ZDX_BOSS.cs b/Assets/MyGame/aa/Game/FLZY/BOSS/ZDX/FLZY_ZDX_BOSS.cs index 21e68e5f3..24affb06a 100644 --- a/Assets/MyGame/aa/Game/FLZY/BOSS/ZDX/FLZY_ZDX_BOSS.cs +++ b/Assets/MyGame/aa/Game/FLZY/BOSS/ZDX/FLZY_ZDX_BOSS.cs @@ -47,6 +47,7 @@ public class FLZY_ZDX_BOSS : MonsterControl_BossBase { //GetComponent().enabled = false; - currEffect_DieAward.SetActive(true); + if (currEffect_DieAward != null) + currEffect_DieAward.SetActive(true); } } diff --git a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs index 8af3497a1..54892646d 100644 --- a/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs +++ b/Assets/MyGame/aa/UI/Game/GameMainPanel_FishBase.cs @@ -433,7 +433,9 @@ public class GameMainPanel_FishBase : GameMainPanel_Base EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_FreezeFish, SyncGameData_FreezeFish); EventCenter.Instance.RemoveEventListener(ConstEventString.SyncGameData_BotFireAngle, SyncGameData_BotFireAngle); - + // ⭐ 离场清理:子弹池 + 特效池彻底清空,防止内存泄漏 + BulletPool.Instance.ClearAll(); + EffectPool.Instance.ClearAll(); } public virtual void Start() @@ -1408,7 +1410,8 @@ public class GameMainPanel_FishBase : GameMainPanel_Base } else { - var lp = monster.lockPoint ?? monster.transform; + // 注意:Unity 序列化字段未赋值时是"假 null",?? 运算符无法识别,必须用 == null + var lp = (monster.lockPoint != null) ? monster.lockPoint : monster.transform; pos = lp.position; } if (!hitPos.gameObject.activeInHierarchy) @@ -1821,7 +1824,7 @@ public class GameMainPanel_FishBase : GameMainPanel_Base if (monster.colliderList != null && monster.colliderList.Count > 0 && data.colliderIndex != -1) hitPos = monster.colliderList[data.colliderIndex]; else - hitPos = monster.lockPoint ?? monster.transform; + hitPos = (monster.lockPoint != null) ? monster.lockPoint : monster.transform; list_Sits[sync.sitPos].SetLockTarget(monster, hitPos); }