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

504 lines
19 KiB
Markdown
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.
# 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 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`+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 进入更长时间的 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 双接口设计
```csharp
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 枚举
```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
```