41 KiB
03 · 玩家系统
命名空间
BaseGames.Player·BaseGames.Player.States
所属文档集 ← 返回索引 · 总览
依赖BaseGames.Input·BaseGames.Combat·BaseGames.Parry· Animancer Pro
目录
- 系统总览
- PlayerController — 协调器
- PlayerMovement — 物理移动
- PlayerStats — 属性系统
- PlayerCombat — 战斗管理
- FormController — 形态系统
- Animancer 双层动画系统
- PlayerAnimationConfigSO
- FSM 状态体系
- 状态详细设计
- 能力解锁系统
- PlayerMovementConfigSO
- 编辑器友好设计
- 空中冲刺(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 是纯协调器,只负责:
- 初始化并持有子组件引用
- 维护 Animancer FSM(当前状态切换)
- 在受击时调用
ForceState(HurtState) - 在死亡时触发
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对外暴露 - 支持单向平台(
OneWayPlatformLayer,向下跳跃时临时忽略)
墙壁检测
- 左右两侧各一个
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,形态切换时自动刷新所有攻击数据:
// 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 — 形态数据
[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.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<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)(保持 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)
- 播放 Hurt 动画(Layer[0])
- PlayerMovement.ApplyKnockback(info)
- PlayerStats.BeginInvincibility(IFrameDuration)
- PlayerFeedback.OnTakeHit() ↓ 若 HP = 0:
- 播放 Death 动画
- 禁用所有输入(InputReader.DisableAllInput)
- 延迟 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(空中冲刺)
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 实现
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.3,Feel Squash) |
| 下冲中 | 速度线粒子(向上冒出) |
| 落地冲击 | 地面冲击圈(白色扩散环)+ 屏幕震动(Feel Shake Camera)+ 尘土粒子 |
| 冲击范围可视(仅 Debug) | Gizmos 绘制绿色圆圈 |