# 05 · 弹反系统 > **命名空间** `BaseGames.Parry` > **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md) > **依赖** `BaseGames.Player` · `BaseGames.Combat` · `BaseGames.Feedback` · Feel `MMTimeManager` --- ## 目录 1. [设计目标与核心体验](#1-设计目标与核心体验) 2. [弹反状态机](#2-弹反状态机) 3. [时间流完整流水线](#3-时间流完整流水线) 4. [弹反判定窗口](#4-弹反判定窗口) 5. [子弹时间(Bullet Time)](#5-子弹时间bullet-time) 6. [反击窗口(Counter Window)](#6-反击窗口counter-window) 7. [敌人 Stagger](#7-敌人-stagger) 8. [Soul 增益](#8-soul-增益) 9. [ParryConfigSO — 配置资产](#9-parryconfigso--配置资产) 10. [事件频道](#10-事件频道) 11. [完美弹反(P1)](#11-完美弹反p1) 12. [编辑器友好设计](#12-编辑器友好设计) 13. [帧嵌入式弹反窗口](#13-帧嵌入式弹反窗口skill-frame-parry) --- ## 1. 设计目标与核心体验 弹反是游戏**最核心的爽点机制**,设计目标: | 目标 | 实现方式 | |------|---------| | **打击感强烈** | 成功弹反瞬间冻帧(2帧)+ 子弹时间 0.25× + Cinemachine Impulse | | **高风险高回报** | 弹反窗口 0.28s,需要精确预判 | | **战术价值** | 成功后 +33 Soul(1/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`(+Soul)、`EnemyBase`(Stagger)、`PlayerFeedback`(视觉/音效)、`CameraSystem`(Impulse)| | `OnParryWindowOpened.asset` | `VoidEventChannelSO` | 进入弹反 Active 窗口 | `PlayerFeedback`(弹反姿态特效)| | `OnParryWindowClosed.asset` | `VoidEventChannelSO` | 弹反窗口关闭(超时/成功)| `PlayerFeedback`(恢复正常 Shader)| --- ## 11. 完美弹反(P1) > **优先级 P1,当前版本不实现** 完美弹反在弹反窗口的**前 1/3 时间(约 0.09s)**内触发,额外效果: - Soul 增量提升至 +50(满格 1/2) - 无需消耗攻击次数即恢复连击链 - 使 Boss 进入更长时间的 Stagger(1.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 双接口设计 ```csharp public class ParrySystem : MonoBehaviour { // ── 专用弹反接口(由 PlayerParryState.OnEnter 调用)── /// 启动完整时序:Startup → Active → Endlag(代码计时器驱动)。 public void BeginParry() { ... } // ── 帧嵌入式弹反接口(由 AnimationEvent 调用)── /// 直接进入 Active 状态,窗口时长由 DisableParryWindow 事件关闭。 public void OpenWindow() { if (State != ParryState.Inactive) return; // 避免与专用弹反冲突 State = ParryState.Active; } /// 直接退出 Active 状态(仅在帧嵌入模式下有效)。 public void CloseWindow() { if (State == ParryState.Active) State = ParryState.Inactive; } public bool IsInParryWindow => State == ParryState.Active; } ``` > `OpenWindow()` 检查当前状态为 `Inactive` 才执行,避免在专用 `PlayerParryState` 已通过 `BeginParry()` 激活的情况下冲突。 ### 13.3 携带弹反帧的技能列表 | 技能 | 弹反帧位置(归一化时间)| 设计意图 | |------|---------------------|----------| | 攻击2(Attack2)| 0.05 ~ 0.18(前摇蓄力阶段)| 挥剑准备期可反击近身攻击 | | 攻击3(Attack3)| 0.05 ~ 0.12(大幅前摇阶段)| 终结技前摇明显,高风险高回报 | | (预留)特殊技能 | 由技能设计决定 | 护符/解锁技能可扩展此列表 | > 攻击1 节奏最快,不携带弹反帧。 ### 13.4 帧嵌入弹反的后续处理 弹反成功后走相同的 `ParrySystem.TriggerParrySuccess()` 流程(子弹时间 + Soul +33 + 反击窗口),不区分来源。唯一区别:帧嵌入弹反成功时玩家处于攻击动画中,反击窗口的 `ParryCounter` 攻击会从当前状态直接切换,而非从 `PlayerParryState` 切换。 --- ## 14. 弹反冲突解决(Parry Conflict Resolution) > 本节定义边界情况下弹反系统的决策优先级,确保不出现"吃弹反"或"双重消耗"问题。 ### 14.1 ParryResolveResult 枚举 ```csharp public enum ParryResolveResult { Parried, // 弹反成功,伤害被消除 Blocked_ByIFrame, // 玩家处于无敌帧,弹反未触发(伤害也被无敌帧吸收) Failed_OutsideWindow, // 弹反窗口关闭,伤害正常生效 Consumed, // 同帧首次弹反已消耗,后续相同帧的弹反触发失败 Rejected_NotParryable,// 攻击来源标记为不可弹反 } ``` ### 14.2 同帧多伤害处理 在同一 FixedUpdate 帧内,多个伤害实体可能同时命中玩家(如群体爆炸、多段敌人攻击)。规则如下: ``` 同帧伤害解决顺序(按命中顺序): 第 1 次伤害(可弹反)→ 触发弹反 → ParryResolveResult.Parried 第 2 次伤害(可弹反)→ 弹反已消耗 → ParryResolveResult.Consumed → 作为普通伤害处理 第 N 次伤害 → 同上,普通伤害 ``` 实现方式:在 `ParrySystem` 内部维护 `_parriedThisFrame` 布尔标志,于 `LateUpdate` 重置: ```csharp 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 | ```csharp 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 ```