19 KiB
05 · 弹反系统
命名空间
BaseGames.Parry
所属文档集 ← 返回索引 · 总览
依赖BaseGames.Player·BaseGames.Combat·BaseGames.Feedback· FeelMMTimeManager
目录
- 设计目标与核心体验
- 弹反状态机
- 时间流完整流水线
- 弹反判定窗口
- 子弹时间(Bullet Time)
- 反击窗口(Counter Window)
- 敌人 Stagger
- Soul 增益
- ParryConfigSO — 配置资产
- 事件频道
- 完美弹反(P1)
- 编辑器友好设计
- 帧嵌入式弹反窗口
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
弹反成功条件(同时满足):
ParrySystem.State == Active(在弹反窗口内)DamageInfo.Flags.HasFlag(CanBeParried)为 true- 玩家当前未处于无敌帧(无敌帧中收不到 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 双接口设计
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 携带弹反帧的技能列表
| 技能 | 弹反帧位置(归一化时间) | 设计意图 |
|---|---|---|
| 攻击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 枚举
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