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

41 KiB
Raw Permalink Blame History

03 · 玩家系统

命名空间 BaseGames.Player · BaseGames.Player.States
所属文档集 ← 返回索引 · 总览
依赖 BaseGames.Input · BaseGames.Combat · BaseGames.Parry · Animancer Pro


目录

  1. 系统总览
  2. PlayerController — 协调器
  3. PlayerMovement — 物理移动
  4. PlayerStats — 属性系统
  5. PlayerCombat — 战斗管理
  6. FormController — 形态系统
  7. Animancer 双层动画系统
  8. PlayerAnimationConfigSO
  9. FSM 状态体系
  10. 状态详细设计
  11. 能力解锁系统
  12. PlayerMovementConfigSO
  13. 编辑器友好设计
  14. 空中冲刺Aerial Dash

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 / IsWallRightAirState 判断墙跳/墙滑

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) HurtBoxPlayerController 扣血,触发 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,形态切换时自动刷新所有攻击数据:

// 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 — 形态数据

[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 组件

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 — 形态配置容器

/// <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

[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);
// PlayerCombat有效命中时通知充能重置
void OnHitBoxTriggered(HurtInfo hurt)
{
    if (hurt.hitCount > 0)   // 有效命中(不是空挥)
        _stats.ResetAerialDashCharges();
}

13.3 DashState 扩展(空中冲刺分支)

DashState 内部区分地面冲刺和空中冲刺:

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空中冲刺

CanDashInAirPlayerStats.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 实现

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 绘制绿色圆圈