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

953 lines
41 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.
# 03 · 玩家系统
> **命名空间** `BaseGames.Player` · `BaseGames.Player.States`
> **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md)
> **依赖** `BaseGames.Input` · `BaseGames.Combat` · `BaseGames.Parry` · Animancer Pro
---
## 目录
1. [系统总览](#1-系统总览)
2. [PlayerController — 协调器](#2-playercontroller--协调器)
3. [PlayerMovement — 物理移动](#3-playermovement--物理移动)
4. [PlayerStats — 属性系统](#4-playerstats--属性系统)
5. [PlayerCombat — 战斗管理](#5-playercombat--战斗管理)
6. [FormController — 形态系统](#6-formcontroller--形态系统)
7. [Animancer 双层动画系统](#7-animancer-双层动画系统)
8. [PlayerAnimationConfigSO](#8-playeranimationconfigso)
9. [FSM 状态体系](#9-fsm-状态体系)
10. [状态详细设计](#10-状态详细设计)
11. [能力解锁系统](#11-能力解锁系统)
12. [PlayerMovementConfigSO](#12-playermovementconfigso)
13. [编辑器友好设计](#13-编辑器友好设计)
14. [空中冲刺Aerial Dash](#14-空中冲刺aerialDash)
---
## 1. 系统总览
```
Player Prefab
├── PlayerController ← 协调器(无业务逻辑)
│ ├── PlayerMovement ← 物理移动Rigidbody2D
│ ├── PlayerStats ← 属性HP/灵力/魄元/灵泉/Geo/能力
│ ├── PlayerCombat ← HitBox 开关、连击链(读取 WeaponManager
│ ├── FormController ← 形态管理(天魂/地魂/命魂,见 §6
│ ├── WeaponManager ← 武器切换管理,形态联动(见 53_WeaponSystem
│ ├── SkillModifierRegistry← 技能修改器(见 21_SpellSystem §5
│ ├── SkillManager ← 魂技能/魄技能执行(见 21_SkillSystem
│ ├── SpringSystem ← 灵泉使用次数管理(见 §4.3
│ ├── ParrySystem ← 弹反状态(见 05_ParrySystem
│ └── AnimancerComponent ← Animancer 动画主入口
├── HurtBox ← 受击区域(路由 DamageInfo 给 PlayerController
├── HitBox_Ground ← 地面水平攻击判定
├── HitBox_Up ← 上劈攻击判定
├── HitBox_Down ← 下劈攻击判定Pogo
├── HitBox_Air ← 空中攻击判定(各形态独立闸题)
├── PlayerFeedback ← Feel MMF_Player 集合
└── SpriteRenderer
```
**设计要点**`PlayerController` 是**纯协调器**,只负责:
1. 初始化并持有子组件引用
2. 维护 Animancer FSM当前状态切换
3. 在受击时调用 `ForceState(HurtState)`
4. 在死亡时触发 `OnPlayerDied` 事件频道
所有具体行为(移动计算、攻击判定、弹反逻辑)委托给各子组件或各 State。
---
## 2. PlayerController — 协调器
### 持有引用Inspector 序列化)
| 字段 | 类型 | 说明 |
|------|------|------|
| `_inputReader` | `InputReaderSO` | 输入频道SO全局共享|
| `_movementConfig` | `PlayerMovementConfigSO` | 移动参数配置 |
| `_animConfig` | `PlayerAnimationConfigSO` | 动画配置 |
| `_statsConfig` | `PlayerStatsSO` | 初始属性配置 |
| `_formConfig` | `FormConfigSO` | 形态配置(天魂/地魂/命魂 SO 引用)|
| `_onPlayerDied` | `VoidEventChannelSO` | 死仪事件频道SO|
| `_onHPChanged` | `IntEventChannelSO` | HP 变化频道SO|
### 公开接口(供外部调用)
| 方法/属性 | 调用方 | 说明 |
|----------|--------|------|
| `ForceState(PlayerStateBase)` | HurtBox / ParrySystem | 强制切换状态(受击/死亡)|
| `TryTransitionState(PlayerStateBase)` | 各 State 内部 | 正常状态转换(带优先级检查)|
| `IsGrounded` | 各 State | 地面检测结果(只读)|
| `FacingDirection` | 各 State / PlayerCombat | 当前朝向 (+1 右 / -1 左)|
| `Rb` | PlayerMovement / 各 State | Rigidbody2D 引用 |
| `Animancer` | 各 State | AnimancerComponent 引用 |
---
## 3. PlayerMovement — 物理移动
`PlayerMovement` 封装所有 `Rigidbody2D` 操作,各 State 通过调用其方法间接控制物理:
### 移动接口
| 方法 | 调用时机 | 说明 |
|------|---------|------|
| `Move(float speedX)` | `RunState.OnStateFixedUpdate` | 设置水平速度 |
| `Jump(bool isVariable)` | `AirState.OnStateEnter` | 施加跳跃冲量 |
| `CutJump()` | `AirState`(松开跳跃键)| 减半垂直速度(可变跳跃高度)|
| `Dash(Vector2 direction)` | `DashState.OnStateEnter` | 施加冲刺速度 |
| `ApplyKnockback(DamageInfo)` | `HurtState.OnStateEnter` | 根据 DamageInfo 施加击退 |
| `ZeroVelocity()` | `IdleState.OnStateEnter` | 清零速度 |
| `ZeroHorizontalVelocity()` | `HurtState` 等 | 仅清零水平速度 |
### 地面检测
- 使用 `Physics2D.OverlapBox` 在脚部检测 Ground Layer
- 结果每帧更新,通过 `PlayerController.IsGrounded` 对外暴露
- 支持单向平台(`OneWayPlatform` Layer向下跳跃时临时忽略
### 墙壁检测
- 左右两侧各一个 `Physics2D.Raycast` 检测 Wall Layer
- `IsWallLeft` / `IsWallRight``AirState` 判断墙跳/墙滑
---
## 4. PlayerStats — 属性系统
`PlayerStats` 持有玩家当前**运行时属性**,初始值来自 `PlayerStatsSO`,通过事件频道对外广播变化:
### 属性表
| 属性 | 类型 | 初始值 | 说明 |
|------|------|--------|------|
| `CurrentHP` | `int` | `StatsSO.MaxHP` | 当前生命值 |
| `MaxHP` | `int` | 配置 SO | 最大生命值(心脏容器增加)|
| `CurrentSoulPower` | `int` | 0 | 当前灵力0-100近战命中积累用于魂技能 |
| `MaxSoulPower` | `int` | 100 | 灵力上限 |
| `CurrentSpiritPower` | `int` | `StatsSO.MaxSpiritPower` | 当前魄元0-100随时间自动回复用于魄技能 |
| `MaxSpiritPower` | `int` | 配置 SO | 魄元上限 |
| `CurrentSpringCharges` | `int` | `StatsSO.MaxSpringCharges` | 当前灵泉使用次数,击杀敌人积累点数达阙后增加 |
| `MaxSpringCharges` | `int` | 配置 SO | 灵泉次数上限(探索可升级)|
| `SpringKillPoints` | `int` | 0 | 当前击杀点数(达阙后清零)|
| `SpringKillThreshold` | `int` | 配置 SO | 增加 1 次灵泉所需击杀点数 |
| `CurrentGeo` | `int` | 0 | 当前货币 |
| `InvincibleTimer` | `float` | 0 | 无敌帧剩余时间 |
| `IsInvincible` | `bool` | false | 是否处于无敌状态 |
| `CurrentGeo` | `int` | 0 | 当前货币 |
| `InvincibleTimer` | `float` | 0 | 无敌帧剩余时间 |
| `IsInvincible` | `bool` | false | 是否处于无敌状态 |
### 方法(供外部调用)
| 方法 | 调用方 | 说明 |
|------|--------|------|
| `TakeDamage(int amount)` | `HurtBox``PlayerController` | 扣血,触发 `OnHPChanged` 频道 |
| `HealHP(int amount)` | `SpringState` | 回血,触发频道 |
| `AddSoulPower(int amount)` | 攻击命中调用 | 增加灵力(上限 MaxSoulPower|
| `ConsumeSoulPower(int amount)` | `SkillManager`(魂技能) | 消耗灵力,返回 bool 表示是否成功 |
| `AddSpiritPower(int amount)` | 自动恢复计时器 | 增加魄元 |
| `ConsumeSpiritPower(int amount)` | `SkillManager`(魄技能) | 消耗魄元,返回 bool |
| `AddKillPoints(int points)` | 敌人死亡调用 | 增加击杀点数,自动检查是否增加灵泉次数 |
| `UseSpring()` | `SpringState` | 消耗 1 次灵泉,返回 bool |
| `RestoreSpringCharges()` | `SavePoint` 交互 | 恢复灵泉次数至上限 |
| `AddGeo(int amount)` | `Collectible.OnPickup` | 增加 Geo触发频道 |
| `BeginInvincibility(float duration)` | `HurtState` / `DashState` | 开始无敌帧(带计时)|
| `HasAbility(AbilityType)` | 各 State | 检查能力是否解锁 |
| `UnlockAbility(AbilityType)` | `AbilityUnlock` 世界物件 | 解锁能力 |
### Soul 获取规则
> **灵力**:近战技能驱动资源
| 来源 | 灵力增量 |
|------|----------|
| 攻击命中普通敌人 | +10 |
| 攻击命中 Boss | +5 |
| 弹反成功 | +20 |
| 受到伤害 | +0 |
> **魄元**:自动回复资源
| 回复时机 | 魄元回复量 |
|---------|----------|
| 每秒自动回复 | +`SpiritRegenRate`(配置 SO|
| 使用魄技能 | -技能消耗 |
> **灵泉使用次数**:击杀驱动资源
| 来源 | 点数 |
|------|------|
| 击杀景通敌人 | +1 点 |
| 击杀精英怪 | +3 点 |
| 击杀 Boss | +5 点 |
点数积累至 `SpringKillThreshold` 后自动+1次灵泉使用次数点数清零。
### 三资源生命周期总览
```
┌──────────────────────────────────────────────────────────────────┐
│ 灵力SoulPower 资源性质:近战积累,不自动恢复 │
├──────────────────────────────────────────────────────────────────┤
│ 增加来源: │
│ 近战命中普通敌人 +10 (PlayerCombat → OnHitBoxTriggered) │
│ 近战命中 Boss +5 (PlayerCombat → OnHitBoxTriggered) │
│ 弹反成功 +33 (ParrySystem → OnParrySuccess) │
│ 消耗: │
│ 魂技能施放 −技能 baseCost (SkillManager.TryCastSoul) │
│ 重置为 0: │
│ 玩家死亡 (HurtState Death 进入时) │
│ 存档点读取时按 SaveData 恢复 │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ 魄元SpiritPower 资源性质:自动恢复,持续消耗 │
├──────────────────────────────────────────────────────────────────┤
│ 增加来源: │
│ 时间自动恢复 +SpiritRegenRate/s (PlayerStats.Update) │
│ 消耗: │
│ 魄技能 1/2 施放 −技能 baseCost (SkillManager.TryCastSpirit) │
│ 重置为最大: │
│ 死亡复活(返回最近存档点) │
│ 存档点交互(同时恢复灵泉) │
│ 不受影响: │
│ 受伤不消耗魄元 │
└──────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────┐
│ 灵泉次数SpringCharges 资源性质:击杀驱动,有限次数 │
├──────────────────────────────────────────────────────────────────┤
│ 增加来源(击杀点数): │
│ 击杀普通敌人 +1 pt → 积累到阈值 → +1次 (PlayerStats) │
│ 击杀精英怪 +3 pt → 积累到阈值 → +1次 │
│ 击杀 Boss +5 pt → 积累到阈值 → +1次 │
│ 消耗: │
│ 使用灵泉(按键) 1次 (SpringState → PlayerStats.UseSpring) │
│ 恢复为最大: │
│ 与存档点SavePoint交互 │
│ 死亡复活(不恢复,死亡惩罚的一部分) │
│ 上限升级: │
│ 拾取"灵泉上限容器"(类 Hollow Knight 心脏容器) │
│ → PlayerStats.MaxSpringCharges++ │
└──────────────────────────────────────────────────────────────────┘
三资源联动规则:
灵力满格100→ 魂技能可用SkillManager 检查 CurrentSoulPower ≥ baseCost
魄元满格100→ 魄技能可多次连用(无额外奖励,数值设计时注意不要让满魄元过强)
灵泉耗尽0 → UseSpring 输入被 PlayerStats.UseSpring() 拒绝(返回 false
SpringState 不进入
```
---
## 5. PlayerCombat — 战斗管理
`PlayerCombat` 管理攻击**连击题**和 **HitBox 开关**,并考虑**当前形态**的攻击动画适配:
### 连击状态机
```
ComboState 枚举:
None → Attack1 → Attack2 → Attack3 → None (重置)
↑____连击窗口内输入_____| (各形态均 3 连击,岗開/动作组各形态独立)
```
| 连击段 | 触发方式 | HitBox | 伤害倍率 | 说明 |
|--------|---------|--------|---------|------|
| `Attack1` | 第一次攻击输入 | `HitBox_Ground` | ×1 | 水平支行(各形态动画不同)|
| `Attack2` | 连击窗口内再按攻击 | `HitBox_Ground` | ×1 | 连击第 2 击 |
| `Attack3` | 连击窗口内再按攻击 | `HitBox_Ground` | ×2 | 第 3 击(重击)|
| `AirAttack` | 空中攻击 | `HitBox_Air` | ×1 | 空中水平支行 |
| `UpAttack` | MoveY > 0.5 时攻击 | `HitBox_Up` | ×1 | 上劈,角色微小下移 |
| `DownAttack` | 空中 + MoveY < -0.5 | `HitBox_Down` | ×1 | 下劈命中弹起Pogo|
| `ParryCounter` | 弹反成功后攻击 | `HitBox_Ground` | ×3 | 不可被弹反 |
### 形态攻击适配
各形态的连击段数和功能一致3 连击 + 空中/上/下但动作组、HitBox 尺寸、伤害数值完全不同。`PlayerCombat` 订阅 `WeaponManager.OnWeaponChanged`,形态切换时自动刷新所有攻击数据:
```csharp
// PlayerCombat 监听武器切换事件(形态切换 → WeaponManager → PlayerCombat
_weaponManager.OnWeaponChanged += RefreshWeaponData;
// RefreshWeaponData刷新动画片段 + DamageSource + HitBox 尺寸 + ResetCombo()
// 详见 53_WeaponSystem §5
```
### 连击窗口时长
| 参数 | 值 | 位置 |
|------|-----|------|
| `ComboWindowDuration` | 0.5s | `PlayerCombatConfigSO` |
| `ComboResetDelay` | 0.3sAttack3 后)| `PlayerCombatConfigSO` |
连击窗口由 `AttackState.OnAttackAnimationComplete` 事件触发开启,超时后 `ComboState` 回到 `None`
---
## 6. FormController — 形态系统
`FormController` 管理三种形态的切换、当前形态的技能居子、以及形态切换过渡。
### FormSO — 形态数据
```csharp
[CreateAssetMenu(menuName = "Player/Form")]
public class FormSO : ScriptableObject
{
[Header("形态标识")]
public string formId; // 存档 + 识别 ID如 "SkyForm"(替代枚举,彻底数据驱动)
public string displayName; // "天魂"
public Sprite formIcon; // HUD 图标
public Color formAccentColor; // HUD/动画特效色调
[Header("默认武器")]
/// <summary>该形态绑定的默认武器 SO替代旧 FormAttackConfig 内联结构体)</summary>
public WeaponSO defaultWeapon; // 连击动画 + DamageSources + HitBox 配置,见 53_WeaponSystem
[Header("技能配置")]
public FormSkillSO soulSkill; // 魂技能(消耗灵力)
public FormSkillSO spiritSkill1; // 魄技能 1消耗魄元
public FormSkillSO spiritSkill2; // 魄技能 2消耗魄元
}
// 注FormType 枚举已移除。使用 formId 字符串识别形态。
// 新增形态只需:① 创建新 FormSO 资产 ② 加入 FormConfigSO.forms[] ③ 配置对应输入槽
// FormAttackConfig 已废弃——其所有字段已迁移至 WeaponSO见 53_WeaponSystem §2
```
### FormController 组件
```csharp
namespace BaseGames.Player
{
/// <summary>
/// 形态控制器。通过 FormConfigSO 持有所有可用形态,
/// 不硬编码具体形态数量——新增形态只需创建新 FormSO 并加入 FormConfigSO.forms[]。
/// </summary>
public class FormController : MonoBehaviour
{
[SerializeField] FormConfigSO _config; // 持有所有可用形态的配置 SO
[SerializeField] InputReaderSO _input;
[Header("事件频道—发布")]
[SerializeField] FormEventChannelSO _onFormChanged; // HUD、动画层监听
public FormSO CurrentForm { get; private set; }
public FormSO[] AllForms => _config.forms; // WeaponManager 全形态枚举时使用
public event Action OnFormChanged;
void Awake()
{
// 默认激活第一个形态Index 0 = 天魂)
if (_config.forms.Length > 0)
CurrentForm = _config.forms[0];
}
void OnEnable()
{
// 形态切换按固定槽位 Index 映射输入事件:
// Slot 0 (SkyForm) → SwitchSkyFormEvent (键 1 / D-Pad Left)
// Slot 1 (EarthForm) → SwitchEarthFormEvent (键 2 / D-Pad Down)
// Slot 2 (DeathForm) → SwitchDeathFormEvent (键 3 / D-Pad Right)
// 若未来扩展第 4 形态,只需在 InputReaderSO 中增加 SwitchForm4Event 并绑定此处
_input.SwitchSkyFormEvent += () => SwitchToIndex(0);
_input.SwitchEarthFormEvent += () => SwitchToIndex(1);
_input.SwitchDeathFormEvent += () => SwitchToIndex(2);
}
void OnDisable()
{
_input.SwitchSkyFormEvent -= () => SwitchToIndex(0);
_input.SwitchEarthFormEvent -= () => SwitchToIndex(1);
_input.SwitchDeathFormEvent -= () => SwitchToIndex(2);
}
void SwitchToIndex(int index)
{
if (index < 0 || index >= _config.forms.Length) return;
SwitchForm(_config.forms[index]);
}
void SwitchForm(FormSO newForm)
{
if (newForm == null || newForm == CurrentForm) return;
CurrentForm = newForm;
_onFormChanged.Raise(newForm);
OnFormChanged?.Invoke();
}
}
}
```
### FormConfigSO — 形态配置容器
```csharp
/// <summary>
/// 持有全部可用形态的有序列表。
/// Index 对应输入槽0 = SkyForm, 1 = EarthForm, 2 = DeathForm …)。
/// 新增形态时:① 创建 FormSO ② 加入此数组尾部 ③ 配置新输入槽,无需改动 FormController。
/// </summary>
[CreateAssetMenu(menuName = "Player/FormConfig")]
public class FormConfigSO : ScriptableObject
{
public FormSO[] forms; // 有序列表Index 即输入槽编号
}
```
**资产存放**`Assets/ScriptableObjects/Player/FormConfig.asset`
**检验规范**Custom Inspector 在 `forms.Length == 0` 时显示黄色警告框。
### FormEventChannelSO
```csharp
[CreateAssetMenu(menuName = "Events/FormEventChannel")]
public class FormEventChannelSO : ScriptableObject
{
public event Action<FormSO> OnEventRaised;
public void Raise(FormSO form) => OnEventRaised?.Invoke(form);
}
```
### HUD 形态切换预设效果
形态切换本身**无前摇**(不中断当前动作),下一次攻击/技能输入时输出新形态动作。如需前摇,在 `FormSO.SwitchInAnimation`(可选)中配置。
---
```
| 连击段 | 触发方式 | HitBox | 伤害倍率 | 说明 |
|--------|---------|--------|---------|------|
| `Attack1` | 第一次攻击输入 | `HitBox_Ground` | ×1 | 横向挥击 |
| `Attack2` | 连击窗口内再按攻击 | `HitBox_Ground` | ×1 | 斜上挥击 |
| `Attack3` | 连击窗口内再按攻击 | `HitBox_Ground` | ×2 | 下砸重击 |
| `AirAttack` | 空中攻击 | `HitBox_Air` | ×1 | 下斩 |
| `UpAttack` | MoveY > 0.5 时攻击 | `HitBox_Up` | ×1 | 上挑 |
| `ParryCounter` | 弹反成功后攻击 | `HitBox_Ground` | ×3 | 不可被弹反 |
### 连击窗口时长
| 参数 | 值 | 位置 |
|------|-----|------|
| `ComboWindowDuration` | 0.5s | `PlayerCombatConfigSO` |
| `ComboResetDelay` | 0.3sAttack3 后)| `PlayerCombatConfigSO` |
连击窗口由 `AttackState.OnAttackAnimationComplete` 事件触发开启,超时后 `ComboState` 回到 `None`。
---
## 7. Animancer 双层动画系统
```
AnimancerComponent
├── Layer[0] — 全身动画
│ 权重: 1.0AvatarMask: 全身
│ 播放: Idle / Run / Fall / Land / Hurt / Death / Dash / UseSpring / Interact / WallGrab
└── Layer[1] — 上半身覆盖(攻击/弹反/技能)
权重: 0.0 → 1.0(攻击/技能时),结束后淡出回 0.0
AvatarMask: UpperBody脊椎以上所有骨骼
IsAdditive: false覆盖模式
播放: Attack1/2/3 / AirAttack / UpAttack / DownAttack / SoulSkill / SpiritSkill1 / SpiritSkill2
/ ParryPose / ParrySuccess / ParryCounter
```
**双层动画效果**
| 玩家状态 | Layer[0] | Layer[1] | 视觉效果 |
|---------|---------|---------|---------|
| 奔跑 | Run | 无(权重=0| 仅奔跑 |
| 奔跑时攻击 | Run继续| Attack1 | 腿部奔跑 + 上身攻击 |
| 空中攻击 | Fall | AirAttack | 下落姿态 + 下斩动作 |
| 弹反 | ParryPose全身| 无 | 弹反姿态全身Layer[1]关闭)|
---
## 7. PlayerAnimationConfigSO
`PlayerAnimationConfigSO` 存放所有 `ClipTransition`Inspector 中拖入对应 AnimationClip
### Layer[0] 全身动画
| 字段名 | Fade 时长 | 说明 |
|--------|----------|------|
| `Idle` | 0.2s | 待机循环 |
| `Run` | 0.1s | 奔跑循环 |
| `JumpRise` | 0.05s | 跳跃上升 |
| `JumpFall` | 0.15s | 下落 |
| `Land` | 0.05s | 落地(接 Idle/Run|
| `Dash` | 0.05s | 冲刺 |
| `Hurt` | 0.05s | 受击 |
| `Death` | 0.1s | 死亡(不循环)|
| `Interact` | 0.1s | 交互动作 |
| `WallSlide` | 0.1s | 墙壁滑行(解锁后)|
| `Swim` | 0.2s | 游泳循环P1解锁后|
### Layer[1] 上半身动画
| 字段名 | Fade 时长 | AnimationEvent | 说明 |
|--------|----------|---------------|------|
| `Attack1` | 0.05s | `EnableHitBox` Frame 3, `DisableHitBox` Frame 8 | 第1击 |
| `Attack2` | 0.05s | `EnableHitBox` Frame 3, `DisableHitBox` Frame 7 | 第2击 |
| `Attack3` | 0.05s | `EnableHitBox` Frame 2, `DisableHitBox` Frame 10 | 第3击|
| `AirAttack` | 0.05s | `EnableHitBox` Frame 2, `DisableHitBox` Frame 8 | 空中下斩 |
| `UpAttack` | 0.05s | `EnableHitBox` Frame 3, `DisableHitBox` Frame 7 | 上挑 |
| `ParryPose` | 0.08s | — | 弹反等待姿态Layer[0] |
| `ParrySuccess` | 0.05s | — | 弹反成功Layer[0]|
| `ParryCounter` | 0.05s | `EnableHitBox` Frame 2 | 弹反反击 |
---
## 9. FSM 状态体系
使用 `Animancer.FSM.StateMachine<PlayerStateBase>` 作为 FSM 骨架:
### 状态优先级(数字越高越优先抢占)
| 优先级 | 状态 | 说明 |
|--------|------|------|
| 100 | `HurtState` (Death) | 死亡,不可被打断 |
| 90 | `HurtState` (Alive) | 受击,只能被死亡打断 |
| 80 | `DashState` | 冲刺无敌,只能被受伤死亡打断 |
| 75 | `SoulSkillState` | 魂技能前摇,可被冲刺/受伤打断 |
| 72 | `SpiritSkillState` | 魄技能前摇,可被冲刺/受伤打断 |
| 70 | `ParryState` | 弹反,可被受伤打断 |
| 60 | `AttackState` | 攻击Layer[1]),可被弹反/受伤打断 |
| 55 | `SpringState` | 用灵泉(前摇可打断)|
| 40 | `InteractState` | 交互,可被大多数行为打断 |
| 35 | `WallGrabState` | 抓墙悬挂,可被冲刺/技能打断 |
| 30 | `AirState` | 空中,可被攻击/弹反/冲刺打断 |
| 20 | `RunState` | 奔跑 |
| 10 | `SwimState` | 游泳P1|
| 0 | `IdleState` | 待机(最低)|
### 状态转换规则
```
任意状态
├─ 受击HP > 0 → HurtState (Alive) [强制 ForceState]
├─ 受击HP = 0 → HurtState (Death) [强制 ForceState]
└─ (具体见下方状态详细设计)
IdleState
├─ 移动输入 → RunState
├─ 跳跃输入 → AirState
├─ 攻击输入 → AttackState (Layer 1)
├─ 弹反输入 → ParryState
├─ 冲刺输入 → DashState
├─ SoulSkill + 灵力足够 → SoulSkillState
├─ SpiritSkill1/2 + 魄元足够 → SpiritSkillState
├─ UseSpring + 灵泉次数≥1 → SpringState
└─ 交互输入 + 有互动目标 → InteractState
RunState
├─ 无移动输入 → IdleState
├─ 跳跃输入 → AirState
├─ 攻击输入 → AttackState (Layer 1)(保持 RunStateLayer[1] 叠加)
├─ 弹反输入 → ParryState
├─ 冲刺输入 → DashState
├─ SoulSkill/SpiritSkill → 对应技能 State
└─ UseSpring → SpringState
AirState统一处理跳跃/双跳/下落)
├─ 落地 → IdleState 或 RunState视水平输入
├─ 跳跃输入(双跳可用)→ 重置 AirState在 AirState 内部处理,不切换状态)
├─ 贴墙 + 朝墙输入 → WallGrabState见 26_WallMechanicsSystem
├─ 攻击输入 → AttackState (Layer 1)
├─ 弹反输入 → ParryState
└─ 冲刺输入 + 冲刺可用 → DashState
WallGrabState抓墙悬挂见 26_WallMechanicsSystem
├─ 跳跃输入 → 蹬墙跳(背墙/对墙,内部判断)→ AirState
├─ 按反方向键或落地 → AirState / IdleState
├─ 冲刺输入 → DashState
└─ 技能输入 → 对应技能 State
DashState
├─ 冲刺时长结束 → AirState 或 IdleState/RunState
└─ (冲刺无敌,忽略受击)
AttackStateLayer[1] 叠加Layer[0] 保持原状态)
├─ 动画结束 → Layer[1] 淡出恢复原状态Idle/Run/Air
├─ 连击窗口内攻击输入 → 切换至下一连击段
└─ 弹反输入 → ParryState打断攻击Layer[1] 立即淡出)
SoulSkillState / SpiritSkillStateLayer[1] 叠加)
├─ 前摇动画结束 → 执行技能效果
└─ 冲刺/受伤打断 → DashState / HurtState技能费用已扣除
SpringState
├─ 前摇完成 → 立即回血,进入后摇
├─ 前摇被打断 → IdleState灵泉次数已扣不返还
└─ 后摇结束 → IdleState/RunState
ParryState
├─ 弹反成功 → 播放 ParrySuccess开启反击窗口
│ ↓ 反击窗口内攻击 → AttackState (ParryCounter)
│ ↓ 反击窗口超时 → IdleState/RunState
└─ 弹反超时 → IdleState/RunState
HurtState
└─ 硬直时长结束 → IdleState
```
---
## 10. 状态详细设计
### PlayerAirState — 空中状态
AirState 内部管理多种空中子行为(不拆分为多个 State避免频繁切换
| 空中行为 | 触发条件 | 说明 |
|---------|---------|------|
| **初始跳跃** | 地面按下跳跃 / Coyote Time | 施加 `JumpForce` 冲量 |
| **可变跳跃高度** | 松开跳跃键时速度仍向上 | `CutJump()`:垂直速度截断 |
| **双跳** | 空中按下跳跃 + `HasDoubleJump` + 未使用过 | 重置垂直速度 + 施加 `DoubleJumpForce` |
| **跳跃顶点缓降** | 上升速度接近 0 | 重力系数降低,轻盈漂浮感 |
| **下垂加速** | 垂直速度 < 0 | 额外重力 × `FallMultiplier` |
| **抑制单向平台** | 下方输入 + 跳跃键 | 临时忽略 `OneWayPlatform` Layer |
### PlayerDashState — 冲刺状态
| 参数 | 值 | 位置 |
|------|-----|------|
| `DashSpeed` | 18 units/s | `PlayerMovementConfigSO` |
| `DashDuration` | 0.18s | `PlayerMovementConfigSO` |
| `DashCooldown` | 0.5s | `PlayerMovementConfigSO` |
| `InvincibleDuringDash` | true | 冲刺全程无敌帧 |
| `CanDashInAir` | falseP0 / true解锁后| `PlayerStats.HasAerialDash` |
### PlayerHurtState — 受击/死亡状态
```
OnStateEnter(DamageInfo info)
1. 播放 Hurt 动画Layer[0]
2. PlayerMovement.ApplyKnockback(info)
3. PlayerStats.BeginInvincibility(IFrameDuration)
4. PlayerFeedback.OnTakeHit()
若 HP = 0:
5. 播放 Death 动画
6. 禁用所有输入InputReader.DisableAllInput
7. 延迟 1.2s 后触发 OnPlayerDied 事件频道
```
---
## 11. 能力解锁系统
`AbilityType` 枚举定义所有可解锁能力,`PlayerStats.HasAbility(type)` 在各 State 中查询:
| 能力枚举值 | 触发状态改变 | 说明 |
|-----------|-----------|------|
| `DoubleJump` | `AirState`:允许双跳 | 默认锁定 |
| `WallGrab` | `WallGrabState`:允许抓墙 | 默认**解锁**(基础能力)|
| `AerialDash` | `DashState`:允许空中冲刺 | 默认锁定 |
| `InvincibleDash` | `DashState`:冲刺全程无敌 | 默认锁定(升级冲刺)|
| `Swim` | 接触水体不死亡,进入 `SwimState` | 默认锁定P1|
| `Parry` | `ParryState`:弹反可用 | 默认**解锁** |
能力解锁通过 `AbilityUnlock` 世界物件(见 `08_WorldSystem`)触发,调用 `PlayerStats.UnlockAbility(type)`,同步到 `SaveData`。
## 12. PlayerMovementConfigSO
所有移动相关数值,策划友好配置:
| 参数 | 类型 | 推荐值 | 说明 |
|------|------|--------|------|
| `WalkSpeed` | `float` | 6 | 地面行走速度units/s|
| `RunSpeed` | `float` | 9 | 奔跑速度(触发加速后)|
| `JumpForce` | `float` | 16 | 跳跃冲量 |
| `DoubleJumpForce` | `float` | 14 | 双跳冲量 |
| `WallJumpAwayHForce` | `float` | 10 | 蓬墙跳—背墙跳水平分量 |
| `WallJumpTowardHForce` | `float` | 3 | 蓬墙跳—对墙跳水平分量 |
| `WallJumpAwayHForce` | `float` | 10 | 蓬墙跳—背墙跳水平分量 |
| `WallJumpTowardHForce` | `float` | 3 | 蓬墙跳—对墙跳水平分量 |
| `WallJumpVerticalForce` | `float` | 15 | 蓬墙跳垂直分量(两种共用)|
| `WallGrabSlideSpeed` | `float` | 2 | 抓墙超出高度记忆值时强制下滑速度 |记忆值时强制下滑速度 |
| `DashSpeed` | `float` | 18 | 冲刺速度 |
| `DashDuration` | `float` | 0.18 | 冲刺持续时间 |
| `DashCooldown` | `float` | 0.5 | 冲刺冷却 |
| `FallMultiplier` | `float` | 2.5 | 下坠额外重力倍率 |
| `LowJumpMultiplier` | `float` | 2.0 | 短跳额外重力倍率 |
| `CoyoteTimeDuration` | `float` | 0.12 | 土狼时间 |
| `JumpBufferDuration` | `float` | 0.15 | 跳跃缓冲窗口 |
| `GroundCheckDistance` | `float` | 0.05 | 地面检测距离 |
| `InvincibleAfterHit` | `float` | 1.2 | 受击无敌帧时长 |
---
## 12. 编辑器友好设计
### PlayerController 自定义 Inspector
Play Mode 下显示完整运行时状态:
```
┌─ PlayerController ─────────────────────────────┐
│ Current State : WallGrabState │
│ 形态 : 天魂 [Sky] │
│ HP : ████████████░░ 8 / 10 │
│ 灵力 : ███████░░░░░░░ 70 / 100 │
│ 魄元 : ████████████░░ 85 / 100 │
│ 灵泉 : 3 / 5 次 │
│ Geo : 340 │
│ IsGrounded : ✓ │
│ FacingDir : → (+1) │
│ Invincible : ░░░░░░░░░░ (0.00s) │
│ ─────────────────────────────────────────────── │
│ Abilities: [DoubleJump ✓] [WallGrab ✓] │
│ [AerialDash ✗] [InvDash ✗] │
│ ─────────────────────────────────────────────── │
│ [强制 Idle] [强制 Run] [强制 Air] [Hurt 2] │
│ [AerialDash ✗] [InvDash ✗] │
│ ─────────────────────────────────────────────── │
│ [强制 Idle] [强制 Run] [强制 Air] [Hurt 2] │
└───────────────────────────────────────────────┘
```
### Scene 视图 Gizmos
- **地面检测区域**:绘制绿色/红色矩形(绿=接触地面,红=空中)
- **HitBox 范围**:绘制各方向 HitBox 橙色矩形(仅激活时显示)
- **墙体检测 Raycast**:绘制左右两侧蓝色射线
---
## 14. 空中冲刺Aerial Dash
> **依赖能力**`AbilityType.AerialDash`(默认锁定,探索解锁)
空中冲刺是《丝之歌》的核心移动机制:玩家在空中获得一次额外冲刺机会,大幅扩展垂直与横向移动自由度。
### 13.1 设计参数
| 参数 | 值 | 位置 |
|------|-----|------|
| `AerialDashSpeed` | 15 units/s地面冲刺的 83%| `PlayerMovementConfigSO` |
| `AerialDashDuration` | 0.20s | `PlayerMovementConfigSO` |
| `AerialDashCooldown` | 独立计次(见下方)| — |
| `InvincibleDuringAerialDash` | true与地面冲刺相同| — |
| `AerialDashCharges` | 1固定护符可扩展至 2| `PlayerStats` |
| `AerialDashDirection` | 8 方向(左摇杆/键盘方向)| `PlayerMovement` |
### 13.2 充能重置条件
空中冲刺充能Charge在以下情况**重置为最大值**
| 条件 | 说明 |
|------|------|
| 落地 | 触地瞬间立即恢复 |
| 墙跳触发后 | 墙跳给予新的空中冲刺机会(允许垂直攀升)|
| 命中敌人HitBox 触发有效命中)| 《丝之歌》核心机制:攻击命中即恢复(鼓励进攻性空中移动)|
| 弹反成功 | 弹反也视为"命中",给予充能恢复 |
```csharp
// PlayerStats 追踪充能
int _aerialDashChargesRemaining;
public void OnAerialDashUsed() => _aerialDashChargesRemaining--;
public void ResetAerialDashCharges() => _aerialDashChargesRemaining = MaxAerialDashCharges;
public bool HasAerialDashCharge => _aerialDashChargesRemaining > 0
&& HasAbility(AbilityType.AerialDash);
```
```csharp
// PlayerCombat有效命中时通知充能重置
void OnHitBoxTriggered(HurtInfo hurt)
{
if (hurt.hitCount > 0) // 有效命中(不是空挥)
_stats.ResetAerialDashCharges();
}
```
### 13.3 DashState 扩展(空中冲刺分支)
`DashState` 内部区分地面冲刺和空中冲刺:
```csharp
public class DashState : PlayerStateBase
{
bool _isAerialDash;
public override void OnStateEnter()
{
var isGrounded = _movement.IsGrounded;
_isAerialDash = !isGrounded;
float speed = _isAerialDash ? _config.AerialDashSpeed : _config.DashSpeed;
float duration = _isAerialDash ? _config.AerialDashDuration : _config.DashDuration;
// 方向:空中冲刺支持 8 方向;地面冲刺仅水平
Vector2 dir = _isAerialDash
? GetAerialDashDirection()
: new Vector2(_movement.FacingDir, 0);
_movement.BeginDash(dir, speed, duration);
_stats.BeginInvincibility(duration);
if (_isAerialDash)
_stats.OnAerialDashUsed();
PlayDashAnimation(_isAerialDash, dir);
}
Vector2 GetAerialDashDirection()
{
var input = _inputReader.MoveInput;
// 若无输入,默认朝向当前面朝方向(水平)
return input.sqrMagnitude > 0.01f
? GetClosest8Direction(input)
: new Vector2(_movement.FacingDir, 0);
}
// 将任意输入方向量化为 8 个标准方向之一
static Vector2 GetClosest8Direction(Vector2 input)
{
float angle = Mathf.Atan2(input.y, input.x) * Mathf.Rad2Deg;
float snapped = Mathf.Round(angle / 45f) * 45f;
float rad = snapped * Mathf.Deg2Rad;
return new Vector2(Mathf.Cos(rad), Mathf.Sin(rad)).normalized;
}
}
```
### 13.4 可进入 DashState 的新条件
在 FSM 转换中,`AirState` 现增加额外判断:
```
AirState
...(原有转换)
└─ 冲刺输入 + HasAerialDashCharge → DashState空中冲刺
```
`CanDashInAir``PlayerStats.HasAerialDashCharge` 替代,原 `CanDashInAir` bool 参数废弃。
### 13.5 视觉反馈
| 阶段 | 效果 | 组件 |
|------|------|------|
| 空中冲刺激活 | 冲刺拖影颜色变为蓝紫色(区别于地面冲刺白色)| `TrailRenderer` 参数切换 |
| 充能耗尽 | HitBox 周围短暂显示灰色粒子(无充能警示)| `OnAerialDashUsed` 调用 Feel FB |
| 充能恢复(命中)| 玩家周围一圈蓝色闪光 | `OnHitBoxTriggered` 调用 Feel FB |
| 落地恢复 | 轻微光环扩散 | `OnLand` 事件触发(可选,避免频繁播放)|
### 13.6 参数更新PlayerMovementConfigSO
在 §11 参数表末尾新增:
| 参数 | 类型 | 推荐值 | 说明 |
|------|------|--------|------|
| `AerialDashSpeed` | `float` | 15 | 空中冲刺速度 |
| `AerialDashDuration` | `float` | 0.20 | 空中冲刺持续时间 |
| `MaxAerialDashCharges` | `int` | 1 | 默认最大充能(护符扩展至 2|
---
## 14. 下冲Plunge / Down Dash
> **依赖能力**:无(基础能力)
> **触发条件**:空中 + 向下输入 + 冲刺键
下冲是垂直向下的特殊冲刺,落地时产生冲击波效果,可对地面敌人造成 AoE 伤害。
### 14.1 设计参数
| 参数 | 值 | 说明 |
|------|-----|------|
| `PlungeSpeed` | 28 units/s | 下冲速度(比空中冲刺更快)|
| `PlungeImpactRadius` | 2.0 units | 落地冲击波范围 |
| `PlungeImpactDamage` | `BaseDamage × 2.0` | 冲击波伤害 |
| `PlungeStagger` | 强硬直0.8s| 范围内所有敌人 |
| `PlungeInvincible` | true下冲全程| 无敌帧 |
| `PlungeConsumesDash` | true | 消耗冲刺充能(与空中冲刺互斥,同一次空中只能选一个)|
### 14.2 触发条件与优先级
```
AirState 内部判断(优先级从高到低):
1. 向下输入 + 冲刺键 + 非下落过快 → PlungeState下冲
2. 任意方向输入 + 冲刺键 + HasAerialDashCharge → DashState空中冲刺
3. 冲刺键(无方向输入) + HasAerialDashCharge → DashState水平空中冲刺
```
### 14.3 PlungeState 实现
```csharp
public class PlungeState : PlayerStateBase
{
bool _hasLanded;
public override void OnStateEnter()
{
_hasLanded = false;
// 取消垂直速度,施加极大向下速度
_movement.SetVelocity(new Vector2(_movement.Velocity.x * 0.2f, -_config.PlungeSpeed));
_stats.BeginInvincibility(_config.PlungeDuration);
_stats.OnAerialDashUsed(); // 消耗充能(共用空中冲刺充能)
PlayAnimation("Plunge_Descend");
}
public override void OnStateFixedUpdate()
{
if (_hasLanded) return;
if (_movement.IsGrounded)
TriggerImpact();
}
void TriggerImpact()
{
_hasLanded = true;
PlayAnimation("Plunge_Impact");
// 冲击波 AoE
var hits = Physics2D.OverlapCircleAll(
_movement.transform.position,
_config.PlungeImpactRadius,
_enemyLayer);
foreach (var hit in hits)
{
if (hit.TryGetComponent<IDamageable>(out var target))
target.TakeDamage(new DamageInfo
{
damage = _combat.CalculateDamage(_config.PlungeImpactDamage),
knockback = (hit.transform.position - _movement.transform.position).normalized * 5f,
isParryable = false, // 落地冲击不可弹反
staggerDuration = 0.8f,
});
}
// 演出
_feedback.PlayFeedbacks("PlungeImpact");
// 轻微回弹(保持手感)
_movement.SetVelocity(new Vector2(_movement.Velocity.x, 4f));
// 延迟后转换到 Idle/Run
StartCoroutine(ExitAfterDelay(0.15f));
}
IEnumerator ExitAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
_fsm.TrySetState(_movement.IsGrounded
? (PlayerStateBase)_idle
: _air);
}
}
```
### 14.4 视觉反馈
| 阶段 | 效果 |
|------|------|
| 下冲开始 | 玩家 Sprite 变形(向上拉伸 × 1.3Feel Squash |
| 下冲中 | 速度线粒子(向上冒出)|
| 落地冲击 | 地面冲击圈(白色扩散环)+ 屏幕震动Feel Shake Camera+ 尘土粒子 |
| 冲击范围可视(仅 Debug| Gizmos 绘制绿色圆圈 |