Files
zeling_v2/Docs/Design/05_ParrySystem.md
2026-05-08 11:04:00 +08:00

19 KiB
Raw Permalink Blame History

05 · 弹反系统

命名空间 BaseGames.Parry
所属文档集 ← 返回索引 · 总览
依赖 BaseGames.Player · BaseGames.Combat · BaseGames.Feedback · Feel MMTimeManager


目录

  1. 设计目标与核心体验
  2. 弹反状态机
  3. 时间流完整流水线
  4. 弹反判定窗口
  5. 子弹时间Bullet Time
  6. 反击窗口Counter Window
  7. 敌人 Stagger
  8. Soul 增益
  9. ParryConfigSO — 配置资产
  10. 事件频道
  11. 完美弹反P1
  12. 编辑器友好设计
  13. 帧嵌入式弹反窗口

1. 设计目标与核心体验

弹反是游戏最核心的爽点机制,设计目标:

目标 实现方式
打击感强烈 成功弹反瞬间冻帧2帧+ 子弹时间 0.25× + Cinemachine Impulse
高风险高回报 弹反窗口 0.28s,需要精确预判
战术价值 成功后 +33 Soul1/3 满格)+ 3× 反击伤害 + 使敌人 Stagger
失败可控 错误弹反(弹反窗口前受伤)不会有额外惩罚
视觉清晰 弹反姿态动画清晰可辨,成功闪光明显

2. 弹反状态机

ParrySystem 是独立组件(挂在玩家身上),与 PlayerStateMachine 协作:

[ParrySystem 内部状态枚举]

Inactive
   │
   ├─ 玩家进入 ParryState按弹反键
   ▼
Startup弹反前摇不可受益
   │ 持续: ParryStartupDuration (0.05s)
   ▼
Active弹反窗口可弹反  ← IsInParryWindow = true
   │ 持续: ParryWindowDuration (0.28s)
   │
   ├─ 受到带 CanBeParried 的 DamageInfo → 弹反成功 →
   │        ▼
   │    ParrySuccess成功处理
   │        ├─ 冻帧 (2frames)
   │        ├─ 启动子弹时间
   │        ├─ +33 Soul
   │        ├─ 触发 OnParrySuccess 事件频道
   │        └─ 进入 CounterWindow
   │             │ 持续: CounterWindowDuration (0.5s)
   │             ├─ 攻击输入 → AttackState (ParryCounter)
   │             └─ 超时/其他输入 → 返回 Inactive
   │
   └─ 窗口超时(无攻击命中)→ 进入 Endlag
         │ 持续: ParryEndlagDuration (0.1s)
         ▼
      Inactive返回玩家恢复控制

帧嵌入弹反Skill-Frame Parry第二触发路径
AnimationEvent EnableParryWindow 调用 ParrySystem.OpenWindow(),直接进入 Active 状态,跳过 Startup/Endlag。窗口关闭由 AnimationEvent DisableParryWindow 驱动,时长由动画帧间隔决定。详见 §13。


3. 时间流完整流水线

玩家按下 Parry 键
      │
      ▼
InputReader.OnParryPerformed
      │
      ▼
PlayerController → ForceState(ParryState)
      │
      ├─ 播放 ParryPose 动画
      └─ ParrySystem.BeginParry()
            │
            [Startup 0.05s]
            │
            [Active Window 0.28s] ←── 受到攻击
                  │                        │
                  │          HurtBox.ReceiveDamage(DamageInfo)
                  │                        │
                  │          检测到 IsInParryWindow == true
                  │                        │
                  │          && DamageInfo.Flags.HasFlag(CanBeParried)
                  │                        │
                  ├────────────────────────┘
                  │
                  ▼
            ParrySystem.TriggerParrySuccess(DamageInfo)
                  │
                  ├─ 1. 播放 ParrySuccess 动画
                  ├─ 2. HitStopManager.FreezeFrames(2)
                  ├─ 3. MMTimeManager.SetTimescale(0.25, 0.2s)
                  ├─ 4. CinemachineImpulseSource_Parry.GenerateImpulse()
                  ├─ 5. PlayerStats.AddSoul(+33)
                  ├─ 6. 触发 OnParrySuccess 事件频道(附带原 DamageInfo
                  └─ 7. 进入 CounterWindow 状态

4. 弹反判定窗口

时间轴(从按下弹反键开始):

├─[0ms]──[50ms]──────[330ms]──[430ms]─────────────►
  按键    结束          窗口      Endlag
         Startup        Active    结束

  ◄── 0.05s ──►◄──── 0.28s ────►◄── 0.1s ──►
     Startup       可弹反窗口      Endlag

弹反成功条件(同时满足):

  1. ParrySystem.State == Active(在弹反窗口内)
  2. DamageInfo.Flags.HasFlag(CanBeParried) 为 true
  3. 玩家当前未处于无敌帧(无敌帧中收不到 DamageInfo 触发弹反)

弹反失败情况

情况 结果
Startup 期间受攻击 正常受击(无弹反,无额外惩罚)
Active 窗口超时 进入 Endlag返回 Inactive
受到 Unblockable 攻击 无法弹反,正常受击
受到 PerfectParryOnly 攻击 仅限完美弹反P1 特性)

