fix(enemy): 无受击/僵直动画时不再永久卡死
EnemyHurtState 与 EnemyStaggerState 的 Enter 在对应 Clip 为空时守卫式早退, **同时丢掉了恢复安排**:_currentState 永远停在该态,IsControllable 恒假, AiRuntime 的让位门永久挡住条件边求值——敌人被打一次即永久失能。 美术未接入时这影响的是大部分敌人。 TakeDamage 是分发点:霸体被破进 Stagger、否则进 Hurt,两条都可达, 所以这是两个活 bug 而非一个。EnemyKnockUpState 本就有 else 分支走 ScheduleStateRecovery,是正确范式;本次把该分支补给另外两个,并加一条 KnockUp 回归测试守住它。 时长权威没有新造: - Hurt 接上 DamageSourceSO.HitStunDuration——该字段一路铺到 DamageInfo 却零消费者,本就是「这一击让对方僵多久」的既有权威;经 PendingHitStun 传给状态,形状与既有的 PendingLaunchDir 一致。 - Stagger 用 HitTierConfig.staggerDuration——该结构 tooltip 本就写着 「Stagger / KnockUp 参数」却只有 KnockUp 的字段,属补齐而非发明。 顺带修 ScheduleStateRecovery 不取消前一次安排的问题:两个重叠安排里较短的 会先把状态改回 Controlled,静默缩短调用方想要的时长(ReceiveParry 接线后 就会撞上)。并在 OnSpawn 清掉跨池化周期残留的协程句柄。 注:Knockback 目前零调用者,其中的 PendingHitStun 赋值今天是死代码, 但它是 public virtual 且会进 Hurt 态,不设则将来接线时会用到陈旧时长。 验证:编译 0 错;EditMode 263/263(260 + 新增 3)。 变异验证——移除两个 else 分支后恰好 Hurt/Stagger 两条测试变红、 KnockUp 回归测试保持绿,恢复后复验 263/263。
This commit is contained in:
@@ -179,6 +179,8 @@ namespace BaseGames.Enemies
|
||||
}
|
||||
else
|
||||
{
|
||||
// 存储硬直时长供 EnemyHurtState 在无受击动画时使用
|
||||
_pendingHitStun = info.HitStunDuration;
|
||||
nextState = EnemyStateType.Hurt;
|
||||
reason = InterruptReason.Hurt;
|
||||
}
|
||||
@@ -200,17 +202,37 @@ namespace BaseGames.Enemies
|
||||
private Vector2 _pendingLaunchDir;
|
||||
|
||||
/// <summary>
|
||||
/// 协程兜底:在无对应 Animancer 动画时按时长自动恢复到 Controlled 状态。
|
||||
/// 仅在 AnimConfig 对应 Clip 为 null 时由状态类调用。
|
||||
/// 本次受击的硬直时长(由 TakeDamage / Knockback 从 <see cref="DamageInfo.HitStunDuration"/> 写入,
|
||||
/// 供 <see cref="States.EnemyHurtState"/> 在无受击动画时作为恢复时长)。
|
||||
/// 权威在攻击方:同一敌人挨重击比挨轻击僵得久。有受击动画时以动画长度为准。
|
||||
/// </summary>
|
||||
public void ScheduleStateRecovery(EnemyStateType fromState, float delay)
|
||||
internal float PendingHitStun => _pendingHitStun;
|
||||
private float _pendingHitStun;
|
||||
|
||||
/// <summary>
|
||||
/// 在无对应 Animancer 动画时按时长自动恢复到 Controlled 状态。
|
||||
/// 由受击类状态(Hurt / Stagger / KnockUp)在其 Clip 为 null 时调用——
|
||||
/// 缺了它,敌人会永久停在该态,<c>IsControllable</c> 恒假、决策层永久停摆。
|
||||
///
|
||||
/// 同一时刻只保留一个待恢复安排:后一次调用取消前一次。
|
||||
/// 否则两次重叠安排里较短的那个会先把状态改回 Controlled,
|
||||
/// 静默缩短调用方本想要的时长(如弹反惩罚窗口)。
|
||||
///
|
||||
/// virtual:供 EditMode 测试替换掉协程,直接断言"是否安排了恢复"。
|
||||
/// </summary>
|
||||
public virtual void ScheduleStateRecovery(EnemyStateType fromState, float delay)
|
||||
{
|
||||
StartCoroutine(StateRecoveryRoutine(fromState, delay));
|
||||
if (_stateRecovery != null) StopCoroutine(_stateRecovery);
|
||||
_stateRecovery = StartCoroutine(StateRecoveryRoutine(fromState, delay));
|
||||
}
|
||||
|
||||
private Coroutine _stateRecovery;
|
||||
|
||||
private System.Collections.IEnumerator StateRecoveryRoutine(EnemyStateType fromState, float delay)
|
||||
{
|
||||
yield return new WaitForSeconds(delay);
|
||||
if (delay > 0f) yield return new WaitForSeconds(delay);
|
||||
else yield return null; // 时长为 0 = 不硬直,下一帧即恢复
|
||||
_stateRecovery = null;
|
||||
if (_currentState == fromState)
|
||||
ForceState(EnemyStateType.Controlled);
|
||||
}
|
||||
@@ -385,6 +407,7 @@ namespace BaseGames.Enemies
|
||||
if (info.Flags.HasFlag(DamageFlags.NoKnockback)) return;
|
||||
_movement?.ApplyKnockback(info.KnockbackDirection, info.KnockbackForce);
|
||||
// 统一路径:击退必须经过状态机,确保能力被中断且动画一致。
|
||||
_pendingHitStun = info.HitStunDuration; // 无受击动画时的恢复时长
|
||||
ForceState(EnemyStateType.Hurt);
|
||||
_abilities.InterruptAll(InterruptReason.Hurt);
|
||||
}
|
||||
@@ -727,6 +750,9 @@ namespace BaseGames.Enemies
|
||||
// 重置状态(对象池复用:跳过 Dead 终态守卫,强制恢复到 Controlled)
|
||||
ForceStateRespawn(EnemyStateType.Controlled);
|
||||
IsEngaged = false;
|
||||
// 上一条命若在受击恢复途中被回收,协程已随物体停用而终止,
|
||||
// 这里清掉句柄,避免跨池化周期持有一个已死的 Coroutine 引用
|
||||
_stateRecovery = null;
|
||||
|
||||
// 重置对象池复用相关的运行时感知数据
|
||||
// 注意:_playerTransform 不重置(场景中玩家仍存在),只重置追踪历史
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace BaseGames.Enemies
|
||||
[Min(0f)] public float launchHorzForce;
|
||||
[Tooltip("无 KnockUp 动画时的状态持续时长(s)")]
|
||||
[Min(0f)] public float knockUpDuration;
|
||||
[Tooltip("无 Stagger 动画时的僵直持续时长(s);0 = 用默认 0.5s。" +
|
||||
"受击(Hurt)的时长不在这里——它取攻击方 DamageSourceSO 的 HitStunDuration")]
|
||||
[Min(0f)] public float staggerDuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,7 +2,8 @@ namespace BaseGames.Enemies.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 受击状态。播放受击动画,动画结束后自动回到 Controlled。
|
||||
/// 原 EnemyBase.ForceState 中的 Hurt if-else 逻辑已迁移至此。
|
||||
/// 无受击动画时按 <see cref="EnemyBase.PendingHitStun"/>(来自攻击方的硬直时长)恢复——
|
||||
/// 二者必须都安排恢复,否则敌人会永久停在本态,决策层被 IsControllable 门永久挂起。
|
||||
/// </summary>
|
||||
public sealed class EnemyHurtState : IEnemyState
|
||||
{
|
||||
@@ -10,15 +11,22 @@ namespace BaseGames.Enemies.States
|
||||
|
||||
public void Enter(EnemyBase owner)
|
||||
{
|
||||
if (owner.Animancer == null || owner.AnimConfig?.Hurt == null) return;
|
||||
|
||||
var animState = owner.Animancer.Play(owner.AnimConfig.Hurt);
|
||||
animState.Events(owner).OnEnd = () =>
|
||||
if (owner.Animancer != null && owner.AnimConfig?.Hurt != null)
|
||||
{
|
||||
// 只在仍处于 Hurt 时才回 Controlled,避免 Die 时被覆盖
|
||||
if (owner.CurrentState == EnemyStateType.Hurt)
|
||||
owner.ForceState(EnemyStateType.Controlled);
|
||||
};
|
||||
var animState = owner.Animancer.Play(owner.AnimConfig.Hurt);
|
||||
animState.Events(owner).OnEnd = () =>
|
||||
{
|
||||
// 只在仍处于 Hurt 时才回 Controlled,避免 Die 时被覆盖
|
||||
if (owner.CurrentState == EnemyStateType.Hurt)
|
||||
owner.ForceState(EnemyStateType.Controlled);
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无受击动画:按本次攻击的硬直时长恢复。
|
||||
// 时长为 0 是合法的"不硬直",由 ScheduleStateRecovery 处理为下一帧恢复。
|
||||
owner.ScheduleStateRecovery(EnemyStateType.Hurt, owner.PendingHitStun);
|
||||
}
|
||||
}
|
||||
|
||||
public void Exit(EnemyBase owner) { }
|
||||
|
||||
@@ -3,21 +3,35 @@ namespace BaseGames.Enemies.States
|
||||
/// <summary>
|
||||
/// 僵直状态(霸体耗尽时触发)。
|
||||
/// 播放 Stagger 动画(若配置),动画结束后自动回到 Controlled。
|
||||
/// 无僵直动画时按 <see cref="EnemyStatsSO.HitTiers"/> 的 staggerDuration 恢复——
|
||||
/// 二者必须都安排恢复,否则敌人会永久停在本态,决策层被 IsControllable 门永久挂起。
|
||||
/// </summary>
|
||||
public sealed class EnemyStaggerState : IEnemyState
|
||||
{
|
||||
/// <summary>未配置 staggerDuration 时的兜底时长(与 KnockUp 同规格)。</summary>
|
||||
private const float DefaultStaggerDuration = 0.5f;
|
||||
|
||||
public EnemyStateType StateType => EnemyStateType.Stagger;
|
||||
|
||||
public void Enter(EnemyBase owner)
|
||||
{
|
||||
if (owner.Animancer == null || owner.AnimConfig?.Stagger == null) return;
|
||||
|
||||
var animState = owner.Animancer.Play(owner.AnimConfig.Stagger);
|
||||
animState.Events(owner).OnEnd = () =>
|
||||
if (owner.Animancer != null && owner.AnimConfig?.Stagger != null)
|
||||
{
|
||||
if (owner.CurrentState == EnemyStateType.Stagger)
|
||||
owner.ForceState(EnemyStateType.Controlled);
|
||||
};
|
||||
var animState = owner.Animancer.Play(owner.AnimConfig.Stagger);
|
||||
animState.Events(owner).OnEnd = () =>
|
||||
{
|
||||
if (owner.CurrentState == EnemyStateType.Stagger)
|
||||
owner.ForceState(EnemyStateType.Controlled);
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无僵直动画:按配置时长恢复。僵直是给玩家的惩罚窗口,
|
||||
// 时长必须为正,否则等同于霸体被破却没有任何回报。
|
||||
float cfg = owner.StatsSO?.HitTiers.staggerDuration ?? 0f;
|
||||
float duration = cfg > 0f ? cfg : DefaultStaggerDuration;
|
||||
owner.ScheduleStateRecovery(EnemyStateType.Stagger, duration);
|
||||
}
|
||||
}
|
||||
|
||||
public void Exit(EnemyBase owner) { }
|
||||
|
||||
Reference in New Issue
Block a user