| Main/System/Battle/BattleObject/BattleObject.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Main/System/Battle/BattleObject/HeroBattleObject.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Main/System/Battle/Motion/MotionBase.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Main/System/Battle/Skill/SkillBase.Cast.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Main/System/Battle/Skill/SkillBase.Finish.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
Main/System/Battle/BattleObject/BattleObject.cs
@@ -41,6 +41,10 @@ battleField = _battleField; } // SkillBase.MoveToTarget 发起的前冲 / 回位 tween 句柄(由 SkillBase 维护)。 // CanCastSkillAnimation 只判断这一个句柄,避免误伤 caster 身上其它业务 tween。 public Tween activeMoveTween; // ============ 抽象访问方法(子类返回各自的Team类型信息) ============ public abstract int GetPositionNum(); Main/System/Battle/BattleObject/HeroBattleObject.cs
@@ -198,6 +198,15 @@ public override bool CanCastSkillAnimation(SkillSkinConfig skillSkinConfig) { // 除了等 Spine 技能动画锁(playingSkillWithAnim),还必须等 caster 自身由 SkillBase.MoveToTarget // 发起的前冲 / 回位 tween 跑完。否则上一个技能的回位 tween 还在半路,下一个技能(尤其是被动 // BattleType=5)会在当前位置再发起前冲,旧回位 tween 和新前冲 tween 同时写 anchoredPosition, // 视觉表现是"没回位又突然冲出去攻击"。这里只判定 SkillBase 自己记录的移动 tween 句柄,不误伤 // 挂在 heroRectTrans 上的其它业务 tween。 if (activeMoveTween != null && activeMoveTween.IsActive() && !activeMoveTween.IsComplete()) { return false; } return motionBase.CanCastSkill(skillSkinConfig); } Main/System/Battle/Motion/MotionBase.cs
@@ -45,6 +45,13 @@ private bool playingSkill = false; private bool _playingSkillWithAnim = false; #if UNITY_EDITOR // [卡死诊断] 记录是谁把 playingSkillWithAnim 锁为 true、在哪一帧,供 SkillBase.ReportStuckIfNeeded 使用。 // 不做 Run() 内 watchdog(避免刷屏);只在 SkillBase 自身发现卡死时一次性把这些信息打出来。 private int _pswaSetTrueFrame = -1; private int _pswaSetTrueSkillId = 0; private string _pswaSetTrueStack = null; #endif private bool playingSkillWithAnim { get => _playingSkillWithAnim; @@ -58,6 +65,19 @@ // 每次变更打印栈,定位到底是谁把 playingSkillWithAnim 设 true 了、谁清/不清 BattleDebug.LogError($"[MotionBase owner={owner} hash={GetHashCode()}] playingSkillWithAnim {_playingSkillWithAnim} -> {value}\n" + UnityEngine.StackTraceUtility.ExtractStackTrace()); } // [卡死诊断] 维护锁的持有者信息(上一轮的字段被回滚了,这里是最小必要集合) if (value && !_playingSkillWithAnim) { _pswaSetTrueFrame = UnityEngine.Time.frameCount; _pswaSetTrueStack = UnityEngine.StackTraceUtility.ExtractStackTrace(); // skillId 由 ExecuteSkillAnim 紧接着赋值(setter 这里拿不到) } else if (!value && _playingSkillWithAnim) { _pswaSetTrueFrame = -1; _pswaSetTrueSkillId = 0; _pswaSetTrueStack = null; } #endif _playingSkillWithAnim = value; @@ -335,6 +355,10 @@ activeSkillTracks[trackIndex] = skillTrack; playingSkillWithAnim = true; #if UNITY_EDITOR // [卡死诊断] setter 里拿不到 skillId,这里补上,供 SkillBase.ReportStuckIfNeeded dump _pswaSetTrueSkillId = skillConfig != null ? skillConfig.SkillID : 0; #endif } playingSkill = true; @@ -857,6 +881,11 @@ #if UNITY_EDITOR /// <summary>卡死诊断用:暴露 playingSkillWithAnim 标志位。</summary> public bool PlayingSkillWithAnimForDebug => playingSkillWithAnim; // [卡死诊断] 给 SkillBase 卡死报告用:谁把 playingSkillWithAnim 锁为 true、在哪一帧。 public int PlayingSkillAnimOwnerSkillIdForDebug => _pswaSetTrueSkillId; public int PlayingSkillAnimOwnerFrameForDebug => _pswaSetTrueFrame; public string PlayingSkillAnimOwnerStackForDebug => _pswaSetTrueStack; #endif public bool CanStartDeath() Main/System/Battle/Skill/SkillBase.Cast.cs
@@ -10,6 +10,17 @@ // 技能释放主逻辑:广播事件、高亮目标、执行释放 public virtual void Cast() { #if UNITY_EDITOR // [前冲诊断] Cast() 入口:记录 skillId/caster/BattleType/当前位置/当前是否还在 tween。 // 用来判断新技能是否在上一个技能的回位 tween 还没完成时就进入 Cast()。 { Vector2 castEntryPos = (caster != null && caster.GetRectTransform() != null) ? caster.GetRectTransform().anchoredPosition : Vector2.zero; bool casterTweening = caster != null && caster.GetRectTransform() != null && DG.Tweening.DOTween.IsTweening(caster.GetRectTransform()); BattleDebug.LogError($"[前冲诊断] Cast 入口 skillId={skillConfig?.SkillID} caster={caster?.ObjID} battleType={tagUseSkillAttack?.BattleType} castMode={skillSkinConfig?.castMode} anchoredPos={castEntryPos} casterTweening={casterTweening}"); } #endif // 广播技能释放事件 string guid = battleField.guid; // 获取释放者数据:Hero 传递 teamHero,Mingge 传递 null(因为事件监听器只处理 Hero 数据) @@ -163,9 +174,15 @@ // 执行移动-施法-返回序列:通用的移动攻击流程 private void ExecuteMoveAndCastSequence(RectTransform target, Action onReturnComplete) { #if UNITY_EDITOR BattleDebug.LogError($"[前冲诊断] ExecuteMoveAndCastSequence 开始 skillId={skillConfig?.SkillID} caster={caster?.ObjID} battleType={tagUseSkillAttack?.BattleType} CastDistance={skillSkinConfig?.CastDistance} castMode={skillSkinConfig?.castMode}"); #endif ShadowIllutionCreate(true); MoveToTarget(target, new Vector2(skillSkinConfig.CastDistance, 0), () => { #if UNITY_EDITOR BattleDebug.LogError($"[前冲诊断] 前冲完成 skillId={skillConfig?.SkillID} caster={caster?.ObjID} 准备 CastImpl"); #endif if (skillSkinConfig.CastDistance < 9999 && skillSkinConfig.SkinllSFX2 != 0) { battleField.soundManager.PlayEffectSound(skillSkinConfig.SkinllSFX2, false); @@ -197,18 +214,52 @@ // 移动到目标位置:处理角色的移动动画和逻辑 protected void MoveToTarget(RectTransform target, Vector2 offset, Action _onComplete = null, float speed = 750f) { #if UNITY_EDITOR // [前冲诊断] 记录入口参数:CastDistance、offset、speed;以及当前 caster 的锚点位置。 Vector2 fromPos = caster != null && caster.GetRectTransform() != null ? caster.GetRectTransform().anchoredPosition : Vector2.zero; bool mttTweening = caster != null && caster.GetRectTransform() != null && DG.Tweening.DOTween.IsTweening(caster.GetRectTransform()); BattleDebug.LogError($"[前冲诊断] MoveToTarget 入口 skillId={skillConfig?.SkillID} caster={caster?.ObjID} battleType={tagUseSkillAttack?.BattleType} CastDistance={skillSkinConfig?.CastDistance} offset={offset} speed={speed} fromPos={fromPos} casterTweening={mttTweening}"); #endif if (skillSkinConfig.CastDistance >= 9999) { #if UNITY_EDITOR BattleDebug.LogError($"[前冲诊断] CastDistance>=9999 直接跳过移动 skillId={skillConfig?.SkillID} caster={caster?.ObjID}"); #endif _onComplete?.Invoke(); return; } caster.PlayAnimation(MotionName.run, true); #if UNITY_EDITOR // [前冲诊断] 记录 target 的名字/世界坐标/父节点 scale,便于定位镜像坐标系导致 offset 方向反转 string targetName = target != null ? target.name : "(null)"; Vector3 targetWorld = target != null ? target.position : Vector3.zero; Vector3 targetLossyScale = target != null ? (Vector3)target.lossyScale : Vector3.one; Vector2 targetAnchored = target != null ? target.anchoredPosition : Vector2.zero; BattleDebug.LogError($"[前冲诊断] target信息 skillId={skillConfig?.SkillID} caster={caster?.ObjID} casterCamp={caster?.Camp} target.name={targetName} target.anchoredPos={targetAnchored} target.worldPos={targetWorld} target.lossyScale={targetLossyScale}"); #endif var tweener = BattleUtility.MoveToTarget(caster.GetRectTransform(), target, offset, () => { #if UNITY_EDITOR Vector2 toPos = caster != null && caster.GetRectTransform() != null ? caster.GetRectTransform().anchoredPosition : Vector2.zero; BattleDebug.LogError($"[前冲诊断] MoveToTarget 完成 skillId={skillConfig?.SkillID} caster={caster?.ObjID} toPos={toPos}"); #endif // tween 完成时清除 caster 上的 activeMoveTween 句柄,放开 CanCastSkillAnimation 的闸门。 if (caster != null) { caster.activeMoveTween = null; } caster.PlayAnimation(MotionName.idle, true); _onComplete?.Invoke(); }, speed); // 记录到 caster,让 CanCastSkillAnimation 能精确等待这一个 tween(而不是 caster 身上任意 tween)。 if (caster != null) { caster.activeMoveTween = tweener; } battleField.battleTweenMgr.OnPlayTween(tweener); } @@ -225,6 +276,16 @@ // 攻击完成后的处理:转身、恢复状态、播放待机动画 protected void OnAttackFinish() { #if UNITY_EDITOR // [前冲诊断] OnAttackFinish 入口:记录帧号和当前位置,和 Cast()/MoveToTarget 日志对齐。 { Vector2 finPos = (caster != null && caster.GetRectTransform() != null) ? caster.GetRectTransform().anchoredPosition : Vector2.zero; bool finTweening = caster != null && caster.GetRectTransform() != null && DG.Tweening.DOTween.IsTweening(caster.GetRectTransform()); BattleDebug.LogError($"[前冲诊断] OnAttackFinish skillId={skillConfig?.SkillID} caster={caster?.ObjID} battleType={tagUseSkillAttack?.BattleType} anchoredPos={finPos} casterTweening={finTweening}"); } #endif TurnBack(null, 1f); OnAllAttackMoveFinished(); caster.PlayAnimation(MotionName.idle, true); Main/System/Battle/Skill/SkillBase.Finish.cs
@@ -190,6 +190,22 @@ if (caster is HeroBattleObject hbo && hbo.motionBase != null) { casterAnim = $" caster.motionBase: playingSkillWithAnim={hbo.motionBase.PlayingSkillWithAnimForDebug}"; // [卡死诊断] 当 playingSkillWithAnim=true 但本 skillBase 还没 OnSkillStart, // 一次性把 MotionBase 的现场也 dump 出来:锁是谁加的 + 活跃轨道列表 + 加锁时的调用栈。 // 这样 SkillBase 的 120 帧卡死报告就能直接定位到 MotionBase 侧的 owner skill。 if (hbo.motionBase.PlayingSkillWithAnimForDebug) { int ownerSid = hbo.motionBase.PlayingSkillAnimOwnerSkillIdForDebug; int ownerFrame = hbo.motionBase.PlayingSkillAnimOwnerFrameForDebug; int elapsed = ownerFrame > 0 ? (UnityEngine.Time.frameCount - ownerFrame) : -1; casterAnim += $"\n MotionBase锁持有者: skillId={ownerSid} setFrame={ownerFrame} 已持有{elapsed}帧"; casterAnim += $"\n MotionBase现场: {hbo.motionBase.DumpActiveTracksForDebug()}"; string ownerStack = hbo.motionBase.PlayingSkillAnimOwnerStackForDebug; if (!string.IsNullOrEmpty(ownerStack)) { casterAnim += $"\n MotionBase锁加锁调用栈:\n{ownerStack}"; } } } string skinInfo = $" skillSkinConfig.SkillMotionName={(skillSkinConfig == null ? "null" : (string.IsNullOrEmpty(skillSkinConfig.SkillMotionName) ? "(空)" : skillSkinConfig.SkillMotionName))}"; string skillEffectDump = skillEffect == null ? " skillEffect=null" : $" skillEffect: {skillEffect.DumpState()}";