5. 子弹时间Bullet Time

弹反成功后立即通过 Feel MMTimeManager 降低全局时间缩放:

配置参数

参数 说明
BulletTimeScale 0.25 时间缩放倍率(原速 25%
BulletTimeDuration 0.2s非缩放时间 子弹时间持续时长
BulletTimeChannel MMTimeManager Channel 0 Feel 时间管理频道

影响范围

  • Time.timeScale = 0.25

    • 影响所有 Update物理帧、AI 决策、动画)
    • 敌人 AI 决策频率降低
    • 玩家动画也会减速(营造英雄感)
  • 不受影响(使用 Time.unscaledDeltaTime

    • 镜头震动计时
    • UI 动画
    • 音频(正常速度播放)

MMTimeScaleEvent 广播

Feel 的 MMTimeManager 接收 MMTimeScaleEvent

参数
TimeScale 0.25
Duration 0.2
Lerp true
LerpSpeed 20快速进入快速恢复
Infinite false

6. 反击窗口Counter Window

子弹时间内(以及结束后的短暂时间),玩家处于反击窗口:

弹反成功时刻
      │
      ├─ 子弹时间开始0.25×, 0.2s 非缩放时长)
      │
      └─ CounterWindow 开始0.5s 非缩放时长)
            │
            ├─ 玩家攻击输入 → AttackState(ParryCounter)
            │     └─ 使用 DS_Player_ParryCounter.asset×3 伤害Unblockable
            │
            └─ 0.5s 超时 → 反击窗口关闭
                  └─ 子弹时间已提前恢复0.2s < 0.5s

反击窗口关闭条件(任意一个触发):

  • CounterWindow 计时超过 0.5s
  • 玩家执行了 ParryCounter 攻击
  • 玩家受到伤害
  • 玩家再次进入弹反状态

7. 敌人 Stagger

OnParrySuccess 事件频道触发后,被弹反的敌人进入 Stagger 状态:

参数 说明
StaggerDuration 0.8s 敌人硬直时长(缩放时间)
StaggerKnockback (0.5× 原击退力) 弹反后敌人轻微后退

Stagger 流程

OnParrySuccess 事件频道DamageInfo
      │
      └─ EnemyBase.OnParrySuccessHandler(DamageInfo)
            ├─ 检查 DamageInfo.SourceLayer 是否为此敌人攻击
            │   (防止误触发其他敌人)
            ├─ ForceState(EnemyStaggerState)
            └─ EnemyFeedback.OnParriedByPlayer()
                  └─ MMF_Player: Flash + ImpulseSource_Light

8. Soul 增益

弹反结果 Soul 增量
弹反成功(任意攻击) +33满格的 1/3
弹反成功完美弹反P1 +50满格的 1/2
弹反失败(窗口超时) +0
弹反中受伤 +0按正常受击处理

9. ParryConfigSO — 配置资产

ParryConfigSO 存放于 Assets/ScriptableObjects/Config/Parry/ParryConfigSO.asset

参数 类型 推荐值 说明
StartupDuration float 0.05s 弹反前摇时长
WindowDuration float 0.28s 弹反判定窗口
EndlagDuration float 0.10s 弹反后摇
CounterWindowDuration float 0.5s(非缩放) 反击窗口时长
BulletTimeScale float 0.25 子弹时间缩放比
BulletTimeDuration float 0.2s(非缩放) 子弹时间时长
StaggerDuration float 0.8s 敌人 Stagger 时长
SoulGainOnParry int 33 弹反成功 Soul 增量
ParryCounterMultiplier float 3.0 弹反反击伤害倍率

10. 事件频道

