# 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.3s(Attack3 后)| `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("默认武器")]
/// 该形态绑定的默认武器 SO(替代旧 FormAttackConfig 内联结构体)
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
{
///
/// 形态控制器。通过 FormConfigSO 持有所有可用形态,
/// 不硬编码具体形态数量——新增形态只需创建新 FormSO 并加入 FormConfigSO.forms[]。
///
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
///
/// 持有全部可用形态的有序列表。
/// Index 对应输入槽(0 = SkyForm, 1 = EarthForm, 2 = DeathForm …)。
/// 新增形态时:① 创建 FormSO ② 加入此数组尾部 ③ 配置新输入槽,无需改动 FormController。
///
[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 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.3s(Attack3 后)| `PlayerCombatConfigSO` |
连击窗口由 `AttackState.OnAttackAnimationComplete` 事件触发开启,超时后 `ComboState` 回到 `None`。
---
## 7. Animancer 双层动画系统
```
AnimancerComponent
├── Layer[0] — 全身动画
│ 权重: 1.0,AvatarMask: 全身
│ 播放: 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` 作为 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)(保持 RunState,Layer[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
└─ (冲刺无敌,忽略受击)
AttackState(Layer[1] 叠加,Layer[0] 保持原状态)
├─ 动画结束 → Layer[1] 淡出,恢复原状态(Idle/Run/Air)
├─ 连击窗口内攻击输入 → 切换至下一连击段
└─ 弹反输入 → ParryState(打断攻击,Layer[1] 立即淡出)
SoulSkillState / SpiritSkillState(Layer[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` | false(P0) / 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(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.3,Feel Squash) |
| 下冲中 | 速度线粒子(向上冒出)|
| 落地冲击 | 地面冲击圈(白色扩散环)+ 屏幕震动(Feel Shake Camera)+ 尘土粒子 |
| 冲击范围可视(仅 Debug)| Gizmos 绘制绿色圆圈 |