using Assets.MyGame.Scripts.Net.APIResponses.Enum; using DG.Tweening; using Newtonsoft.Json.Linq; using System; using System.Collections; using System.Collections.Generic; using UnityEngine; /// /// 星际巡航 Boss 爆奖事件(继承 BJEventBase)。 /// /// ==================== 美术规格(4种形态) ==================== /// /// 形态一(富甲天下,无道具武器): /// Settle: PW_Effect_Settle_FuJiaTianXia (Node子物体指认Source Image为tj_fcboss_0) /// /// 形态二(铁拳 IronFist, weaponID=6): /// 1. FullScreen(PW_Effect_FullScreen_Symmetra): 依次激活 tap_tip → knock_tip(Source Image=Prop_Poster_IronFist) → 隐藏Tip /// 2. Prop(PW_Prop_IronFist): 激活Hole子对象 → Hole从死亡位置移动到屏幕左下角 → 隐藏Hole → 激活Hand /// 3. TextCrit: PW_Effect_TextCrit_Other / PW_Effect_TextCrit_Self (满屏暴击) /// 4. AwardFrame: PW_Effect_AwardFrame_IronFist /// 5. Settle: PW_Effect_Settle_IronFist_Other /// /// 形态三(光子 Symmetra, weaponID=8): /// 1. FullScreen(PW_Effect_FullScreen_Symmetra): 依次激活 tap_tip → knock_tip(Source Image=Prop_Poster_Symmetra) → 隐藏Tip /// 2. Prop(PW_Prop_Symmetra): 从死亡位置移动到屏幕中心 → 隐藏Lock → PropL和PropR分别移动到屏幕两边 → 激活laser子物体 /// 3. AwardFrame: PW_Effect_AwardFrame_Symmetra /// 4. Settle: PW_Effect_Settle_Symmetra_Other / PW_Effect_Settle_Symmetra_Self /// /// 形态四(伽马风暴 GammaStorm, weaponID=7): /// 1. FullScreen(PW_Effect_FullScreen_GammaStorm): 依次激活 tap_tip → knock_tip(Source Image=Prop_Poster_GammaStorm) → 隐藏Tip /// 2. Prop(PW_Prop_GammaStorm): 从死亡位置移动到屏幕中心 → 根据视频内容激活特效 /// 3. TextCrit: PW_Effect_TextCrit_Other / PW_Effect_TextCrit_Self (满屏暴击) /// 4. Crit: PW_Effect_Crit_GammaStorm (暴击倍率) /// 5. AwardFrame: PW_Effect_AwardFrame_GammaStorm /// 6. Settle: PW_Effect_Settle_GammaStorm_Other /// /// ⭐ 注意: 铁拳(形态二)的全屏 UI 框按美术规格用 PW_Effect_FullScreen_Symmetra, /// 海报(Source Image) 仍设置 Prop_Poster_IronFist。 /// /// ==================== 完整流程 ==================== /// /// Init → 解析 weaponID → 道具出场 → 全屏特效(tap_tip/knock_tip序列) → 道具动画 /// → 奖励框 → CallBJ发红包 → 暴击文字 → 结算特效 → EffEnd /// → DelayThenScore → StartAddScore → MoveToSit → OnScoreEnd → JiaFen → die /// public class XingJiXunHangEvent : BJRedPacketEvent { // ==================== 形态一:富甲天下(无道具武器) ==================== [Header("形态一 - 富甲天下")] [Tooltip("富甲天下结算特效(PW_Effect_Settle_FuJiaTianXia_XingJiXunHang)")] public GameObject settleFuJiaTianXia; // ==================== 形态二:铁拳 (weaponID=6) ==================== [Header("形态二 - 铁拳 IronFist")] [Tooltip("铁拳道具预制体(PW_Prop_IronFist Variant)")] public GameObject propIronFist; [Tooltip("铁拳全屏特效(PW_Effect_FullScreen_IronFist_XingJiXunHang)")] public GameObject fullScreenIronFist; [Tooltip("铁拳奖励框(PW_Effect_AwardFrame_IronFist_XingJiXunHang)")] public GameObject awardFrameIronFist; [Tooltip("铁拳结算特效-自己(PW_Effect_Settle_IronFist_Self_XingJiXunHang)")] public GameObject settleIronFistSelf; [Tooltip("铁拳结算特效-他人(PW_Effect_Settle_IronFist_Other_XingJiXunHang)")] public GameObject settleIronFistOther; [Tooltip("铁拳海报Sprite(Prop_Poster_IronFist),设置到FullScreen的knock_tip上")] public Sprite posterIronFist; // ==================== 形态三:光子 (weaponID=8) ==================== [Header("形态三 - 光子 Symmetra")] [Tooltip("光子道具预制体(PW_Prop_Symmetra Variant)")] public GameObject propSymmetra; [Tooltip("光子全屏特效(PW_Effect_FullScreen_Symmetra_XingJiXunHang)")] public GameObject fullScreenSymmetra; [Tooltip("光子奖励框(PW_Effect_AwardFrame_Symmetra_XingJiXunHang)")] public GameObject awardFrameSymmetra; [Tooltip("光子结算特效-自己(PW_Effect_Settle_Symmetra_Self_XingJiXunHang)")] public GameObject settleSymmetraSelf; [Tooltip("光子结算特效-他人(PW_Effect_Settle_Symmetra_Other_XingJiXunHang)")] public GameObject settleSymmetraOther; [Tooltip("光子海报Sprite(Prop_Poster_Symmetra),设置到FullScreen的knock_tip上")] public Sprite posterSymmetra; // ==================== 形态四:伽马风暴 (weaponID=7) ==================== [Header("形态四 - 伽马风暴 GammaStorm")] [Tooltip("伽马风暴道具预制体(PW_Prop_GammaStorm Variant)")] public GameObject propGammaStorm; [Tooltip("伽马风暴全屏特效(PW_Effect_FullScreen_GammaStorm_XingJiXunHang)")] public GameObject fullScreenGammaStorm; [Tooltip("伽马风暴奖励框(PW_Effect_AwardFrame_GammaStorm_XingJiXunHang)")] public GameObject awardFrameGammaStorm; [Tooltip("伽马风暴结算特效-自己(PW_Effect_Settle_GammaStorm_Self_XingJiXunHang)")] public GameObject settleGammaStormSelf; [Tooltip("伽马风暴结算特效-他人(PW_Effect_Settle_GammaStorm_Other_XingJiXunHang)")] public GameObject settleGammaStormOther; [Tooltip("伽马风暴暴击框(PW_Effect_Crit_GammaStorm_XingJiXunHang)")] public GameObject critFrameGammaStorm; [Tooltip("伽马风暴海报Sprite(Prop_Poster_GammaStorm),设置到FullScreen的knock_tip上")] public Sprite posterGammaStorm; // ==================== 满屏暴击文字(铁拳/伽马风暴共用) ==================== [Header("满屏暴击文字")] [Tooltip("满屏暴击-自己(PW_Effect_TextCrit_Self_XingJiXunHang)")] public GameObject textCritSelf; [Tooltip("满屏暴击-他人(PW_Effect_TextCrit_Other_XingJiXunHang)")] public GameObject textCritOther; [Tooltip("暴击分数 TMP 所在子物体名(预制体里数字节点,默认 Text)")] public string critTextNodeName = "Text"; [Tooltip("暴击图子物体名(预制体里\"暴击\"图,从 0 延时弹出,默认 Crit)")] public string critNodeName = "Crit"; [Tooltip("暴击文字弹出后停留时长(秒),之后整体缩小消失(默认0.8)")] public float critHoldDuration = 0.8f; // ==================== 道具出场特效 ==================== [Header("道具出场")] [Tooltip("道具出场特效(三种武器共用,PW_Effect_Appear_Prop)")] public GameObject appearPropEffect; // ==================== FullScreen 子物体名称配置 ==================== [Header("FullScreen 子物体名称")] [Tooltip("tap_tip 子物体名(点击提示)")] public string tapTipName = "tap_tip"; [Tooltip("knock_tip 子物体名(敲击提示,需设置海报Sprite)")] public string knockTipName = "knock_tip"; [Tooltip("Tip 容器子物体名(tap_tip/knock_tip 的父级,序列结束后隐藏,所有形态通用)")] public string tipContainerName = "Tip"; // ==================== Prop 子物体名称配置 ==================== [Header("铁拳 Prop 子物体名称")] [Tooltip("铁拳-Hole子物体名(从死亡位置移到屏幕左下角)")] public string ironFistHoleName = "Hole"; [Tooltip("铁拳-Hand子物体名(Hole隐藏后激活)")] public string ironFistHandName = "Hand"; [Tooltip("铁拳-Hole终点偏移(世界单位,往屏幕内偏移,如 (1.5, 1, 0) 从左下角往右上挪一点)")] public Vector3 ironFistCornerOffset = new Vector3(1.5f, 1f, 0f); [Tooltip("铁拳-Hole 到位后停留时长(秒),让玩家看清再释放 Hand(默认0.5)")] public float ironFistHoldDelay = 0.5f; [Header("铁拳 - 击打杀鱼")] [Tooltip("铁拳杀鱼检测半径(世界单位,Hand 击打范围)")] public float ironFistKillRadius = 2.5f; [Tooltip("铁拳杀鱼检测层(Fish)")] public LayerMask ironFistKillLayer; [Tooltip("铁拳是否杀鱼(被击中的飞机死亡,金币从左下角飞向炮台)")] public bool ironFistKillFish = true; [Tooltip("从近到远逐个杀鱼的总时长(秒,默认1秒)")] public float ironFistKillTotalTime = 1f; [Tooltip("Hand 激活后延迟(秒,默认2),延迟结束隐藏 Hand 并开始死飞机/爆金币")] public float ironFistAwardDelay = 2f; [Header("光子 Prop 子物体名称")] [Tooltip("光子-Lock子物体名(到屏幕中心后隐藏)")] public string symmetraLockName = "Lock"; [Tooltip("光子-PropL子物体名(移到屏幕左边)")] public string symmetraPropLName = "PropL"; [Tooltip("光子-PropR子物体名(移到屏幕右边)")] public string symmetraPropRName = "PropR"; [Tooltip("光子-laser子物体名(PropL/PropR到位后激活)")] public string symmetraLaserName = "laser"; [Tooltip("光子-laser 激活后上下摆动幅度(世界单位)")] public float laserSwingAmplitude = 0.5f; [Tooltip("光子-laser 激活后上下摆动来回次数(0=不摆动)")] public int laserSwingCycles = 0; [Header("光子 - 4条激光伸展")] [Tooltip("laser1 子物体名(向 X 正方向伸到屏幕最右)")] public string laser1Name = "laser1"; [Tooltip("laser2 子物体名(向 -Y 方向伸到屏幕最下)")] public string laser2Name = "laser2"; [Tooltip("laser3 子物体名(向 +Y 方向伸到屏幕最上)")] public string laser3Name = "laser3"; [Tooltip("laser4 子物体名(向 -X 方向伸到屏幕最左)")] public string laser4Name = "laser4"; [Tooltip("4条激光伸展到最长位置的耗时(秒)")] public float laserExpandSpeed = 0.6f; [Tooltip("光子激光结束后等待时长(秒),然后直接隐藏 Prop(默认1)")] public float symmetraPropHideDelay = 1f; [Header("光子 - 电网扫动杀鱼")] [Tooltip("电网(Prop整体)上下扫动幅度(世界单位)")] public float netSweepAmplitude = 5f; [Tooltip("电网上下扫动来回次数(0=不扫动)")] public int netSweepCycles = 3; [Tooltip("电网每次扫动耗时(秒)")] public float netSweepSpeed = 0.8f; [Tooltip("电网杀鱼检测半径(世界单位,电网线两侧厚度)")] public float netKillRadius = 1f; [Tooltip("电网杀鱼检测层(Fish,Inspector 选 Fish 层)")] public LayerMask netKillLayer; [Tooltip("电网是否杀鱼(碰到飞机飞机爆炸)")] public bool netKillFish = true; [Header("伽马风暴 - 四射线旋转")] [Tooltip("四根射线子物体名(伽马风暴激光,按顺序填;预制体内为小写 laser1~4)")] public string[] gammaLaserNames = { "laser1", "laser2", "laser3", "laser4" }; [Tooltip("射线显示后到开始旋转的等待(秒,默认1)")] public float gammaRotateDelay = 1f; [Tooltip("总旋转角度(度,默认90°,转完为止)")] public float gammaRotateAngle = 90f; [Tooltip("转到该角度后激光开始一点一点缩短(度,默认45°)")] public float gammaShrinkStartAngle = 45f; [Tooltip("旋转速度(度/秒,正=逆时针)")] public float gammaRotateSpeed = 45f; [Tooltip("伽马风暴射线杀鱼检测层(Fish)")] public LayerMask gammaKillLayer; [Tooltip("伽马风暴射线是否杀鱼(碰到激光的飞机直接死亡)")] public bool gammaKillFish = true; // ==================== 爆奖配置 ==================== [Header("爆奖配置")] [Tooltip("爆奖红包次数(CallBJ 调用次数)")] public int bjCount = 1; [Tooltip("每次 CallBJ 最小红包数")] public int bjMinCount = 10; [Tooltip("每次 CallBJ 最大红包数")] public int bjMaxCount = 15; [Header("时间配置")] [Tooltip("道具出场特效持续时间")] public float appearDuration = 1f; [Tooltip("tap_tip 显示持续时间")] public float tapTipDuration = 0.5f; [Tooltip("knock_tip 显示持续时间")] public float knockTipDuration = 0.8f; [Tooltip("全屏特效总持续时间")] public float fullScreenDuration = 2f; [Tooltip("道具动画持续时间(Hole移动/Prop移动等)")] public float propAnimDuration = 1.5f; [Tooltip("结算特效持续时间")] public float settleDuration = 3f; // ==================== 运行时状态 ==================== /// Boss 引用(爆奖结束时通知 Boss 回收) [System.NonSerialized] public XingJiXunHang_BOSS bossCtrl; /// Boss 死亡位置(世界坐标,由 Boss 传入) [HideInInspector] public Vector3 diePos = Vector3.zero; /// 服务端返回的武器 ID(6=IronFist, 7=GammaStorm, 8=Symmetra, 0=富甲天下) private int _weaponId = 0; /// 武器标签(GammaStorm / IronFist / Symmetra / FuJiaTianXia) private string _weaponTag = ""; /// 当前选中的结算预制体 private GameObject _settlePrefab; /// 当前选中的奖励框预制体 private GameObject _awardFramePrefab; /// 当前选中的全屏特效预制体 private GameObject _fullScreenPrefab; /// 当前选中的道具预制体 private GameObject _propPrefab; /// 当前选中的海报Sprite private Sprite _posterSprite; /// 本局电网已炸的鱼 guid(防止扫动中重复炸同一鱼) private readonly HashSet _netKilledFish = new HashSet(); /// Boss 分数面板 TMP(挂在独立 UI 层,Boss 删除后仍有效) private TMPro.TextMeshProUGUI _bossScoreTmp; /// 本局杀鱼累计加到 Boss 面板的分值 private long _bossPanelScore = 0; /// 爆奖过程中实例化的所有临时特效(die() 时统一销毁,防止残留) private readonly System.Collections.Generic.List _spawnedEffects = new System.Collections.Generic.List(); /// UI 层父节点(UI 类特效:FullScreen/AwardFrame/Settle/Crit 挂这里) private Transform _uiLayer; /// UI Canvas(世界坐标 → UI 坐标转换用) private Canvas _uiCanvas; // ==================== 初始化 ==================== public override void Init(long goldCount, int posID, long paobei, long fishScore, int beishu, Action action, Action action1 = null) { base.Init(goldCount, posID, paobei, fishScore, beishu, action, action1); // 恢复位置到 Boss 死亡位置 transform.position = diePos; // 设置爆奖次数 scoreCount = bjCount; totalBJCount = bjCount; // ⭐ 星际巡航不需要 ScoreNumScroll(Clone) 加分数字滚动特效(杀鱼分已加 Boss 面板) showAddScoreFX = false; // ⭐ 爆奖只分包不关联场上鱼(已有完整的道具武器/全屏/结算动画, // 避免全屏随机位置死飞机爆奖) bigPrizeFishMode = BigPrizeFishMode.NoFish; // 解析武器 ID ParseWeaponId(); // 根据武器 ID 选择预制体 SelectWeaponPrefabs(); // 启动完整爆奖流程 StartCoroutine(PropWeaponFlow()); } /// 重写:不移动到屏幕中心,但缓存座位引用 protected override void ApplySeatOffset() { GetSeatPosition(); } /// 重置子类运行时状态 protected override void ResetState() { base.ResetState(); _weaponId = 0; _weaponTag = ""; _settlePrefab = null; _awardFramePrefab = null; _fullScreenPrefab = null; _propPrefab = null; _posterSprite = null; bossCtrl = null; diePos = Vector3.zero; _netKilledFish.Clear(); _bossScoreTmp = null; _bossPanelScore = 0; // ⭐ 对象池复用:清理上次残留的临时特效 for (int i = 0; i < _spawnedEffects.Count; i++) { if (_spawnedEffects[i] != null) Destroy(_spawnedEffects[i]); } _spawnedEffects.Clear(); } // ==================== 武器 ID 解析 ==================== private void ParseWeaponId() { if (obj == null) { Debug.LogWarning("[XingJiXunHang] obj 为 null,无法解析 weaponID,走富甲天下"); _weaponId = 0; return; } try { JToken root = obj is JToken t ? t : JToken.Parse(obj.ToString()); _weaponId = root["weaponID"]?.Value() ?? root["weaponId"]?.Value() ?? root["weapon_id"]?.Value() ?? 0; // ⭐ 服务端现在用 animId 区分三种带死亡的爆奖(weaponID 可能不再下发): // animId=2 → 形态二 铁拳 IronFist (weaponID=6) // animId=3 → 形态三 光子 Symmetra (weaponID=8) // animId=4 → 形态四 伽马风暴 GammaStorm (weaponID=7) if (_weaponId == 0) { int animId = root["animId"]?.Value() ?? 0; switch (animId) { case 2: _weaponId = 6; break; // 铁拳 case 3: _weaponId = 8; break; // 光子 case 4: _weaponId = 7; break; // 伽马风暴 } if (animId >= 2) Debug.Log($"[XingJiXunHang] 无 weaponID,按 animId={animId} 映射为 weaponID={_weaponId}"); } Debug.Log($"[XingJiXunHang] 解析武器 ID: weaponID={_weaponId}"); } catch (Exception e) { Debug.LogError($"[XingJiXunHang] 解析 weaponID 失败: {e.Message}; 原始={obj}"); _weaponId = 0; } } /// 根据武器 ID 选择对应的预制体和标签 private void SelectWeaponPrefabs() { _weaponId = 6; switch (_weaponId) { case 0: // 形态一:富甲天下(无道具武器) _weaponTag = "FuJiaTianXia"; _settlePrefab = settleFuJiaTianXia; _awardFramePrefab = null; _fullScreenPrefab = null; _propPrefab = null; _posterSprite = null; break; //铁拳 case 6: _weaponTag = "IronFist"; _settlePrefab = isSelf ? settleIronFistSelf : settleIronFistOther; _awardFramePrefab = awardFrameIronFist; // ⭐ 美术规格:铁拳的全屏 UI 框用 PW_Effect_FullScreen_Symmetra(海报仍为 Prop_Poster_IronFist) _fullScreenPrefab = fullScreenSymmetra; _propPrefab = propIronFist; _posterSprite = posterIronFist; break; case 7: _weaponTag = "GammaStorm"; _settlePrefab = isSelf ? settleGammaStormSelf : settleGammaStormOther; _awardFramePrefab = awardFrameGammaStorm; _fullScreenPrefab = fullScreenGammaStorm; _propPrefab = propGammaStorm; _posterSprite = posterGammaStorm; break; //激光 case 8: _weaponTag = "Symmetra"; _settlePrefab = isSelf ? settleSymmetraSelf : settleSymmetraOther; _awardFramePrefab = awardFrameSymmetra; _fullScreenPrefab = fullScreenSymmetra; _propPrefab = propSymmetra; _posterSprite = posterSymmetra; break; default: Debug.LogWarning($"[XingJiXunHang] 未知 weaponID={_weaponId},走富甲天下"); goto case 0; } Debug.Log($"[XingJiXunHang] 选择武器: tag={_weaponTag} isSelf={isSelf}" + $" settle={(_settlePrefab != null ? _settlePrefab.name : "null")}" + $" awardFrame={(_awardFramePrefab != null ? _awardFramePrefab.name : "null")}" + $" fullScreen={(_fullScreenPrefab != null ? _fullScreenPrefab.name : "null")}" + $" prop={(_propPrefab != null ? _propPrefab.name : "null")}"); } // ==================== 爆奖主流程 ==================== /// /// 道具武器完整流程: /// 1. 道具出场特效 (AppearProp) /// 2. 全屏特效 (FullScreen: tap_tip → knock_tip → 隐藏Tip,仅 isSelf) /// 3. 道具动画 (Prop: 各武器不同的子物体操作) /// 4. 奖励框 (AwardFrame) /// 5. CallBJ 发红包 /// 6. 满屏暴击文字 (TextCrit,仅 IronFist/GammaStorm) /// 7. 结算特效 (Settle) /// 8. EffEnd → 标准收尾 /// private IEnumerator PropWeaponFlow() { Debug.Log($"[XingJiXunHang] 开始爆奖流程 tag={_weaponTag} isSelf={isSelf} diePos={diePos}"); // -------- 形态一(富甲天下):直接结算,无道具流程 -------- if (_weaponId == 0) { yield return StartCoroutine(FuJiaTianXiaFlow()); yield break; } // -------- Phase 1: 道具出场特效 -------- if (appearPropEffect != null) { var appearObj = SpawnWorldEffect(appearPropEffect, diePos); Debug.Log("[XingJiXunHang] Phase1: 道具出场特效"); yield return new WaitForSeconds(appearDuration); if (appearObj != null) Destroy(appearObj); // 出场特效播完即删(不等 die) } else { yield return new WaitForSeconds(appearDuration); } // -------- Phase 2: 全屏特效(仅 isSelf) -------- GameObject fullScreenObj = null; if (isSelf && _fullScreenPrefab != null) { // ⭐ 全屏特效是 UI 预制体,锚点/anchoredPosition 已在预制体内写好, // 只挂到父节点即可,不要传 position/rotation(否则会覆盖写好的值) fullScreenObj = Instantiate(_fullScreenPrefab, CreateFishManager.Instance.bjEffectRoot); _spawnedEffects.Add(fullScreenObj); // 加入统一销毁列表 Debug.Log("[XingJiXunHang] Phase2: 全屏特效(自己)"); // 执行 tap_tip → knock_tip 序列(序列内部已把 Tip 隐藏) yield return StartCoroutine(PlayFullScreenTipSequence(fullScreenObj)); // ⭐ FullScreen 克隆对象本身不提前销毁(只隐藏 Tip),保留到爆奖结束, // 由 die() 统一销毁/回收(_spawnedEffects 列表管理) Debug.Log("[XingJiXunHang] FullScreen UI 框保留到爆奖结束(Tip 已隐藏)"); } else { yield return new WaitForSeconds(0.1f); } // -------- Phase 3: 道具动画 -------- if (_propPrefab != null) { yield return StartCoroutine(PlayPropAnimation()); } // -------- Phase 4: 奖励框(⭐ 不再额外 Spawn:奖励框即加分面板,已在杀鱼加分时由 // EnsureBossScoreTmp 以 _awardFramePrefab 创建并显示分数) -------- Debug.Log("[XingJiXunHang] Phase4: 奖励框即加分面板(EnsureBossScoreTmp 已按武器创建)"); // -------- Phase 5: CallBJ 发红包 -------- for (int i = 0; i < bjCount; i++) { Debug.Log($"[XingJiXunHang] Phase5: CallBJ {i + 1}/{bjCount}"); CallBJ(bjMinCount, bjMaxCount); yield return new WaitForSeconds(0.5f); } // -------- Phase 6: 满屏暴击文字(仅 IronFist/GammaStorm) -------- if (_weaponId == 6 || _weaponId == 7) { PlayTextCrit(); yield return new WaitForSeconds(0.5f); } // -------- Phase 6.5: 伽马风暴暴击框 -------- if (_weaponId == 7 && critFrameGammaStorm != null) { PlayGammaStormCritFrame(); yield return new WaitForSeconds(0.5f); } // -------- Phase 7: 等待金币协程完成 -------- if (_activeCoinRoutines > 0) { yield return new WaitUntil(() => _activeCoinRoutines <= 0); } // -------- Phase 8: 结算特效(⭐ UI,isSelf 放 UI 中心,不再放世界 0,0) -------- if (_settlePrefab != null) { if (isSelf) { // ⭐ 结算特效是 UI 预制体 → 挂 UI 层放屏幕中心,加入统一销毁列表(die() 时删除) var settleObj = SpawnUiEffect(_settlePrefab); // ⭐ 中央结算面板上挂着 BJEndUI(美术已接好 tMP_beishu / tMP_score)→ 写入倍数 + 最终总分 InitSettleBjEndUI(settleObj); Debug.Log("[XingJiXunHang] Phase8: 结算特效(自己,UI中心)"); } else { // 他人视角:显示在击杀者座位上方(世界坐标) Vector3 settlePos = GetSeatPosition() + Vector3.up * 2f; var settleObj = SpawnWorldEffect(_settlePrefab, settlePos); if (settleObj != null) settleObj.transform.localScale = Vector3.one * 0.6f; // ⭐ 他人视角结算面板同样写分数(银币精灵,isSelf=false) InitSettleBjEndUI(settleObj); Debug.Log($"[XingJiXunHang] Phase8: 结算特效(他人) pos={settlePos}"); } } else { Debug.LogWarning($"[XingJiXunHang] 结算预制体为空 tag={_weaponTag}"); } yield return new WaitForSeconds(settleDuration); // -------- Phase 10: 触发 EffEnd → 标准收尾 -------- if (!isEffEnd && !isScoreEnd) { Debug.Log("[XingJiXunHang] Phase10: 触发 EffEnd"); isGoldEffectStart = false; EffEnd(); } } // ==================== 形态一:富甲天下流程 ==================== /// /// 富甲天下流程:无道具武器,直接 CallBJ → 结算。 /// Settle: PW_Effect_Settle_FuJiaTianXia (Node子物体指认Source Image为tj_fcboss_0) /// private IEnumerator FuJiaTianXiaFlow() { Debug.Log("[XingJiXunHang] 富甲天下流程开始"); // CallBJ 发红包 for (int i = 0; i < bjCount; i++) { CallBJ(bjMinCount, bjMaxCount); yield return new WaitForSeconds(0.5f); } // 等待金币协程完成 if (_activeCoinRoutines > 0) { yield return new WaitUntil(() => _activeCoinRoutines <= 0); } // 结算特效(⭐ UI,isSelf 放 UI 中心) if (settleFuJiaTianXia != null) { // 音效:富甲天下结算——金币喷出 + 语音(PW_Settle_FuJiaTianXia1 / PW_Settle_FuJiaTianXia2) AudioManager.Instance.PlayerFXByName("PW_Settle_FuJiaTianXia1"); AudioManager.Instance.PlayerFXByName("PW_Settle_FuJiaTianXia2"); if (isSelf) { var settleObj = SpawnUiEffect(settleFuJiaTianXia); // ⭐ 中央结算面板 BJEndUI 写入倍数 + 最终总分 InitSettleBjEndUI(settleObj); Debug.Log("[XingJiXunHang] 富甲天下结算(自己,UI中心)"); } else { Vector3 settlePos = GetSeatPosition() + Vector3.up * 2f; var settleObj = SpawnWorldEffect(settleFuJiaTianXia, settlePos); if (settleObj != null) settleObj.transform.localScale = Vector3.one * 0.6f; // ⭐ 他人视角结算面板同样写分数 InitSettleBjEndUI(settleObj); Debug.Log($"[XingJiXunHang] 富甲天下结算(他人) pos={settlePos}"); } } yield return new WaitForSeconds(settleDuration); // 触发 EffEnd if (!isEffEnd && !isScoreEnd) { isGoldEffectStart = false; EffEnd(); } } // ==================== FullScreen tap_tip/knock_tip 序列 ==================== /// /// 全屏特效的 tap_tip → knock_tip 序列(美术规格要求)。 /// 1. 激活 Tip 容器(tap_tip/knock_tip 的父级,保证子物体能显示) /// 2. 激活 tap_tip /// 3. 等待 tapTipDuration /// 4. 激活 knock_tip(设置海报Sprite) /// 5. 等待 knockTipDuration /// 6. 隐藏 Tip 容器(连带 tap_tip/knock_tip 一起隐藏,所有形态通用) /// private IEnumerator PlayFullScreenTipSequence(GameObject fullScreenObj) { if (fullScreenObj == null) yield break; // 查找子物体 Transform tapTip = FindChildRecursive(fullScreenObj.transform, tapTipName); Transform knockTip = FindChildRecursive(fullScreenObj.transform, knockTipName); Transform tipContainer = FindChildRecursive(fullScreenObj.transform, tipContainerName); // ⭐ 序列开始前确保 Tip 容器激活(否则里面的 tap_tip/knock_tip 激活了也看不到) if (tipContainer != null) tipContainer.gameObject.SetActive(true); // 初始隐藏 if (tapTip != null) tapTip.gameObject.SetActive(false); if (knockTip != null) knockTip.gameObject.SetActive(false); // 1. 激活 tap_tip if (tapTip != null) { tapTip.gameObject.SetActive(true); Debug.Log($"[XingJiXunHang] FullScreen: 激活 {tapTipName}"); } yield return new WaitForSeconds(tapTipDuration); // 2. 激活 knock_tip(设置海报Sprite) if (knockTip != null) { knockTip.gameObject.SetActive(true); // 设置海报Sprite到 knock_tip 的 Image 组件 if (_posterSprite != null) { var img = knockTip.GetComponent(); if (img == null) img = knockTip.GetComponentInChildren(); if (img != null) { img.sprite = _posterSprite; Debug.Log($"[XingJiXunHang] FullScreen: 设置 {knockTipName} 海报={_posterSprite.name}"); } } Debug.Log($"[XingJiXunHang] FullScreen: 激活 {knockTipName}"); } yield return new WaitForSeconds(knockTipDuration); // 3. ⭐ 隐藏 Tip(隐藏 Tip 容器 + 显式隐藏 tap_tip/knock_tip,所有形态通用) if (tapTip != null) tapTip.gameObject.SetActive(false); if (knockTip != null) knockTip.gameObject.SetActive(false); if (tipContainer != null) { tipContainer.gameObject.SetActive(false); Debug.Log($"[XingJiXunHang] FullScreen: 隐藏 Tip 容器 {tipContainerName}"); } Debug.Log("[XingJiXunHang] FullScreen: 隐藏 Tip"); } // ==================== 道具动画(各武器不同) ==================== /// /// 道具动画,根据武器类型执行不同的子物体操作。 /// private IEnumerator PlayPropAnimation() { GameObject propObj = SpawnWorldEffect(_propPrefab, diePos); Debug.Log($"[XingJiXunHang] Phase3: 道具动画 tag={_weaponTag} pos={diePos}"); switch (_weaponId) { case 6: yield return StartCoroutine(IronFistPropAnim(propObj)); break; case 7: yield return StartCoroutine(GammaStormPropAnim(propObj)); break; case 8: yield return StartCoroutine(SymmetraPropAnim(propObj)); break; } // ⭐ 道具不再延迟销毁,统一由 die() 时清空 _spawnedEffects } /// /// 铁拳道具动画(美术规格): /// 1. 激活 Hole 子对象 /// 2. Hole 从死亡位置移动到屏幕左下角 /// 3. 隐藏 Hole /// 4. 激活 Hand /// private IEnumerator IronFistPropAnim(GameObject propObj) { Transform hole = FindChildRecursive(propObj.transform, ironFistHoleName); Transform hand = FindChildRecursive(propObj.transform, ironFistHandName); // 初始状态 if (hole != null) hole.gameObject.SetActive(false); if (hand != null) hand.gameObject.SetActive(false); // 1. 激活 Hole if (hole != null) { hole.gameObject.SetActive(true); hole.position = diePos; Debug.Log("[XingJiXunHang] 铁拳: 激活Hole"); // 音效:道具铁拳展示(PW_Prop_IronFist_Show) AudioManager.Instance.PlayerFXByName("PW_Prop_IronFist_Show"); } yield return new WaitForSeconds(0.2f); // 2. Hole 移动到屏幕左下角(加偏移,往屏幕内挪一点) Vector3 bottomLeft = GetScreenBottomLeft() + ironFistCornerOffset; if (hole != null) { yield return StartCoroutine(MoveTo(hole, bottomLeft, propAnimDuration)); Debug.Log($"[XingJiXunHang] 铁拳: Hole移动到左下角+偏移 {bottomLeft}"); // ⭐ Hole 到位后停留 0.5 秒,让玩家看清再释放(之前太快直接释放 Hand) yield return new WaitForSeconds(ironFistHoldDelay); Debug.Log($"[XingJiXunHang] 铁拳: Hole 到位停留 {ironFistHoldDelay}s"); } // 3. 隐藏 Hole if (hole != null) hole.gameObject.SetActive(false); // 4. 激活 Hand(击打) Vector3 handPos = bottomLeft; if (hand != null) { hand.gameObject.SetActive(true); hand.position = handPos; Debug.Log("[XingJiXunHang] 铁拳: 激活Hand"); // 音效:道具铁拳击打节拍(PW_Prop_IronFist_Knock_Beat) AudioManager.Instance.PlayerFXByName("PW_Prop_IronFist_Knock_Beat"); } // ⭐ Hand 激活后延迟 2 秒,然后直接把 Hand 隐藏 yield return new WaitForSeconds(ironFistAwardDelay); Debug.Log($"[XingJiXunHang] 铁拳: Hand 激活后延迟 {ironFistAwardDelay}s,隐藏 Hand"); if (hand != null) hand.gameObject.SetActive(false); // ⭐ 铁拳击打:范围内普通鱼从近到远逐个死亡(总时长约1秒),金币从左下角飞向炮台 if (ironFistKillFish) { yield return StartCoroutine(KillFishByIronFistRoutine(handPos)); } yield return new WaitForSeconds(0.3f); } /// /// 铁拳击打杀鱼:检测 Hand 半径 ironFistKillRadius 范围内的普通鱼, /// 计算鱼与预制体(Hand)的距离,从近到远逐个死亡(总时长约 ironFistKillTotalTime 秒), /// 金币从鱼的位置(左下角)飞向炮台座位(从下往上延伸)。 /// private IEnumerator KillFishByIronFistRoutine(Vector3 center) { if (ironFistKillLayer.value == 0) { Debug.LogWarning("[XingJiXunHang] 铁拳杀鱼层 ironFistKillLayer 未设置(Inspector 选 Fish 层),跳过杀鱼"); ironFistKillFish = false; // 只提示一次 yield break; } Collider2D[] hits = Physics2D.OverlapCircleAll(center, Mathf.Max(0.1f, ironFistKillRadius), ironFistKillLayer); if (hits == null || hits.Length == 0) yield break; // 收集范围内未炸过的普通鱼 List fishes = new List(); foreach (var hit in hits) { if (hit == null || !hit.CompareTag("Fish")) continue; var monster = hit.GetComponentInParent(); if (monster == null) continue; if (monster.fishenum != FishTypeEnum.普通鱼) continue; // 只炸普通鱼 if (_netKilledFish.Contains(monster.guid)) continue; // 已炸过跳过 fishes.Add(monster); } if (fishes.Count == 0) yield break; // ⭐ 按距离 Hand(预制体)从近到远排序 fishes.Sort((a, b) => Vector2.Distance(a.transform.position, center) .CompareTo(Vector2.Distance(b.transform.position, center))); // ⭐ 逐个杀:每只间隔 = 总时长/数量,整体约 1 秒 float interval = ironFistKillTotalTime / fishes.Count; Vector3 turretPos = GetSeatPosition(); // 金币飞向炮台 int killed = 0; foreach (var monster in fishes) { // ⭐ 分数加满(=maxScore)后停止杀鱼 if (BossScoreFull) { Debug.Log($"[XingJiXunHang] 铁拳: 面板分已达上限 {maxScore},停止杀鱼"); break; } if (monster == null) continue; if (!_netKilledFish.Add(monster.guid)) continue; var coinNum = new RedPacketRecord { Amount = paoBei * monster.monsterData.fishTypeConfig.fishScore }; // ⭐ 飞机死亡掉金币(飞向炮台),分数同时加到 Boss 面板(封顶 maxScore) CreateFishManager.Instance.Id_DieFish_FX(monster.guid, coinNum, turretPos, isLocal: isSelf); AddBossPanelScore(coinNum.Amount); killed++; Debug.Log($"[XingJiXunHang] 铁拳: 从近到远杀鱼[{killed}/{fishes.Count}] 距离={Vector2.Distance(monster.transform.position, center):0.00}"); yield return new WaitForSeconds(interval); } Debug.Log($"[XingJiXunHang] 铁拳: 击打杀鱼完成 {killed} 条(总时长约{ironFistKillTotalTime}s)"); } /// /// 光子道具动画(美术规格): /// 1. Prop 从死亡位置移动到屏幕中心 /// 2. 隐藏 Lock /// 3. PropL 和 PropR 分别移动到屏幕两边 /// 4. 激活 laser 子物体 /// private IEnumerator SymmetraPropAnim(GameObject propObj) { Transform lockObj = FindChildRecursive(propObj.transform, symmetraLockName); Transform propL = FindChildRecursive(propObj.transform, symmetraPropLName); Transform propR = FindChildRecursive(propObj.transform, symmetraPropRName); Transform laser = FindChildRecursive(propObj.transform, symmetraLaserName); // 初始状态 if (lockObj != null) lockObj.gameObject.SetActive(true); if (propL != null) propL.gameObject.SetActive(false); if (propR != null) propR.gameObject.SetActive(false); if (laser != null) laser.gameObject.SetActive(false); // 1. Prop 移动到屏幕中心 propObj.transform.position = diePos; yield return StartCoroutine(MoveTo(propObj.transform, Vector3.zero, propAnimDuration)); Debug.Log("[XingJiXunHang] 光子: 移动到屏幕中心"); // 2. 隐藏 Lock if (lockObj != null) { lockObj.gameObject.SetActive(false); Debug.Log("[XingJiXunHang] 光子: 隐藏Lock"); } // 3. PropL/PropR 移动到屏幕两边 Vector3 leftPos = GetScreenLeft(); Vector3 rightPos = GetScreenRight(); if (propL != null && propR != null) { propL.gameObject.SetActive(true); propR.gameObject.SetActive(true); propL.position = Vector3.zero; propR.position = Vector3.zero; // 音效:道具对称体到达边缘(PW_Prop_Symmetra_To_Edge) AudioManager.Instance.PlayerFXByName("PW_Prop_Symmetra_To_Edge"); StartCoroutine(MoveTo(propL, leftPos, propAnimDuration * 0.5f)); yield return StartCoroutine(MoveTo(propR, rightPos, propAnimDuration * 0.5f)); Debug.Log("[XingJiXunHang] 光子: PropL/PropR移动到两边"); } // 4. 激活 laser if (laser != null) { laser.gameObject.SetActive(true); // 音效:道具对称体接触(PW_Prop_Symmetra_Contact) AudioManager.Instance.PlayerFXByName("PW_Prop_Symmetra_Contact"); // ⭐ laser 里的 LineRenderer:端点用 左右(PropL/PropR)里面各自的 laser 子对象位置 var lr = laser.GetComponent(); if (lr != null && propL != null && propR != null) { // 找左右里面的 laser 发射点对象(找不到则回退用 PropL/PropR 自身) Transform laserL = FindChildRecursive(propL, symmetraLaserName); Transform laserR = FindChildRecursive(propR, symmetraLaserName); Vector3 endL = laserL != null ? laserL.position : propL.position; Vector3 endR = laserR != null ? laserR.position : propR.position; lr.positionCount = 2; if (lr.useWorldSpace) { lr.SetPosition(0, endL); lr.SetPosition(1, endR); } else { // 本地坐标空间:转成 laser 的本地坐标 lr.SetPosition(0, laser.InverseTransformPoint(endL)); lr.SetPosition(1, laser.InverseTransformPoint(endR)); } Debug.Log($"[XingJiXunHang] 光子: laser LineRenderer 端点 L={endL} R={endR} useWorldSpace={lr.useWorldSpace}"); } else { Debug.LogWarning($"[XingJiXunHang] 光子: laser 无 LineRenderer 或 PropL/PropR 为空(lr={lr != null} propL={propL != null} propR={propR != null})"); } // ⭐ laser 设置好并激活后,上下摆动(让激光看起来在动) if (laserSwingCycles > 0) { yield return StartCoroutine(LaserSwing(laser, laserSwingAmplitude, laserSwingCycles)); } } // ⭐ 4条激光沿各自方向伸展到最长位置(laser1:+X → laser2:-Y → laser3:+Y → laser4:-X) yield return StartCoroutine(LaserExpand(propObj)); // ⭐ 电网:Prop 整体上下扫动 + 碰到飞机(鱼)爆炸 if (netSweepCycles > 0) { // 音效:道具对称体攻击(PW_Prop_Symmetra_Attack,电网扫动) AudioManager.Instance.PlayerFXByName("PW_Prop_Symmetra_Attack"); yield return StartCoroutine(ElectricNetSweep(propObj, propL, propR)); } // ⭐ 激光结束:等 1 秒后直接隐藏 Prop(不再一直显示到爆奖结束才回收) yield return new WaitForSeconds(symmetraPropHideDelay); if (propObj != null) { propObj.SetActive(false); Debug.Log($"[XingJiXunHang] 光子: 激光结束等待 {symmetraPropHideDelay}s 后隐藏 Prop"); } } /// /// 4条激光伸展:laser1 向 +X(屏幕最右)、laser2 向 -Y(屏幕最下)、 /// laser3 向 +Y(屏幕最上)、laser4 向 -X(屏幕最左),到达最长位置。 /// private IEnumerator LaserExpand(GameObject propObj) { if (propObj == null) yield break; Transform l1 = FindChildRecursive(propObj.transform, laser1Name); Transform l2 = FindChildRecursive(propObj.transform, laser2Name); Transform l3 = FindChildRecursive(propObj.transform, laser3Name); Transform l4 = FindChildRecursive(propObj.transform, laser4Name); int count = 0; // laser1 → +X 正方向(保持当前 Y,X 到屏幕最右) if (l1 != null) { Vector3 dest = new Vector3(GetScreenRight().x, l1.position.y, 0); StartCoroutine(MoveTo(l1, dest, laserExpandSpeed)); count++; } // laser2 → -Y 方向(保持当前 X,Y 到屏幕最下) if (l2 != null) { Vector3 dest = new Vector3(l2.position.x, GetScreenBottom().y, 0); StartCoroutine(MoveTo(l2, dest, laserExpandSpeed)); count++; } // laser3 → +Y 方向(保持当前 X,Y 到屏幕最上) if (l3 != null) { Vector3 dest = new Vector3(l3.position.x, GetScreenTop().y, 0); StartCoroutine(MoveTo(l3, dest, laserExpandSpeed)); count++; } // laser4 → -X 方向(保持当前 Y,X 到屏幕最左) if (l4 != null) { Vector3 dest = new Vector3(GetScreenLeft().x, l4.position.y, 0); StartCoroutine(MoveTo(l4, dest, laserExpandSpeed)); count++; } yield return new WaitForSeconds(laserExpandSpeed + 0.2f); Debug.Log($"[XingJiXunHang] 光子: 4条激光伸展完成 找到{count}条({laser1Name}/{laser2Name}/{laser3Name}/{laser4Name})"); } /// /// 电网扫动:Prop 整体上下移动(幅度 netSweepAmplitude,最多 netSweepCycles 次), /// 扫动过程中每帧检测 PropL~PropR 之间的矩形区域,碰到普通鱼 → 爆炸(Id_DieFish_FX)。 /// ⭐ 不需要一直按设定的圈数移动:面板分加满(=maxScore)后提前停止扫动。 /// private IEnumerator ElectricNetSweep(GameObject propObj, Transform propL, Transform propR) { if (propObj == null) yield break; Vector3 basePos = propObj.transform.position; for (int i = 0; i < netSweepCycles; i++) { // ⭐ 面板分加满:提前停止扫动(不需要一直按圈数移动) if (BossScoreFull) { Debug.Log($"[XingJiXunHang] 光子: 面板分已达上限 {maxScore},提前停止电网扫动"); break; } // 上扫 yield return StartCoroutine(SweepSegment(propObj, propL, propR, basePos + Vector3.up * netSweepAmplitude)); if (BossScoreFull) break; // 下扫 yield return StartCoroutine(SweepSegment(propObj, propL, propR, basePos - Vector3.up * netSweepAmplitude)); } // 回原位 yield return StartCoroutine(SweepSegment(propObj, propL, propR, basePos)); Debug.Log($"[XingJiXunHang] 光子: 电网扫动结束(计划{netSweepCycles}圈)炸鱼数={_netKilledFish.Count}"); } /// 电网从当前位置移动到 dest(逐帧插值 + 途中杀鱼) private IEnumerator SweepSegment(GameObject propObj, Transform propL, Transform propR, Vector3 dest) { if (propObj == null) yield break; Vector3 start = propObj.transform.position; float elapsed = 0f; while (elapsed < netSweepSpeed) { elapsed += Time.deltaTime; float t = netSweepSpeed > 0 ? elapsed / netSweepSpeed : 1f; if (t > 1f) t = 1f; propObj.transform.position = Vector3.Lerp(start, dest, t); if (netKillFish) KillFishInNet(propL, propR); // ⭐ 分数加满:本段提前结束 if (BossScoreFull) break; yield return null; } propObj.transform.position = dest; if (netKillFish && !BossScoreFull) KillFishInNet(propL, propR); } /// 检测 PropL~PropR 之间的矩形电网区域,普通鱼直接爆炸 private void KillFishInNet(Transform propL, Transform propR) { if (propL == null || propR == null) return; if (netKillLayer.value == 0) { Debug.LogWarning("[XingJiXunHang] 电网杀鱼层 netKillLayer 未设置(Inspector 选 Fish 层),跳过杀鱼"); netKillFish = false; // 只提示一次 return; } // 电网矩形区域:左右端 ± 半径 float radius = Mathf.Max(0.1f, netKillRadius); Vector2 min = new Vector2( Mathf.Min(propL.position.x, propR.position.x) - radius, Mathf.Min(propL.position.y, propR.position.y) - radius); Vector2 max = new Vector2( Mathf.Max(propL.position.x, propR.position.x) + radius, Mathf.Max(propL.position.y, propR.position.y) + radius); Collider2D[] hits = Physics2D.OverlapAreaAll(min, max, netKillLayer); if (hits == null || hits.Length == 0) return; Vector3 turretPos = GetSeatPosition(); // 金币飞向炮台 int killed = 0; foreach (var hit in hits) { // ⭐ 分数加满(=maxScore)后停止杀鱼 if (BossScoreFull) { Debug.Log($"[XingJiXunHang] 电网: 面板分已达上限 {maxScore},停止杀鱼"); break; } if (hit == null || !hit.CompareTag("Fish")) continue; var monster = hit.GetComponentInParent(); if (monster == null) continue; if (monster.fishenum != FishTypeEnum.普通鱼) continue; // 只炸普通鱼 if (!_netKilledFish.Add(monster.guid)) continue; // 已炸过跳过 var coinNum = new RedPacketRecord { Amount = paoBei * monster.monsterData.fishTypeConfig.fishScore }; // ⭐ 飞机死亡掉金币(飞向炮台),分数同时加到 Boss 面板(封顶 maxScore) CreateFishManager.Instance.Id_DieFish_FX(monster.guid, coinNum, turretPos, isLocal: isSelf); AddBossPanelScore(coinNum.Amount); killed++; } if (killed > 0) Debug.Log($"[XingJiXunHang] 电网炸鱼 {killed} 条"); } /// laser 上下摆动动画(幅度 amplitude,来回 cycles 次,结束后回到原位) private IEnumerator LaserSwing(Transform target, float amplitude, int cycles) { if (target == null) yield break; Vector3 basePos = target.position; for (int i = 0; i < cycles; i++) { yield return StartCoroutine(MoveTo(target, basePos + Vector3.up * amplitude, 0.3f)); yield return StartCoroutine(MoveTo(target, basePos - Vector3.up * amplitude, 0.3f)); } // 回到原位 yield return StartCoroutine(MoveTo(target, basePos, 0.2f)); Debug.Log($"[XingJiXunHang] 光子: laser 上下摆动完成 cycles={cycles}"); } /// /// 伽马风暴道具动画(美术规格): /// 1. Prop 从死亡位置移动到屏幕中心 /// 2. 显示四根射线(激光) /// 3. 等待 gammaRotateDelay(默认1秒) /// 4. 开始旋转(Prop 整体绕 Z 轴),旋转中碰到激光的普通鱼直接死亡 /// private IEnumerator GammaStormPropAnim(GameObject propObj) { // 1. 移动到屏幕中心 propObj.transform.position = diePos; yield return StartCoroutine(MoveTo(propObj.transform, Vector3.zero, propAnimDuration)); Debug.Log("[XingJiXunHang] 伽马风暴: 移动到屏幕中心"); // 2. 显示四根射线(激光,默认隐藏则激活) Transform[] lasers = FindGammaLasers(propObj); int shown = 0; foreach (var l in lasers) { if (l != null) { l.gameObject.SetActive(true); shown++; } } Debug.Log($"[XingJiXunHang] 伽马风暴: 显示激光 {shown}/{lasers.Length} 根"); // 音效:道具伽马风暴准备(PW_Prop_GammaStorm_Ready) AudioManager.Instance.PlayerFXByName("PW_Prop_GammaStorm_Ready"); // ⭐ 射线长度 = 斜边(直角边 = 屏幕宽度一半 / 高度一半),延伸到屏幕角落 float diag = GetScreenHalfDiagonal(); UpdateGammaLaserEndpoints(lasers, propObj.transform.position, diag); Debug.Log($"[XingJiXunHang] 伽马风暴: 射线长度设为斜边 {diag}"); // 3. 等待 1 秒(Tip 隐藏后停顿,让玩家看清激光) yield return new WaitForSeconds(gammaRotateDelay); Debug.Log($"[XingJiXunHang] 伽马风暴: 激光显示等待 {gammaRotateDelay}s 后开始旋转"); // 音效:道具伽马风暴攻击(PW_Prop_GammaStorm_Attack,开始旋转) AudioManager.Instance.PlayerFXByName("PW_Prop_GammaStorm_Attack"); // 4. 旋转 + 射线杀鱼 yield return StartCoroutine(GammaRotateAndKill(propObj, lasers, diag)); } /// 按 gammaLaserNames 查找四根射线子物体 private Transform[] FindGammaLasers(GameObject propObj) { if (propObj == null || gammaLaserNames == null || gammaLaserNames.Length == 0) return new Transform[0]; Transform[] lasers = new Transform[gammaLaserNames.Length]; for (int i = 0; i < gammaLaserNames.Length; i++) { // ⭐ 先忽略大小写找(预制体实际小写 laser1~4),找不到再精确匹配 lasers[i] = FindChildIgnoreCase(propObj.transform, gammaLaserNames[i]); if (lasers[i] == null) lasers[i] = FindChildRecursive(propObj.transform, gammaLaserNames[i]); if (lasers[i] == null) Debug.LogWarning($"[XingJiXunHang] 伽马风暴: 找不到射线子物体 {gammaLaserNames[i]}"); } return lasers; } /// /// 伽马风暴:Prop 整体旋转 gammaRotateAngle 度(默认80°)。 /// 转到 gammaShrinkStartAngle(45°)后激光开始慢慢缩短,转到终点时缩到看不见(长度 0)。 /// 旋转中射线扫到的普通鱼死亡。 /// private IEnumerator GammaRotateAndKill(GameObject propObj, Transform[] lasers, float laserLength) { if (propObj == null) yield break; float rotated = 0f; int killCount = 0; float shrinkSpan = Mathf.Max(0.01f, gammaRotateAngle - gammaShrinkStartAngle); while (rotated < gammaRotateAngle) { float step = gammaRotateSpeed * Time.deltaTime; propObj.transform.Rotate(0f, 0f, step); rotated += step; // ⭐ 激光长度:前 gammaShrinkStartAngle 度保持全斜边,之后线性缩短到 0(看不见) float curLen = laserLength; if (rotated > gammaShrinkStartAngle) { float t = (rotated - gammaShrinkStartAngle) / shrinkSpan; curLen = laserLength * (1f - Mathf.Clamp01(t)); } // 每帧更新射线端点(LineRenderer 世界坐标不随父旋转,必须跟随激光新朝向) UpdateGammaLaserEndpoints(lasers, propObj.transform.position, curLen); if (gammaKillFish) KillFishByGammaLasers(propObj.transform, lasers, curLen, ref killCount); yield return null; } // 终点:激光缩到 0 长度(隐藏) UpdateGammaLaserEndpoints(lasers, propObj.transform.position, 0f); Debug.Log($"[XingJiXunHang] 伽马风暴: 旋转完成 {rotated:0.#}°(45°后激光缩短至消失)炸鱼 {killCount} 条"); // ⭐ 分不够就直接补齐到 maxScore(旋转杀鱼没杀满时,面板补满) if (!BossScoreFull) { long need = maxScore - _bossPanelScore; if (need > 0) { AddBossPanelScore(need); Debug.Log($"[XingJiXunHang] 伽马风暴: 分不够,直接补齐 {need} → {_bossPanelScore}"); } } } /// /// 设置四根射线长度为斜边:LineRenderer 端点 = 中心 → 中心 + 射线朝向×长度。 /// 射线朝向优先取"激光当前位置-中心"(随 Prop 旋转自动更新),零向量时用激光本地 Y 轴方向。 /// private void UpdateGammaLaserEndpoints(Transform[] lasers, Vector3 center, float length) { if (lasers == null) return; foreach (var laser in lasers) { if (laser == null) continue; // 射线方向:激光相对中心的位置(旋转后自动更新);为 0 则用激光自身的朝向 Vector2 dir = (Vector2)laser.position - (Vector2)center; if (dir.sqrMagnitude < 0.0001f) dir = laser.up; // 本地 Y 轴世界方向(或 right,按预制体实际朝向) if (dir.sqrMagnitude < 0.0001f) dir = Vector2.right; dir.Normalize(); Vector3 endWorld = center + (Vector3)dir * length; var lr = laser.GetComponent(); if (lr != null) { lr.positionCount = 2; if (lr.useWorldSpace) { lr.SetPosition(0, center); lr.SetPosition(1, endWorld); } else { lr.SetPosition(0, laser.InverseTransformPoint(center)); lr.SetPosition(1, laser.InverseTransformPoint(endWorld)); } } else { // 非 LineRenderer:把激光整体拉伸到目标长度(假定沿本地 Y 轴伸长) float curLen = laser.localScale.y; if (curLen < 0.001f) curLen = 1f; laser.localScale = new Vector3(laser.localScale.x, length / curLen, laser.localScale.z); } } } /// 屏幕半宽半高为直角边的斜边长度(世界单位)= 屏幕中心到角落的距离 private float GetScreenHalfDiagonal() { var cam = Camera.main; if (cam == null) return 10f; float z = cam.transform.position.z * -1f; Vector3 center = cam.ScreenToWorldPoint(new Vector3(Screen.width * 0.5f, Screen.height * 0.5f, z)); Vector3 corner = cam.ScreenToWorldPoint(new Vector3(Screen.width, Screen.height, z)); float halfW = Mathf.Abs(corner.x - center.x); float halfH = Mathf.Abs(corner.y - center.y); return Mathf.Sqrt(halfW * halfW + halfH * halfH); } /// 沿每根射线方向 Raycast 检测(长度=斜边),扫到的普通鱼直接爆炸(防重复) private void KillFishByGammaLasers(Transform center, Transform[] lasers, float laserLength, ref int killCount) { if (center == null || lasers == null || lasers.Length == 0) return; if (gammaKillLayer.value == 0) { Debug.LogWarning("[XingJiXunHang] 伽马风暴杀鱼层 gammaKillLayer 未设置(Inspector 选 Fish 层),跳过杀鱼"); gammaKillFish = false; // 只提示一次 return; } Vector3 turretPos = GetSeatPosition(); // 金币飞向炮台 foreach (var laser in lasers) { if (laser == null) continue; // 射线:从 Prop 中心朝激光朝向,距离 = 斜边长度(整根激光覆盖范围) Vector2 origin = center.position; Vector2 toEnd = (Vector2)laser.position - origin; Vector2 dir = toEnd.sqrMagnitude < 0.0001f ? (Vector2)laser.up : toEnd.normalized; if (dir.sqrMagnitude < 0.0001f) dir = Vector2.right; dir.Normalize(); RaycastHit2D[] hits = Physics2D.RaycastAll(origin, dir, laserLength, gammaKillLayer); if (hits == null || hits.Length == 0) continue; foreach (var hit in hits) { // ⭐ 分数加满(=maxScore)后停止杀鱼 if (BossScoreFull) { Debug.Log($"[XingJiXunHang] 伽马射线: 面板分已达上限 {maxScore},停止杀鱼"); return; } if (hit.collider == null || !hit.collider.CompareTag("Fish")) continue; var monster = hit.collider.GetComponentInParent(); if (monster == null) continue; if (monster.fishenum != FishTypeEnum.普通鱼) continue; // 只炸普通鱼 if (!_netKilledFish.Add(monster.guid)) continue; // 已炸过跳过 var coinNum = new RedPacketRecord { Amount = paoBei * monster.monsterData.fishTypeConfig.fishScore }; // ⭐ 飞机死亡掉金币(飞向炮台),分数同时加到 Boss 面板(封顶 maxScore) CreateFishManager.Instance.Id_DieFish_FX(monster.guid, coinNum, turretPos, isLocal: isSelf); AddBossPanelScore(coinNum.Amount); killCount++; } } } // ==================== 满屏暴击文字 ==================== /// /// 播放满屏暴击文字(IronFist/GammaStorm 用)。 /// 自己看 TextCrit_Self,他人看 TextCrit_Other。 /// /// ⭐ 文字内容:预制体里写死的是占位 '123',必须由代码写入「爆奖小鱼死亡累加的面板分」 /// (,与奖励框上滚动的分同一个值,封顶 maxScore),前缀 "+"。 /// ⭐ 两个预制体都自带 Canvas(RenderMode=WorldSpace,美术里是无效层ID + order 5), /// 必须统一挂到游戏 UI 层、用 anchoredPosition 定位: /// 自己 → 屏幕中心;他人 → 击杀者座位(座位世界坐标 → 画布坐标)。 /// (之前"他人"走 SpawnWorldEffect 挂到 bjEffectRoot(场景根、不在 Canvas 下), /// 它的 WorldSpace 画布会按世界单位渲染 100×100 → 尺寸/位置全错。) /// ⭐ 层级:预制体自带的是无效层ID + order 5,会被 FullScreen 框(Default, order 12) /// 压在下面 → 这里显式改成 Default + 更高 order。 /// ⭐ 动画:对齐 lua CritTextEffect —— Text/Crit 从 0 弹出、停留后整体缩小消失并销毁。 /// private void PlayTextCrit() { GameObject critPrefab = isSelf ? textCritSelf : textCritOther; if (critPrefab == null) { Debug.LogWarning("[XingJiXunHang] 暴击文字预制体为空"); return; } // ⭐ 自己/他人统一挂 UI 层(同一个渲染空间),位置:自己=屏幕中心,他人=击杀者座位 GameObject critObj = SpawnUiEffect(critPrefab); if (critObj == null) { Debug.LogWarning("[XingJiXunHang] 暴击文字生成失败,跳过分数写入"); return; } if (isSelf) { Debug.Log("[XingJiXunHang] 播放满屏暴击(自己,UI中心)"); } else { // ⭐ 他人:座位世界坐标 → 画布 anchoredPosition(与其它 UI 飘字同一套换算) var critRt = critObj.GetComponent(); EnsureUiLayer(); if (critRt != null && _uiCanvas != null) { Vector3 seatPos = GetSeatPosition() + Vector3.up * 2f; critRt.anchoredPosition = CoordinateTransformation.WorldPos_ToCanvas(seatPos, _uiCanvas); Debug.Log($"[XingJiXunHang] 播放满屏暴击(他人,UI层) seat={seatPos} anchored={critRt.anchoredPosition}"); } else { Debug.LogWarning("[XingJiXunHang] 播放满屏暴击(他人):Canvas 缺失,退回 UI 中心"); } } // ⭐ 取分数 TMP:优先按名字找数字节点(lua 也是按名字取 Text),回退第一个 TMP Transform textNode = string.IsNullOrEmpty(critTextNodeName) ? null : FindChildRecursive(critObj.transform, critTextNodeName); var critTmp = textNode != null ? textNode.GetComponent() : critObj.GetComponentInChildren(true); if (critTmp != null) { // lua: "+" .. num(该 TMP 的 spriteAsset 已配好「数字→图片」映射) critTmp.text = "+" + _bossPanelScore; Debug.Log($"[XingJiXunHang] 暴击文字分数 = +{_bossPanelScore}(上限{maxScore})"); if (_bossPanelScore <= 0) Debug.LogWarning("[XingJiXunHang] 暴击文字分数为 0:本次没有小鱼死亡累加(面板分仍为 0)"); } else { Debug.LogWarning($"[XingJiXunHang] 暴击文字 {critObj.name} 上没找到 TMP,分数未写入"); } // ⭐ 修层级:预制体自带 Canvas → Default(0) + 高于 FullScreen(order 12), // 否则暴击文字会被全屏特效框挡住(美术里的 -559336389 在本项目 TagManager 里不存在) var critCanvas = critObj.GetComponent(); if (critCanvas == null) critCanvas = critObj.GetComponentInChildren(true); if (critCanvas != null) { critCanvas.overrideSorting = true; critCanvas.sortingLayerID = 0; // Default(本项目只有 Default 一个排序层) critCanvas.sortingOrder = 15; // 高于 FullScreen 的 12 Debug.Log($"[XingJiXunHang] 暴击文字 Canvas 层级修正: Default / order={critCanvas.sortingOrder}"); } else { Debug.LogWarning($"[XingJiXunHang] 暴击文字 {critObj.name} 上没找到 Canvas,层级未修正"); } // ⭐ 弹出动画 + 定时消失(原版有,之前完全没做 → 静止挂在屏幕中央直到爆奖结束) PlayCritAnim(critObj, textNode); } /// /// 暴击文字弹出动画(对齐 lua CritTextEffect): /// Text / Crit 从 scale 0 弹回原始 scale(OutElastic,Crit 延时 0.15s) /// → 停留 critHoldDuration 秒 → 整体缩小到 0 并销毁。 /// ⭐ 弹回目标用"原始 scale":预制体里 Text 带美术缩放(如 1.727),直接弹到 1 会变小。 /// private void PlayCritAnim(GameObject critObj, Transform textNode) { if (critObj == null) return; if (textNode != null) { Vector3 target = textNode.localScale; textNode.localScale = Vector3.zero; textNode.DOScale(target, 0.3f).SetEase(Ease.OutElastic); } Transform critNode = string.IsNullOrEmpty(critNodeName) ? null : FindChildRecursive(critObj.transform, critNodeName); if (critNode != null) { Vector3 target = critNode.localScale; critNode.localScale = Vector3.zero; critNode.DOScale(target, 0.2f).SetEase(Ease.OutElastic).SetDelay(0.15f); } critObj.transform.DOScale(Vector3.zero, 0.2f) .SetEase(Ease.InBack) .SetDelay(Mathf.Max(0f, critHoldDuration)) .OnComplete(() => { if (critObj != null) { // 已自行销毁 → 从统一销毁列表移除,避免 die() 时重复 Destroy _spawnedEffects.Remove(critObj); Destroy(critObj); } }); } // ==================== 伽马风暴暴击框 ==================== /// /// 播放伽马风暴暴击框(PW_Effect_Crit_GammaStorm)。 /// 仅 weaponID=7 (GammaStorm) 有效。 /// public void PlayGammaStormCritFrame() { if (_weaponTag != "GammaStorm" || critFrameGammaStorm == null) return; // ⭐ 伽马暴击框是 UI → 放 UI 中心,加入统一销毁列表 SpawnUiEffect(critFrameGammaStorm); Debug.Log("[XingJiXunHang] 播放伽马风暴暴击框(UI中心)"); } // ==================== 辅助方法 ==================== /// 每死亡一个飞机,就往 Boss 分数面板上加一个分(面板独立于 Boss 本体,删除后仍有效) private void AddBossPanelScore(long amount) { if (amount <= 0) return; // ⭐ 面板加分不能超过金币上限 maxScore(封顶) long newScore = _bossPanelScore + amount; if (newScore > maxScore) newScore = maxScore; if (newScore == _bossPanelScore) return; // 已封顶,无变化 _bossPanelScore = newScore; EnsureBossScoreTmp(); if (_bossScoreTmp != null) { _bossScoreTmp.text = _bossPanelScore.ToString(); Debug.Log($"[XingJiXunHang] Boss分数面板 +{amount} = {_bossPanelScore}(上限{maxScore})"); } } /// 面板分是否已加满(= maxScore 后停止杀鱼) private bool BossScoreFull => _bossPanelScore >= maxScore; /// 确保 Boss 分数面板 TMP 可用(优先用 bossCtrl 已创建的,否则手动创建) private void EnsureBossScoreTmp() { if (_bossScoreTmp != null) return; // 1. 从 bossCtrl 拿(Boss 删除前已缓存的面板引用) if (bossCtrl != null && bossCtrl.tMP_CurrBossScore != null) { _bossScoreTmp = bossCtrl.tMP_CurrBossScore; return; } // 2. ⭐ 用当前武器对应的奖励框作为加分面板(详细指定出的分数预制体), // 不再使用 Manager 默认的 BJEventBase 分数预制体(dieFXPrefabAwardTmp) var prefab = _awardFramePrefab; var parent = CreateFishManager.Instance != null && CreateFishManager.Instance.currGameType != null ? CreateFishManager.Instance.currGameType.subPanelParent : null; if (prefab == null || parent == null) return; var board = Instantiate(prefab, parent); // ⭐ 加分面板位置放在座位上方才是正常的 board.transform.position = GetSeatPosition() + Vector3.up * 150f; // 从 BJImage 拿加分 TMP(指向奖励框自身的 Num),并按 isSelf 应用金/银 sprite 样式 var bjImage = board.GetComponentInChildren(); if (bjImage != null && bjImage.tmp_CoinNum != null) { _bossScoreTmp = bjImage.tmp_CoinNum; _bossScoreTmp.text = "0"; var spriteAsset = isSelf ? bjImage.goldSprite : bjImage.silverSprite; if (spriteAsset != null) _bossScoreTmp.spriteAsset = spriteAsset; } else { _bossScoreTmp = board.GetComponentInChildren(); } } /// /// ⭐ 给结算特效上的 BJEndUI 写入爆奖分数(倍数 + 最终总分)。 /// /// 结算预制体(PW_Effect_Settle_*_XingJiXunHang,Art/PW_Effect_Settle_* 的变体) /// 根节点上挂了 BJEndUI,tMP_beishu / tMP_score 已由美术接好,代码只需 Init: /// isSelf → 面板挂在 UI 层、屏幕中心(SpawnUiEffect);他人 → 击杀者座位上方(银币样式)。 /// 不写会一直显示预制体里的占位文本,所以爆奖结束前必须 Init 一次。 /// /// 已实例化的结算特效对象(SpawnUiEffect / SpawnWorldEffect 的返回值) private void InitSettleBjEndUI(GameObject settleObj) { if (settleObj == null) return; // 优先根节点(预制体挂在根上),回退子节点(防止后续挪到子物体) var endUI = settleObj.GetComponent(); if (endUI == null) endUI = settleObj.GetComponentInChildren(true); if (endUI == null) { Debug.LogWarning($"[XingJiXunHang] 结算特效 {settleObj.name} 上未找到 BJEndUI,跳过分数写入"); return; } // ⭐ 空安全:TMP 引用全空(预制体没接)时不调 Init,避免空引用 if (endUI.tMP_beishu == null && endUI.tMP_score == null) { Debug.LogWarning($"[XingJiXunHang] 结算面板 BJEndUI 未接 tMP_beishu/tMP_score({settleObj.name}),跳过分数写入"); return; } // ⭐ 分数 = maxScore(服务端下发的爆奖最终总分),与基类 ShowBJEndUI 保持一致; // 他人视角传 isSelf=false → 银币精灵 endUI.Init(bJbeishu, maxScore, isSelf); Debug.Log($"[XingJiXunHang] 结算面板 BJEndUI 写入分数: 倍数={bJbeishu} 分数={maxScore} " + $"isSelf={isSelf} obj={settleObj.name}"); } /// /// 缓存 UI 层父节点 + Canvas: /// 父节点 = currGameType.subPanelParent.parent(与爆奖预制体同层,随 UI 卸载自动清理) /// Canvas = UIManager.uiRoot 上的 Canvas /// private void EnsureUiLayer() { if (_uiLayer != null && _uiCanvas != null) return; if (CreateFishManager.Instance != null && CreateFishManager.Instance.currGameType != null) { var subPanel = CreateFishManager.Instance.currGameType.subPanelParent; if (subPanel != null) _uiLayer = subPanel.parent; } if (UIManager.Instance != null && UIManager.Instance.uiRoot != null) _uiCanvas = UIManager.Instance.uiRoot.GetComponent(); if (_uiCanvas == null && _uiLayer != null) _uiCanvas = _uiLayer.GetComponentInParent(); } /// /// ⭐ 在 UI 层实例化 UI 类特效(FullScreen/AwardFrame/Settle/Crit 等),放到 UI 中心。 /// 不再放世界坐标 (0,0)。自动加入 _spawnedEffects,die() 时统一销毁。 /// private GameObject SpawnUiEffect(GameObject prefab) { if (prefab == null) return null; EnsureUiLayer(); if (_uiLayer == null || _uiCanvas == null) { // 兜底:UI 层拿不到才放世界层(原逻辑) var fallback = Instantiate(prefab, Vector3.zero, Quaternion.identity, CreateFishManager.Instance.bjEffectRoot); _spawnedEffects.Add(fallback); return fallback; } var obj = Instantiate(prefab, _uiLayer); var rt = obj.GetComponent(); if (rt != null) { rt.anchoredPosition = CoordinateTransformation.ScreenPoint_ToCanvas( new Vector2(Screen.width / 2f, Screen.height / 2f), _uiCanvas); } _spawnedEffects.Add(obj); return obj; } /// 在世界层实例化 2D 类特效(道具出场/道具动画),保持世界坐标,加入统一销毁列表 private GameObject SpawnWorldEffect(GameObject prefab, Vector3 pos) { if (prefab == null) return null; var obj = Instantiate(prefab, pos, Quaternion.identity, CreateFishManager.Instance.bjEffectRoot); _spawnedEffects.Add(obj); return obj; } /// 递归查找子物体(⭐ 忽略大小写,防预制体命名 laser1/Laser1 不一致) private Transform FindChildRecursive(Transform parent, string name) { if (parent.name == name) return parent; for (int i = 0; i < parent.childCount; i++) { var result = FindChildRecursive(parent.GetChild(i), name); if (result != null) return result; } return null; } /// 递归查找子物体(忽略大小写版本,找不到再试精确) private Transform FindChildIgnoreCase(Transform parent, string name) { if (string.Equals(parent.name, name, System.StringComparison.OrdinalIgnoreCase)) return parent; for (int i = 0; i < parent.childCount; i++) { var result = FindChildIgnoreCase(parent.GetChild(i), name); if (result != null) return result; } return null; } /// 物体移动协程 private IEnumerator MoveTo(Transform target, Vector3 destination, float duration) { if (target == null) yield break; Vector3 start = target.position; float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; float t = elapsed / duration; target.position = Vector3.Lerp(start, destination, t); yield return null; } target.position = destination; } /// 获取屏幕左下角世界坐标 private Vector3 GetScreenBottomLeft() { var cam = Camera.main; if (cam != null) return cam.ScreenToWorldPoint(new Vector3(0, 0, cam.transform.position.z * -1f)); return new Vector3(-5, -3, 0); } /// 获取屏幕左边世界坐标 private Vector3 GetScreenLeft() { var cam = Camera.main; if (cam != null) return cam.ScreenToWorldPoint(new Vector3(0, Screen.height * 0.5f, cam.transform.position.z * -1f)); return new Vector3(-5, 0, 0); } /// 获取屏幕右边世界坐标 private Vector3 GetScreenRight() { var cam = Camera.main; if (cam != null) return cam.ScreenToWorldPoint(new Vector3(Screen.width, Screen.height * 0.5f, cam.transform.position.z * -1f)); return new Vector3(5, 0, 0); } /// 获取屏幕顶部世界坐标 private Vector3 GetScreenTop() { var cam = Camera.main; if (cam != null) return cam.ScreenToWorldPoint(new Vector3(Screen.width * 0.5f, Screen.height, cam.transform.position.z * -1f)); return new Vector3(0, 5, 0); } /// 获取屏幕底部世界坐标 private Vector3 GetScreenBottom() { var cam = Camera.main; if (cam != null) return cam.ScreenToWorldPoint(new Vector3(Screen.width * 0.5f, 0, cam.transform.position.z * -1f)); return new Vector3(0, -5, 0); } // ==================== 销毁 ==================== /// /// 回收爆奖预制体 + 回收 Boss 本体 + 统一销毁所有临时特效。 /// public override void die() { CleanupBeforeDie(); // ⭐ 爆奖结束:统一销毁所有临时特效(FullScreen/AwardFrame/Settle/Crit/道具动画等) // 防止 PW_Effect_AwardFrame_XXX(Clone) / PW_Effect_Settle_XXX(Clone) 残留 for (int i = 0; i < _spawnedEffects.Count; i++) { if (_spawnedEffects[i] != null) Destroy(_spawnedEffects[i]); } _spawnedEffects.Clear(); if (bossCtrl != null) { // ⭐ Boss 本体已在最终死亡(isKill=true)时由 XingJiXunHang_BOSS 直接删除, // 这里不需要再调 Id_DieFish_NotFX(重复调用会报"找不到鱼"警告) bossCtrl.NotifyBossRemoved(); bossCtrl = null; } EffectPool.Instance.Return(gameObject); } /// /// 加分完成:先走基类(显示 BJEndUI + 加分 + 收尾), /// 然后删除/回收手动创建的 Boss 分数面板(_bossScoreTmp)。 /// protected override void OnScoreEnd() { base.OnScoreEnd(); // ⭐ 弹出 BJEndUI // ⭐ 弹出 BJEndUI 时回收/删除手动创建的 Boss 分数面板 if (_bossScoreTmp != null) { var board = _bossScoreTmp.transform.parent; if (board != null) Destroy(board.gameObject); _bossScoreTmp = null; Debug.Log("[XingJiXunHang] 弹出 BJEndUI:Boss 分数面板已删除"); } } private void OnDestroy() { StopAllCoroutines(); } }