频道资产 类型 发布时机 主要订阅方
OnParrySuccess.asset DamageInfoEventChannelSO 弹反成功瞬间 PlayerStats+SoulEnemyBaseStaggerPlayerFeedback(视觉/音效)、CameraSystemImpulse
OnParryWindowOpened.asset VoidEventChannelSO 进入弹反 Active 窗口 PlayerFeedback(弹反姿态特效)
OnParryWindowClosed.asset VoidEventChannelSO 弹反窗口关闭(超时/成功) PlayerFeedback(恢复正常 Shader

11. 完美弹反P1

优先级 P1当前版本不实现

完美弹反在弹反窗口的**前 1/3 时间(约 0.09s**内触发,额外效果:

  • Soul 增量提升至 +50满格 1/2
  • 无需消耗攻击次数即恢复连击链
  • 使 Boss 进入更长时间的 Stagger1.5s
  • 触发独立的 PerfectParryFeedback(更强烈的视觉/音效)

12. 编辑器友好设计

ParrySystem 运行时 Inspector

┌─ ParrySystem ──────────────────────────────────┐
│  State         : Active                        │
│  Window Timer  : █████████░░░░  0.19s / 0.28s │
│  Counter Timer : ██████░░░░░░░  0.30s / 0.50s │
│  Soul on Parry : +33                           │
│  ─────────────────────────────────────────────│
│  Bullet Time   : 0.25× for 0.2s (unscaled)   │
│  ─────────────────────────────────────────────│
│  [测试弹反成功] [测试弹反失败] [重置状态]      │
└────────────────────────────────────────────────┘

Scene 视图调试

  • 弹反窗口 Active 时:在玩家身上绘制黄色光圈动画(仅 Scene 视图,不影响游戏画面)
  • 反击窗口 Counter 时:绘制绿色扇形表示反击可用时间剩余

13. 帧嵌入式弹反窗口Skill-Frame Parry

除专用弹反按键(PlayerParryState)外,部分技能动作的特定帧自动携带弹反判定,无需玩家额外输入。

13.1 两种弹反触发路径对比

专用弹反PlayerParryState 帧嵌入式弹反
触发方式 玩家按 Parry 键 技能动作特定帧自动开启
FSM 状态变化 切换到 PlayerParryState 不切换状态,在当前技能状态内
驱动方式 ParrySystem.BeginParry()(代码计时器) AnimationEvent EnableParryWindow / DisableParryWindow
窗口时序 Startup (0.05s) → Active (0.28s) → Endlag (0.1s) 直接 Active时长 = 两事件帧间隔
玩家控制 主动意图(精确输入) 被动奖励(攻击中顺带)
代表场景 格挡反击 Attack2/Attack3 前摇帧、部分特殊技能

13.2 ParrySystem 双接口设计

public class ParrySystem : MonoBehaviour
{
    // ── 专用弹反接口(由 PlayerParryState.OnEnter 调用)──
    /// <summary>启动完整时序Startup → Active → Endlag代码计时器驱动。</summary>
    public void BeginParry() { ... }

    // ── 帧嵌入式弹反接口(由 AnimationEvent 调用)──
    /// <summary>直接进入 Active 状态,窗口时长由 DisableParryWindow 事件关闭。</summary>
    public void OpenWindow()
    {
        if (State != ParryState.Inactive) return;   // 避免与专用弹反冲突
        State = ParryState.Active;
    }

    /// <summary>直接退出 Active 状态(仅在帧嵌入模式下有效)。</summary>
    public void CloseWindow()
    {
        if (State == ParryState.Active) State = ParryState.Inactive;
    }

    public bool IsInParryWindow => State == ParryState.Active;
}

OpenWindow() 检查当前状态为 Inactive 才执行,避免在专用 PlayerParryState 已通过 BeginParry() 激活的情况下冲突。

13.3 携带弹反帧的技能列表

技能 弹反帧位置(归一化时间) 设计意图
攻击2Attack2 0.05 0.18(前摇蓄力阶段) 挥剑准备期可反击近身攻击
攻击3Attack3 0.05 0.12(大幅前摇阶段) 终结技前摇明显,高风险高回报
(预留)特殊技能 由技能设计决定 护符/解锁技能可扩展此列表

攻击1 节奏最快,不携带弹反帧。

13.4 帧嵌入弹反的后续处理

弹反成功后走相同的 ParrySystem.TriggerParrySuccess() 流程(子弹时间 + Soul +33 + 反击窗口),不区分来源。唯一区别:帧嵌入弹反成功时玩家处于攻击动画中,反击窗口的 ParryCounter 攻击会从当前状态直接切换,而非从 PlayerParryState 切换。


14. 弹反冲突解决Parry Conflict Resolution

本节定义边界情况下弹反系统的决策优先级,确保不出现"吃弹反"或"双重消耗"问题。

14.1 ParryResolveResult 枚举

public enum ParryResolveResult
{
    Parried,              // 弹反成功,伤害被消除
    Blocked_ByIFrame,     // 玩家处于无敌帧,弹反未触发(伤害也被无敌帧吸收)
    Failed_OutsideWindow, // 弹反窗口关闭,伤害正常生效
    Consumed,             // 同帧首次弹反已消耗,后续相同帧的弹反触发失败
    Rejected_NotParryable,// 攻击来源标记为不可弹反
}

14.2 同帧多伤害处理

在同一 FixedUpdate 帧内,多个伤害实体可能同时命中玩家(如群体爆炸、多段敌人攻击)。规则如下:

同帧伤害解决顺序(按命中顺序):
  第 1 次伤害(可弹反)→ 触发弹反 → ParryResolveResult.Parried
  第 2 次伤害(可弹反)→ 弹反已消耗 → ParryResolveResult.Consumed → 作为普通伤害处理
  第 N 次伤害         → 同上,普通伤害

实现方式:在 ParrySystem 内部维护 _parriedThisFrame 布尔标志,于 LateUpdate 重置:

public class ParrySystem : MonoBehaviour
{
    bool _parriedThisFrame;

    void LateUpdate() => _parriedThisFrame = false;

    // 修改后的 TryConsumeParry():
    public ParryResolveResult TryConsumeParry(DamageInfo info)
    {
        // 优先级 1不可弹反的攻击
        if (!info.isParryable)
            return ParryResolveResult.Rejected_NotParryable;

        // 优先级 2无敌帧中无敌帧由受击/冲刺触发)
        if (_playerStats.IsInvincible)
            return ParryResolveResult.Blocked_ByIFrame;

        // 优先级 3弹反窗口未激活
        if (State != ParryState.Active)
            return ParryResolveResult.Failed_OutsideWindow;

        // 优先级 4同帧已消耗弹反
        if (_parriedThisFrame)
            return ParryResolveResult.Consumed;

        // 弹反成功
        _parriedThisFrame = true;
        TriggerParrySuccess();
        return ParryResolveResult.Parried;
    }
}

14.3 无敌帧期间的弹反输入

场景 行为 原因
受击无敌帧内敌人攻击到达 无敌帧优先吸收,弹反不被消耗 避免惩罚玩家在受击后立刻进行输入
冲刺无敌帧内敌人攻击到达 同上,无敌帧优先,弹反不消耗 冲刺是主动规避,不触发弹反
弹反 Active 窗口中玩家主动触发无敌(护符等) 以进入无敌时的状态为准:若先进弹反再进无敌,弹反窗口继续有效;若先进无敌再有伤害到来,无敌优先 明确时序,避免双重规避

关键规则:无敌帧 (_playerStats.IsInvincible) 的检查优先于弹反窗口检查,且无敌帧不会消耗弹反计数。

14.4 弹反成功后的 Stagger 优先级

弹反成功时,目标敌人可能处于不同状态。解决规则:

敌人状态 弹反后行为
普通攻击动画中 立即打断 → 强制进入 EnemyStaggerState
普通 AI 行为(移动/待机) 同上,强制 Stagger
Boss 普通攻击 打断并触发 Stagger持续时间 = 普通 Boss Stagger约 0.6s
Boss 阶段技能(标记为 UnInterruptible 弹反成功(玩家获得 Soul 和反击窗口),但 Boss 不进入 Stagger,攻击继续
远程弹射物(不可弹反,弹射物反向) 弹射物速度取反,标记命中敌人为 ReflectedProjectile;不触发 Stagger
void ApplyParryStagger(IDamageable target, DamageInfo info)
{
    // 弹射物弹反:不对 Boss 施加 Stagger仅反射弹射物
    if (info.isProjectile)
    {
        info.projectile.Reflect();
        return;
    }

    // Boss 阶段技能:不可打断
    if (target is BossBase boss && boss.IsCurrentAttackUninterruptible)
        return;

    // 普通情况:施加 Stagger
    target.ApplyStagger(info.parriedStaggerDuration);
}

14.5 完整决策树

玩家受到攻击DamageInfo 到达 PlayerHurtBox
  │
  ├─ info.isParryable == false
  │      → 按普通伤害处理HP 扣减)
  │
  ├─ _playerStats.IsInvincible == true
  │      → 无敌帧吸收HP 不变,弹反不消耗)
  │
  ├─ ParrySystem.State != Active
  │      → 按普通伤害处理HP 扣减,弹反未激活)
  │
  ├─ _parriedThisFrame == true同帧已弹反
  │      → 伤害仍然生效(弹反 "已消耗" 提示ParryResolveResult.Consumed
  │
  └─ 弹反成功Parried
         → TriggerParrySuccess():子弹时间 + Soul +33
         → ApplyParryStagger(attacker, info)
         → 开启 CounterWindow反击窗口
         → _parriedThisFrame = true