Files
zeling_v2/Assets/_Game/Scripts/Enemies/Abilities/RushAbility.cs
T
joywayer 47c2ce7e99 fix(enemy): 冲锋禁止以中心悬空姿态收尾(会导致导航映射失败/巡逻失效)
新增 EnemyMovement.IsCenterOverGround() 与 RushAbility.SettleRoutine:
冲锋结束时若着地却中心悬在崖外,继续朝原方向推进直到离地(自由落体)或中心重回地面上方;
有时限,超时仍未脱离则告警暴露地形问题(崖沿有墙),不静默留下坏站位。
2026-07-27 15:50:01 +08:00

188 lines
8.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections;
using Animancer;
using UnityEngine;
namespace BaseGames.Enemies.Abilities
{
/// <summary>
/// 冲锋能力:起手(Skill_Start) → 朝**锁定点**直冲(Skill_Loop) → 收招(Skill_End)。
/// 一次冲锋为一次能力执行,结束后进入冷却(EnemyAbilitySO.cooldown)。
///
/// 冲锋目标在**起手动画之前**锁定:记下玩家当时的 X 与由此确定的方向,此后玩家怎么动都不改变本次冲锋
/// committed 冲锋——起手即预警,玩家可在起手期间跑开躲掉)。
///
/// 终点判据是"**身体整体越过**锁定点"<see cref="IEnemyBody.HasPassed"/>),即碰撞体后缘越过锁定 X,
/// 而非中心到达——身体宽度取自身体几何权威 <see cref="EnemyBase.Body"/>,本能力不自行解析碰撞体。
///
/// 终止条件(任一满足即收招并进入冷却):身体整体越过锁定点(正常完成)/前方有墙(只判墙,不判悬崖)/
/// 超过 <see cref="RushAbilitySO.maxDashDuration"/>(够不到锁定点时收尾:掉坑、对岸、被非检测层挡住)/
/// 净位移卡死(撞上未纳入墙层的碰撞体时兜底)。
///
/// 悬崖:冲锋期间**允许越过崖沿**(经 <see cref="EnemyMoveInput.AllowLedgeCross"/> 放开移动层的悬崖夹紧),
/// 敌人会冲出平台边缘并按重力下落、空中保持水平冲速,而不是停在崖边悬空。
///
/// <b>收尾不变量</b>:冲锋结束时**绝不允许**停在"着地但身体中心悬在崖外"的姿态——
/// 该姿态会让导航无法把敌人映射到可行走段,导致巡逻/寻路失效。由 SettleRoutine 保证(见其说明)。
///
/// 冲锋动画由本能力全权驱动(步态不抢动画)。身体接触伤害与本能力解耦:
/// 由常驻的 <see cref="BodyContactDamage"/> 全程负责,本能力不开关它。
/// </summary>
public class RushAbility : EnemyAbilityBase
{
private RushAbilitySO _cfg;
// 卡死检测:窗口时长与窗口内最小净位移(撞上未纳入墙层的碰撞体时兜底结束冲锋)
private const float StuckWindowSeconds = 0.15f;
private const float StuckMinProgress = 0.05f;
// 落位收敛的最长额外推进时长(秒):用于脱离"中心悬在崖外"的非法收尾姿态
private const float SettleMaxSeconds = 0.5f;
protected override void Awake()
{
base.Awake();
_cfg = ResolveConfig<RushAbilitySO>();
}
protected override IEnumerator ExecuteCoroutine()
{
if (_cfg == null) yield break;
Phase = AbilityRunState.Active;
_enemy.SetEngaged(true);
// ── 锁定:起手之前记下玩家 X 与方向,此后不再跟随(committed 冲锋,可被躲)──
var player = _enemy.PlayerTransform;
float dir = (player != null && player.position.x < _enemy.transform.position.x) ? -1f : 1f;
float lockX = player != null
? player.position.x
: _enemy.transform.position.x + dir; // 无玩家:朝当前朝向冲一个身位,仍走完整流程
var mv = _enemy.Movement;
// 先转身朝锁定方向(背对时播转身动画),转身完成后再起手
if (mv != null)
{
mv.FaceDirection((int)dir);
while (mv.IsTurning)
yield return null;
}
// 起手:一次性预警动画。期间静止;锁住动画防被步态覆盖。
if (_cfg.startClip.Clip != null)
{
_enemy.LockAnim(_cfg.startClip.Clip.length);
_animancer.Play(_cfg.startClip);
yield return EnemyAbilityWaits.Get(_cfg.startClip.Clip.length);
}
// 冲刺速度:优先能力配置的 dashSpeed,否则回退 Stats.RunSpeed
float speed = _cfg.dashSpeed > 0f ? _cfg.dashSpeed
: (_enemy.Stats != null ? _enemy.Stats.RunSpeed : 0f);
if (_cfg.loopClip.Clip != null)
_animancer.Play(_cfg.loopClip);
yield return RushRoutine(dir, lockX, speed, mv);
yield return SettleRoutine(dir, speed, mv); // 收尾姿态必须合法:中心不得悬在崖外
_enemy.StopMovement();
// 收招 Skill_End;随后基类按 EnemyAbilitySO.cooldown 施加冷却。
yield return PlaySkillEnd();
Cleanup();
}
/// <summary>
/// 冲锋推进:每帧朝锁定方向施加冲速(允许越崖),
/// 直到身体整体越过锁定点/撞墙/超时/卡死。
/// </summary>
private IEnumerator RushRoutine(float dir, float lockX, float speed, EnemyMovement mv)
{
var body = _enemy.Body;
float elapsed = 0f;
float winX = _enemy.transform.position.x;
float winT = 0f;
while (true)
{
// 终点:身体整体越过锁定点(后缘越过;宽度由身体权威提供)
if (body != null && body.HasPassed(lockX, dir)) break;
// 撞墙即止(悬崖不再终止——冲锋要能冲下去)
if (mv != null && mv.WouldHitWallAhead(dir)) break;
// 超时收尾:够不到锁定点(掉坑/对岸/被非检测层挡住)
if (_cfg.maxDashDuration > 0f && elapsed >= _cfg.maxDashDuration) break;
_enemy.MoveInDirectionWithSpeed(dir, speed, allowLedgeCross: true);
yield return null;
float dt = Time.deltaTime;
elapsed += dt;
winT += dt;
// 兜底:撞上未纳入墙层的碰撞体(玩家/边界墙)导致原地不动 → 结束本次冲锋。
// 用窗口净位移判定,避免"贴住微抖"每帧重置。
if (winT >= StuckWindowSeconds)
{
if (Mathf.Abs(_enemy.transform.position.x - winX) < StuckMinProgress) break;
winX = _enemy.transform.position.x;
winT = 0f;
}
}
}
/// <summary>
/// 落位收敛:**禁止**冲锋以"着地但身体中心悬在崖外"的姿态收尾。
/// <para>该姿态下碰撞体后半截压在平台角上、物理会把敌人托住(刚体冻结旋转),看似停稳,
/// 但导航无法把此位置映射到可行走段,会让巡逻/寻路彻底失效。</para>
/// <para>处理:继续朝原方向推进,直到离地(转入自由落体,随后正常落地)或中心重回地面上方。
/// 有时限;若到时仍未脱离(多为崖沿处有墙把敌人卡住的地形问题),显式告警暴露根因,不静默留下坏站位。</para>
/// </summary>
private IEnumerator SettleRoutine(float dir, float speed, EnemyMovement mv)
{
if (mv == null) yield break;
float t = 0f;
while (mv.IsGrounded && !mv.IsCenterOverGround() && t < SettleMaxSeconds)
{
_enemy.MoveInDirectionWithSpeed(dir, speed, allowLedgeCross: true);
yield return null;
t += Time.deltaTime;
}
if (mv.IsGrounded && !mv.IsCenterOverGround())
Debug.LogWarning($"[RushAbility] '{_enemy.name}' 冲锋收尾时身体中心悬在崖外且无法脱离" +
"(多为崖沿处存在墙体阻挡)。该站位会使导航无法映射到可行走段,巡逻将失效——请检查该处地形。", this);
}
protected override void OnInterrupted(InterruptReason reason)
{
// 丢失目标 = AI 主动退出(ExternalRequest) → 播收招 Skill_End
// 受击/死亡/硬直等硬打断不播收招。协程已停,fire-and-forget;动画锁保证 Skill_End
// 在其时长内不被巡逻步态盖掉。
if (reason == InterruptReason.ExternalRequest && _cfg != null && _cfg.endClip.Clip != null)
{
_enemy.LockAnim(_cfg.endClip.Clip.length);
_animancer.Play(_cfg.endClip);
}
Cleanup();
}
private IEnumerator PlaySkillEnd()
{
if (_cfg == null || _cfg.endClip.Clip == null) yield break;
_enemy.LockAnim(_cfg.endClip.Clip.length);
_animancer.Play(_cfg.endClip);
yield return EnemyAbilityWaits.Get(_cfg.endClip.Clip.length);
}
private void Cleanup()
{
_enemy.SetEngaged(false);
_enemy.Locomotion?.Stop(); // 经 StopMovement 路径一并复位 AllowLedgeCross
}
}
}