This commit is contained in:
2026-08-04 21:20:56 +08:00
parent 3359483dfe
commit 8e475e6ab2
14 changed files with 823 additions and 171 deletions
+248 -116
View File
@@ -642,65 +642,137 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
if (data == null || data.fish == null || data.fish.Count == 0) return;
if (isFrozen) return; // 冰冻中不创建鱼
if (spawnDic == null) return; // 场景卸载后忽略
// ⭐ 分帧出鱼:普通鱼/黄金鱼分批生成(每帧5条),其他类型立即全出
// 避免一次出大量普通鱼导致单帧卡顿
var batchedQueue = new List<OnFishSpawnFishItem>(data.fish.Count);
foreach (var item in data.fish)
{
if (item == null) continue;
//BeijingTimeManager.Instance.SyncBeijingTime(() =>
//{
// Debug.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt) / 1000);
// Debug.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt));
// Debug.Log("差值:" + BeijingTimeManager.Instance.CurrentTimestampMs + "创建\n" + item.createdAt);
//});
Debug.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt) / 1000);
Debug.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt));
Debug.Log("差值:" + BeijingTimeManager.Instance.CurrentTimestampMs + "创建\n" + item.createdAt);
// ⭐ 鱼阵时 fishNumber 为空,用 fishArrayNumber 作为配置 key;
// 单体鱼时 fishNumber 非空,直接用 fishNumber
string cfgKey = string.IsNullOrEmpty(item.fishNumber) ? item.fishArrayNumber : item.fishNumber;
if (string.IsNullOrEmpty(cfgKey)) continue;
// guid 用服务端 fishId
string guid = item.fishId.ToString();
// 判断是否需要分帧:普通鱼(1)和黄金鱼(3)且非鱼阵 → 分帧
bool shouldBatch = ShouldBatchSpawn(item);
// guid 去重:已有此 guid 的鱼跳过
// ⭐ 鱼阵鱼的 guid 是 baseGuid_pathId_i 格式,字典里没有 baseGuid 本身,
// 所以要额外检查是否有以 baseGuid+"_" 开头的 key,防止 sync 模式重复出鱼
// 导致 guid 冲突覆盖(旧鱼变野鱼无法销毁)
if (!string.IsNullOrEmpty(guid) &&
fishDatas_hashCodekey != null &&
(fishDatas_hashCodekey.ContainsKey(guid) ||
fishDatas_hashCodekey.Keys.Any(k => k.StartsWith(guid + "_"))))
continue;
// 构造 FrameData_CreateFish 复用 SpawnFishMainThread
var createFishData = new FrameData_CreateFish
if (shouldBatch)
{
fishNumber = cfgKey,
guid = guid,
moveLineId = item.routeId.ToString()
};
// ⭐ 根据 type 决定 moveTime:
// - spawn/init:新鱼,传当前时间 → moveTime≈0,鱼从曲线起点开始游
// (避免服务端/客户端时间差导致 moveTime>0,鱼瞬间出现在曲线中间)
// - sync:同步已有鱼,传 createdAt → moveTime=时间差,鱼出现在路径正确位置
long spawnTime;
//if (data.type == "sync")
spawnTime = item.createdAt;
//else
// spawnTime = BeijingTimeManager.Instance.CurrentTimestampMs;
SpawnFishMainThread(createFishData, spawnTime);
// ⭐ expireAt 过期自动移除暂不启用:
// - 走路线的鱼:OnMoveUpdata 里 moveTime>=moveLine.moveTime 时自动 Id_DieFish_NotFX
// - 原点不动的鱼(Boss):正常播放结束动画后销毁
// 后续若需要服务端强制过期销毁,再启用此协程
// if (item.remainingSeconds > 0)
// StartCoroutine(ExpireFishCoroutine(guid, item.remainingSeconds));
batchedQueue.Add(item);
}
else
{
// Boss/特殊鱼/鱼阵等:立即生成
TrySpawnSingleFish(item);
}
}
Debug.Log($"[CreateFishManager] OnFishSpawn type={data.type}, count={data.fish.Count}");
// 分帧组:启动协程逐帧生成
if (batchedQueue.Count > 0)
{
StartCoroutine(SpawnFishBatchedCoroutine(batchedQueue));
}
NetLog.Log($"[CreateFishManager] OnFishSpawn type={data.type}, count={data.fish.Count}, batched={batchedQueue.Count}");
}
/// <summary>
/// 判断该鱼是否需要分帧生成。
/// 普通鱼(1)和黄金鱼(3)且非鱼阵 → 分帧;其他(Boss/特殊鱼/特殊Boss/鱼阵) → 立即出。
/// </summary>
private bool ShouldBatchSpawn(OnFishSpawnFishItem item)
{
// 鱼阵一次性全出(fishArrayNumber 非空表示鱼阵)
if (!string.IsNullOrEmpty(item.fishArrayNumber)) return false;
// 通过 fishNumber 查 FishTypeConfig 判断鱼类型
if (string.IsNullOrEmpty(item.fishNumber)) return false;
if (FishDatas != null && FishDatas.TryGetValue(item.fishNumber, out var cfg))
{
int ft = cfg.fishType;
// 普通鱼=1, 黄金鱼=3 → 分帧
return ft == (int)FishTypeEnum.普通鱼 || ft == (int)FishTypeEnum.黄金鱼;
}
// 查不到配置,默认分帧(保守处理,避免未知类型一次大量生成)
return true;
}
/// <summary>
/// 分帧出鱼协程:每帧生成 batchSize 条,避免单帧卡顿。
/// </summary>
private IEnumerator SpawnFishBatchedCoroutine(List<OnFishSpawnFishItem> queue)
{
const int batchSize = 5; // 每帧生成5条
int index = 0;
while (index < queue.Count)
{
// 冰冻中持续等待解冻,避免冰冻期间继续生成鱼
while (isFrozen) yield return null;
int end = Mathf.Min(index + batchSize, queue.Count);
for (int i = index; i < end; i++)
{
TrySpawnSingleFish(queue[i]);
}
index = end;
yield return null; // 等待下一帧
}
}
/// <summary>
/// 尝试生成单条鱼(含去重检查)。返回是否成功生成。
/// </summary>
private bool TrySpawnSingleFish(OnFishSpawnFishItem item)
{
if (item == null) return false;
NetLog.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt) / 1000);
NetLog.Log("差值:" + (BeijingTimeManager.Instance.CurrentTimestampMs - item.createdAt));
NetLog.Log("差值:" + BeijingTimeManager.Instance.CurrentTimestampMs + "创建\n" + item.createdAt);
// ⭐ 鱼阵时 fishNumber 为空,用 fishArrayNumber 作为配置 key;
// 单体鱼时 fishNumber 非空,直接用 fishNumber
string cfgKey = string.IsNullOrEmpty(item.fishNumber) ? item.fishArrayNumber : item.fishNumber;
if (string.IsNullOrEmpty(cfgKey)) return false;
// guid 用服务端 fishId
string guid = item.fishId.ToString();
// guid 去重:已有此 guid 的鱼跳过
// ⭐ 鱼阵鱼的 guid 是 baseGuid_pathId_i 格式,字典里没有 baseGuid 本身,
// 所以要额外检查是否有以 baseGuid+"_" 开头的 key,防止 sync 模式重复出鱼
// 导致 guid 冲突覆盖(旧鱼变野鱼无法销毁)
// ⭐ 优化:用 foreach 代替 LINQ Any(StartsWith),消除 Lambda 闭包和迭代器 GC
if (!string.IsNullOrEmpty(guid) && fishDatas_hashCodekey != null)
{
if (fishDatas_hashCodekey.ContainsKey(guid)) return false;
string guidPrefix = guid + "_";
bool hasPrefixMatch = false;
foreach (var key in fishDatas_hashCodekey.Keys)
{
if (key != null && key.StartsWith(guidPrefix))
{
hasPrefixMatch = true;
break;
}
}
if (hasPrefixMatch) return false;
}
// 构造 FrameData_CreateFish 复用 SpawnFishMainThread
var createFishData = new FrameData_CreateFish
{
fishNumber = cfgKey,
guid = guid,
moveLineId = item.routeId.ToString()
};
// ⭐ 根据 type 决定 moveTime:
// - spawn/init:新鱼,传当前时间 → moveTime≈0,鱼从曲线起点开始游
// (避免服务端/客户端时间差导致 moveTime>0,鱼瞬间出现在曲线中间)
// - sync:同步已有鱼,传 createdAt → moveTime=时间差,鱼出现在路径正确位置
long spawnTime = item.createdAt;
SpawnFishMainThread(createFishData, spawnTime);
return true;
}
/// <summary>
@@ -1203,6 +1275,8 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
Destroy(bjEffectRoot.gameObject);
bjEffectRoot = null;
}
// ⭐ 清理特效池:bjEffectRoot 销毁后池内对象失效,清空避免复用已销毁对象
EffectPool.Instance.ClearAll();
//for (int i = fishDatas_hashCodekey.childCount - 1; i >= 0; i--)
// Destroy(fishRoot.GetChild(i).gameObject);
}
@@ -1221,50 +1295,77 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
// 随机工具方法
// ================================================================
// ⭐ 静态复用 Random 实例,避免每次调用 new System.Random() 产生 GC
private static readonly System.Random _sharedRand = new System.Random();
// ⭐ 预分配结果 List,避免调用方反复 new List<int>
private static readonly List<int> _reusableIntResult = new List<int>(64);
/// <summary>
/// 从 [min, max) 区间随机取 count 个不重复整数。
/// 若 min==max 返回空列表。
/// ⭐ 优化:复用静态 Random + 预分配 List,零 GC 分配。
/// 注意:返回的 List 是内部复用对象,调用方用完前不要再次调用本方法。
/// </summary>
public static List<int> RandomUniqueNumbers(int count, int min, int max)
{
_reusableIntResult.Clear();
if (min >= max)
{
Debug.LogWarning("[CreateFishManager] RandomUniqueNumbers: min >= max,返回空列表");
return new List<int>();
return _reusableIntResult;
}
count = Mathf.Min(count, max - min); // 不能超过范围
var result = new HashSet<int>();
var rand = new System.Random();
// ⭐ 用 HashSet 去重,但复用静态实例避免每次 new
_reusableIntSet.Clear();
while (_reusableIntSet.Count < count)
_reusableIntSet.Add(_sharedRand.Next(min, max));
foreach (var v in _reusableIntSet)
_reusableIntResult.Add(v);
return _reusableIntResult;
}
private static readonly HashSet<int> _reusableIntSet = new HashSet<int>(64);
while (result.Count < count)
result.Add(rand.Next(min, max));
return result.ToList();
// ⭐ 预分配 string 结果 List,避免每次调用 new List<string>
private static readonly List<string> _reusableStrResult = new List<string>(64);
// ⭐ 预分配 key 缓冲,避免 keyCollection.ToList() 产生 GC
private static readonly List<string> _reusableKeyBuf = new List<string>(128);
// ⭐ 预分配索引 HashSet,避免每次 new HashSet
private static readonly HashSet<int> _reusableIdxSet = new HashSet<int>(64);
// ⭐ 缓存 uiRoot.Canvas,避免每次杀鱼特效都 GetComponent<Canvas>()(5处复用)
private static Canvas _cachedUiCanvas;
private static Canvas GetUiCanvas()
{
if (_cachedUiCanvas == null && UIManager.Instance != null && UIManager.Instance.uiRoot != null)
_cachedUiCanvas = UIManager.Instance.uiRoot.GetComponent<Canvas>();
return _cachedUiCanvas;
}
/// <summary>
/// 从 KeyCollection 中随机取 count 个不重复的 string key。
/// ⭐ 优化:复用静态 Random + 预分配缓冲,零 GC 分配。
/// 注意:返回的 List 是内部复用对象,调用方用完前不要再次调用本方法。
/// </summary>
public static List<string> RandomUniqueNumbers(int count, int min, int max,
Dictionary<string, List<RoomFishDetailConfig>>.KeyCollection keyCollection)
{
if (min >= max) return new List<string>();
_reusableStrResult.Clear();
if (min >= max) return _reusableStrResult;
count = Mathf.Min(count, max - min);
var keyList = keyCollection.ToList();
var indices = new HashSet<int>();
var rand = new System.Random();
var result = new List<string>(count);
// ⭐ 用 foreach 拷贝到复用缓冲,避免 keyCollection.ToList() 的 GC
_reusableKeyBuf.Clear();
foreach (var k in keyCollection) _reusableKeyBuf.Add(k);
_reusableIdxSet.Clear();
while (indices.Count < count)
while (_reusableIdxSet.Count < count)
{
int idx = rand.Next(min, max);
if (indices.Add(idx))
result.Add(keyList[idx]);
int idx = _sharedRand.Next(min, max);
if (_reusableIdxSet.Add(idx))
_reusableStrResult.Add(_reusableKeyBuf[idx]);
}
return result;
return _reusableStrResult;
}
#endregion
@@ -1312,20 +1413,28 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
int count, List<RedPacketRecord> coinNum, Vector3 v3Point, int killerSitPos = -1)
{
var guids = new List<string>(count);
var tempKeys = fishDatas_hashCodekey_Small.Keys.ToList();
// ⭐ 优化:用 Fisher-Yates 洗牌索引代替 Keys.ToList()+RemoveAt,零 GC
// 将 keys 拷贝到复用缓冲
_reusableKeyBuf.Clear();
foreach (var k in fishDatas_hashCodekey_Small.Keys) _reusableKeyBuf.Add(k);
// 若存活鱼不足 count,全部选取
if (tempKeys.Count <= count)
int keyCount = _reusableKeyBuf.Count;
if (keyCount <= count)
{
guids.AddRange(tempKeys);
// 若存活鱼不足 count,全部选取
for (int i = 0; i < keyCount; i++) guids.Add(_reusableKeyBuf[i]);
}
else
{
// ⭐ Fisher-Yates 洗牌:只洗前 count 个位置,避免全量洗牌
for (int i = 0; i < count; i++)
{
int idx = Random.Range(0, tempKeys.Count);
guids.Add(tempKeys[idx]);
tempKeys.RemoveAt(idx);
int j = Random.Range(i, keyCount);
// 交换 i 和 j
var tmp = _reusableKeyBuf[i];
_reusableKeyBuf[i] = _reusableKeyBuf[j];
_reusableKeyBuf[j] = tmp;
guids.Add(_reusableKeyBuf[i]);
}
}
@@ -1359,13 +1468,24 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
{
if (!fishDatas_hashCodekey.TryGetValue(fishId, out var monster))
{
#if UNITY_EDITOR
// ⭐ 优化:调试日志用 #if UNITY_EDITOR 包裹,Release 包零 GC
// 原代码 Where().Take().ToList() + string.Join + $"" 每次 NotFound 都产生大量 GC
Debug.LogWarning($"[CreateFishManager] Id_DieFish_NotFX 找不到鱼: fishId={fishId}, 字典数量={fishDatas_hashCodekey.Count}");
// ⭐ 记录找不到鱼日志:这是bug的核心症状。附带 baseGuid 前缀匹配,看是否存在 guid 格式不一致
string baseGuid = fishId.Contains("_") ? fishId.Split('_')[0] : fishId;
var similarKeys = fishDatas_hashCodekey.Keys.Where(k => k.StartsWith(baseGuid)).Take(10).ToList();
string similarStr = similarKeys.Count > 0 ? string.Join(",", similarKeys) : "(无)";
GameManager.LogFishFlow("NOT_FOUND", fishId,
$"dictCount={fishDatas_hashCodekey.Count} baseGuid={baseGuid} 相似key={similarStr}");
int matchCount = 0;
foreach (var k in fishDatas_hashCodekey.Keys)
{
if (k.StartsWith(baseGuid))
{
matchCount++;
if (matchCount <= 10) GameManager.LogFishFlow("NOT_FOUND", fishId, $"similarKey={k}");
}
}
if (matchCount == 0) GameManager.LogFishFlow("NOT_FOUND", fishId, "(无相似key)");
#else
GameManager.LogFishFlow("NOT_FOUND", fishId, fishDatas_hashCodekey.Count.ToString());
#endif
return;
}
Pool.Instance.Return(monster.gameObject);
@@ -1435,22 +1555,24 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
}
// 死亡粒子特效
Instantiate(
// 死亡粒子特效(池化,粒子播完自动回池)
EffectPool.Instance.Get(
monster.effect_Die != null ? monster.effect_Die : dieFXPrefab,
monster.transform.position, Quaternion.identity, transform.parent);
// 非 Boss 鱼:金币飞向炮台
if (monster.fishenum != FishTypeEnum.Boss鱼 && monster.fishenum != FishTypeEnum.特殊Boss)
{
var award = Instantiate(
// ⭐ 池化:动画事件(coinEvent/ScoreNumScrollEvent)播完手动回池
var award = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
currGameType.subPanelParent.parent);
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
UIManager.Instance.uiRoot.GetComponent<Canvas>());
GetUiCanvas());
var coinScript = award.GetComponent<BJBase>();
if (coinScript != null)
@@ -1518,12 +1640,13 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
// 2. 屏幕坐标 → Canvas 坐标(跟你原来金币飞行逻辑保持一致)
Vector2 canvasPos = CoordinateTransformation.ScreenPoint_ToCanvas(
screenPos,
UIManager.Instance.uiRoot.GetComponent<Canvas>());
GetUiCanvas());
// 3. 实例化金币特效(沿用你原来的 award 逻辑)
var award = Instantiate(
dieFXPrefabAward, // 或者用某个专门的"掉金币" prefab
currGameType.subPanelParent.parent);
// 3. 实例化金币特效(池化,动画事件播完手动回池)
var award = EffectPool.Instance.Get(
dieFXPrefabAward,
targetWorldPos, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
var rect = award.GetComponent<RectTransform>();
rect.anchoredPosition = canvasPos;
@@ -1547,7 +1670,7 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
var killFish = JsonConvert.DeserializeObject<WebSocketResponseBase<RoomKillNotice>>(msg)
.arguments[0];
Debug.Log($"[CreateFishManager] 杀鱼延迟:{DateTimeOffset.Now.ToUnixTimeSeconds() - killFish.timespan}s");
NetLog.Log($"[CreateFishManager] 杀鱼延迟:{DateTimeOffset.Now.ToUnixTimeSeconds() - killFish.timespan}s");
SitPosData sitData = null; //击杀玩家座位数据
foreach (var item in currGameType.list_Sits)
{
@@ -1565,8 +1688,8 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
if (!fishDatas_hashCodekey.TryGetValue(killFish.fishId, out var monster)) return;
// 死亡粒子特效
Instantiate(
// 死亡粒子特效(池化,粒子播完自动回池)
EffectPool.Instance.Get(
monster.effect_Die != null ? monster.effect_Die : dieFXPrefab,
monster.transform.position, Quaternion.identity, transform.parent);
int goldToAdd = killFish.gold; // 立即复制需要的数据
@@ -1594,16 +1717,17 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
{
case FishTypeEnum.普通鱼:
{
// 普通鱼:金币飞向炮台
var award = Instantiate(
// 普通鱼:金币飞向炮台(池化,动画事件播完手动回池)
var award = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
currGameType.subPanelParent.parent);
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
UIManager.Instance.uiRoot.GetComponent<Canvas>());
GetUiCanvas());
}
catch
{
@@ -1658,16 +1782,19 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
if (bossCtrl.isToUI)
{
bossCtrl.currEffect_DieAward = Instantiate(
// ⭐ 池化:UI 空间 Boss 爆奖,动画事件播完手动回池
bossCtrl.currEffect_DieAward = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
currGameType.subPanelParent.parent);
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
}
else
{
// isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理
bossCtrl.currEffect_DieAward = Instantiate(
// isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理(池化)
bossCtrl.currEffect_DieAward = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
bjEffectRoot);
monster.transform.position, Quaternion.identity, bjEffectRoot,
autoReturn: false);
}
bossCtrl.action_effect_DieAward?.Invoke(bossCtrl.currEffect_DieAward);
@@ -1676,16 +1803,17 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
break;
case FishTypeEnum.黄金鱼:
{
// 普通鱼:金币飞向炮台
var award = Instantiate(
// 黄金鱼:金币飞向炮台(池化,动画事件播完手动回池)
var award = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
currGameType.subPanelParent.parent);
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
UIManager.Instance.uiRoot.GetComponent<Canvas>());
GetUiCanvas());
}
catch
{
@@ -1741,16 +1869,17 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
}
}
// 普通鱼:金币飞向炮台
var award = Instantiate(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward);
// 特殊鱼:金币飞向炮台(池化,动画事件播完手动回池)
var award = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAward,
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
try
{
award.GetComponent<RectTransform>().anchoredPosition =
CoordinateTransformation.WorldPos_ToCanvas(
monster.transform.position,
UIManager.Instance.uiRoot.GetComponent<Canvas>());
award.transform.SetParent(currGameType.subPanelParent.parent);
GetUiCanvas());
}
catch
{
@@ -1852,16 +1981,19 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
if (bossCtrl.isToUI)
{
bossCtrl.currEffect_DieAward = Instantiate(
// ⭐ 池化:UI 空间 Boss 爆奖,动画事件播完手动回池
bossCtrl.currEffect_DieAward = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
currGameType.subPanelParent.parent);
monster.transform.position, Quaternion.identity, currGameType.subPanelParent.parent,
autoReturn: false);
}
else
{
// isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理
bossCtrl.currEffect_DieAward = Instantiate(
// isToUI=false:世界空间特效,无 UI 父对象 → 挂 bjEffectRoot 统一管理(池化)
bossCtrl.currEffect_DieAward = EffectPool.Instance.Get(
monster.effect_DieAward != null ? monster.effect_DieAward : dieFXPrefabAwardTmp,
bjEffectRoot);
monster.transform.position, Quaternion.identity, bjEffectRoot,
autoReturn: false);
}
bossCtrl.action_effect_DieAward?.Invoke(bossCtrl.currEffect_DieAward);