摄像机区域的架构改动

This commit is contained in:
2026-05-15 14:47:24 +08:00
parent 1b37297585
commit f264329751
3591 changed files with 1687228 additions and 446503 deletions

View File

@@ -0,0 +1,84 @@
using UnityEngine;
namespace BaseGames.Player.States
{
/// <summary>
/// 空中冲刺状态(架构 05_PlayerModule §12
/// 与地面 DashState 独立,消耗 MaxAerialDashes 次数;
/// 空中冲刺可向任意方向(使用移动输入方向,无输入则使用朝向)。
/// </summary>
public class AerialDashState : PlayerStateBase
{
private float _timer;
private int _aerialDashesLeft;
private int _facingDir;
public bool HasAerialDash => _aerialDashesLeft > 0;
// ── IsInvincible 不再在状态层硬编码,与 DashState 保持一致:
// 实际无敌用 Stats.BeginInvincibility(DashInvincibilityDuration) 面题。
// PlayerController.TakeDamage 已将 Stats.IsInvincible 纳入硬直判断。
public AerialDashState(PlayerController owner) : base(owner)
{
_aerialDashesLeft = 1;
}
public override void OnStateEnter()
{
_aerialDashesLeft = Mathf.Max(0, _aerialDashesLeft - 1);
_facingDir = Owner.FacingDirection;
_timer = Cfg.DashDuration;
// 无敌帧:与地面冲刺共享同一无敌 CDDashState._invincibilityCooldownTimer
// 条件 1已解锁 InvincibleDash
// 条件 2共享无敌冷却已就绪
var dashState = Owner.GetState<DashState>();
if (Stats != null && Stats.HasAbility(AbilityType.InvincibleDash)
&& dashState != null && dashState.CanGrantInvincibility)
{
Stats.BeginInvincibility(Cfg.DashInvincibilityDuration);
dashState.ResetInvincibilityCooldown(Cfg.DashInvincibilityCooldown);
}
// 关闭重力,施加冲刺速度(空中冲刺不改变垂直速度)
Move?.SetGravityScale(0f);
float dir = Input.MoveInput.x != 0 ? Mathf.Sign(Input.MoveInput.x) : _facingDir;
Move?.Dash(new Vector2(dir, 0f), Cfg.DashSpeed);
// 播放冲刺动画(复用地面冲刺动画)
if (AnimCfg?.Dash != null) Anim?.Play(AnimCfg.Dash);
}
public override void OnStateUpdate()
{
_timer -= Time.deltaTime;
if (_timer <= 0f)
{
Move?.SetGravityScale(Cfg.DefaultGravityScale);
Owner.TransitionTo(Owner.GetState<FallState>());
}
}
public override void OnStateExit()
{
Move?.SetGravityScale(Cfg.DefaultGravityScale);
}
public override void OnStateFixedUpdate()
{
// 冲刺期间锁定速度
if (_timer > 0f)
{
float dir = Input.MoveInput.x != 0 ? Mathf.Sign(Input.MoveInput.x) : _facingDir;
Move?.Dash(new Vector2(dir, 0f), Cfg.DashSpeed);
}
}
/// <summary>着地时重置空中冲刺次数(由 PlayerController 在着地时调用)。</summary>
public void ResetAerialDashes()
{
_aerialDashesLeft = Cfg.MaxAerialDashes;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 38830e1649a9eb548b20540a737bdf09
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,46 @@
using BaseGames.Combat;
namespace BaseGames.Player.States
{
/// <summary>
/// 空中攻击状态(架构 05_PlayerModule §2
/// 由 FallState / JumpState 接收攻击输入后转入;
/// Animancer 帧事件驱动 HitBoxAir 激活;结束后回到 FallState。
/// </summary>
public class AirAttackState : PlayerStateBase
{
public AirAttackState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
Owner.Combat?.SetComboSegmentSource(0);
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Air);
var clip = Owner.Weapon?.ActiveWeapon?.airAttackClip;
if (clip != null && clip.Clip != null)
{
var state = Anim.Play(clip);
state.Events(this).OnEnd = OnClipEnd;
}
else if (AnimCfg?.AirAttack != null)
{
var state = Anim.Play(AnimCfg.AirAttack);
state.Events(this).OnEnd = OnClipEnd;
}
else
{
OnClipEnd();
}
}
public override void OnStateExit()
{
Owner.Combat?.DisableAllWeaponHitBoxes();
}
private void OnClipEnd()
{
Owner.TransitionTo(Owner.GetState<FallState>());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0414d90465115124bbbfb05141a405f4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,69 @@
using BaseGames.Combat;
namespace BaseGames.Player.States
{
/// <summary>
/// 地面攻击状态3 段连击)。
/// 由 PlayerController 实例化AttackEvent 触发切换。
/// 通过 Animancer 帧事件驱动 HitBox 激活/关闭。
/// </summary>
public class AttackState : PlayerStateBase
{
private int _comboIndex;
public AttackState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
_comboIndex = 0;
PlayAttackClip();
Input.AttackEvent += OnAttackInput;
}
public override void OnStateExit()
{
Input.AttackEvent -= OnAttackInput;
Owner.Combat?.DisableAllWeaponHitBoxes();
}
public override void OnStateUpdate() { }
public override void OnStateFixedUpdate() { }
// ── 内部 ──────────────────────────────────────────────────────────────
private void PlayAttackClip()
{
// ⚠️ 字段名 GroundAttacks非 AttackChainClips
var clip = AnimCfg.GroundAttacks[_comboIndex];
var state = Anim.Play(clip);
var events = state.Events(this);
events.OnEnd = OnClipEnd;
// HitBox 由 Animancer 归一化时间事件驱动(时间点配置于 PlayerAnimationConfigSO
var timings = AnimCfg?.GroundAttackTimings;
float enterTime = timings != null && _comboIndex < timings.Length
? timings[_comboIndex].HitBoxEnter : 0.3f;
float exitTime = timings != null && _comboIndex < timings.Length
? timings[_comboIndex].HitBoxExit : 0.6f;
events.Add(enterTime, () => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground));
events.Add(exitTime, () => Owner.Combat?.DisableAllWeaponHitBoxes());
}
private void OnClipEnd()
{
Owner.Combat?.DisableAllWeaponHitBoxes();
Owner.TransitionTo(Owner.GetState<IdleState>());
}
private void OnAttackInput()
{
// 连击段数从配置 SO 动态读取,无须修改代码即可支持任意段数连击
int maxCombo = AnimCfg?.GroundAttacks?.Length ?? 1;
if (_comboIndex < maxCombo - 1)
{
_comboIndex++;
PlayAttackClip();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7d976cdcc6a9c44ba569bff0147f6c7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,24 @@
{
"excludePlatforms": [],
"allowUnsafeCode": false,
"precompiledReferences": [],
"name": "BaseGames.Player.States",
"defineConstraints": [],
"noEngineReferences": false,
"versionDefines": [],
"rootNamespace": "BaseGames.Player.States",
"references": [
"BaseGames.Player",
"BaseGames.Core.Events",
"BaseGames.Input",
"BaseGames.Combat",
"BaseGames.Parry",
"BaseGames.Feedback",
"Kybernetik.Animancer",
"BaseGames.World",
"BaseGames.Skills"
],
"autoReferenced": true,
"overrideReferences": false,
"includePlatforms": []
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c336a32eed62ced4280d1d4c9782ec91
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,101 @@
using UnityEngine;
using System.Collections;
namespace BaseGames.Player.States
{
/// <summary>
/// 地面冲刺状态(架构 05_PlayerModule §2 + §12
/// 施加水平位移 + 无敌帧;冲刺期间重力归零,结束后恢复;
/// 需解锁 AbilityType.Dash 才能进入PlayerController 负责条件检查)。
/// </summary>
public class DashState : PlayerStateBase
{
private float _timer;
private float _cooldownTimer;
/// <summary>
/// 无敌的独立冷却计时器。由 TickCooldown 每帧减少;
/// 地面冲刺和空中冲刺共享此计时器AerialDashState 通过 GetState&lt;DashState&gt;() 读写)。
/// </summary>
private float _invincibilityCooldownTimer;
private int _facingDir;
public bool CanDash => _cooldownTimer <= 0f;
/// <summary>
/// 无敌帧是否已冷却,即本次冲刺可以获得无敌。
/// DashState 和 AerialDashState 共享同一实例计时器实现 "spam 冲刺不能持续无敌" 机制。
/// </summary>
public bool CanGrantInvincibility => _invincibilityCooldownTimer <= 0f;
/// <summary>重置无敌冷却DashState/AerialDashState 驾用无敌后调用)。</summary>
public void ResetInvincibilityCooldown(float cd) => _invincibilityCooldownTimer = cd;
// ── IsInvincible 不再在状态层硬编码,改为全面依赖 Stats._invincibleTimer ────────────
// 冲刺无敌窗口 = BeginInvincibility(DashInvincibilityDuration) 设定的时间限 Stats._invincibleTimer
// PlayerController.TakeDamage 已改为将 Stats.IsInvincible 纳入硬直判断,居间无敌窗口自然失效。
// 不再需要 override IsInvincible。
public DashState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
Debug.Assert(Cfg != null, "[DashState] MovementConfig 未配置,请在 PlayerController Inspector 中绑定 MovementConfig SO。", _owner);
_facingDir = Owner.FacingDirection;
_timer = Cfg.DashDuration;
// 无敌帧:
// 条件 1已解锁 InvincibleDash
// 条件 2无敌冷却已就绪防止 spam 冲刺连序无敌)
// 窗口时长 = DashInvincibilityDuration < DashDuration冲刺后段无保护对齐 HK
if (Stats != null && Stats.HasAbility(AbilityType.InvincibleDash) && CanGrantInvincibility)
{
Stats.BeginInvincibility(Cfg.DashInvincibilityDuration);
_invincibilityCooldownTimer = Cfg.DashInvincibilityCooldown;
}
// 关闭重力,施加冲刺速度
Move?.SetGravityScale(0f);
Move?.Dash(new Vector2(_facingDir, 0f), Cfg.DashSpeed);
// 播放冲刺动画
if (AnimCfg?.Dash != null) Anim?.Play(AnimCfg.Dash);
}
public override void OnStateUpdate()
{
_timer -= Time.deltaTime;
if (_timer <= 0f)
EndDash();
}
public override void OnStateExit()
{
// 恢复默认重力
Move?.SetGravityScale(Cfg.DefaultGravityScale);
_cooldownTimer = Cfg.DashCooldown;
}
public override void OnStateFixedUpdate()
{
// 冲刺期间保持冲刺速度(防止摩擦力减速)
if (_timer > 0f)
Move?.Dash(new Vector2(_facingDir, 0f), Cfg.DashSpeed);
}
private void EndDash()
{
if (Move != null && Move.IsGrounded)
Owner.TransitionTo(Owner.GetState<IdleState>());
else
Owner.TransitionTo(Owner.GetState<FallState>());
}
/// <summary>每帧减少冒小帮冷却(由 PlayerController.Update 调用)。</summary>
public void TickCooldown(float dt)
{
if (_cooldownTimer > 0f) _cooldownTimer -= dt;
if (_invincibilityCooldownTimer > 0f) _invincibilityCooldownTimer -= dt;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 907f53feec688524b87e472f34d46ab3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,39 @@
namespace BaseGames.Player.States
{
/// <summary>
/// 死亡状态(架构 05_PlayerModule §2
/// 冻结物理ZeroVelocity + 关闭重力),播放 Dead 动画;
/// 不自动转出 — 由 DeathRespawnSystem 通过 EVT_PlayerRespawned 触发重置。
/// </summary>
public class DeadState : PlayerStateBase
{
public DeadState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
// 冻结物理
Move?.ZeroVelocity();
Move?.SetGravityScale(0f);
// 禁用 HurtBox防止重复受击
if (Owner.HurtBox != null)
Owner.HurtBox.SetActive(false);
// 播放死亡动画
if (AnimCfg?.Dead != null)
Anim?.Play(AnimCfg.Dead);
}
public override void OnStateExit()
{
// 复活时恢复重力和 HurtBox
Move?.SetGravityScale(Owner.MovConfig.DefaultGravityScale);
if (Owner.HurtBox != null)
Owner.HurtBox.SetActive(true);
}
// 死亡状态不接受任何输入或状态转换
public override void OnStateUpdate() { }
public override void OnStateFixedUpdate() { }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1761709fa9be931418e37b2387899077
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,83 @@
using UnityEngine;
using BaseGames.Combat;
namespace BaseGames.Player.States
{
/// <summary>
/// 下劈(踩踏 Pogo状态架构 05_PlayerModule §2
/// 需解锁 AbilityType.DownSlash 才能进入;
/// 激活 HitBoxDown着地或命中后弹跳。
/// </summary>
public class DownAttackState : PlayerStateBase
{
private bool _hasHitEnemy;
private bool _exited;
public DownAttackState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
_hasHitEnemy = false;
_exited = false;
if (Owner.Combat != null)
Owner.Combat.OnDownHitConfirmed += OnDownHitConfirmed;
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Down);
var clip = Owner.Weapon?.ActiveWeapon?.downAttackClip;
if (clip != null && clip.Clip != null)
{
var state = Anim.Play(clip);
state.Events(this).OnEnd = OnClipEnd;
}
else if (AnimCfg?.DownAttack != null)
{
var state = Anim.Play(AnimCfg.DownAttack);
state.Events(this).OnEnd = OnClipEnd;
}
else
{
OnClipEnd();
}
// 施加向下的速度(下劈冲击)
if (Move?.Rb != null)
Move.Rb.velocity = new Vector2(Move.Rb.velocity.x, -18f);
}
public override void OnStateExit()
{
_exited = true;
if (Owner.Combat != null)
Owner.Combat.OnDownHitConfirmed -= OnDownHitConfirmed;
Owner.Combat?.DisableAllWeaponHitBoxes();
}
private void OnDownHitConfirmed(BaseGames.Combat.DamageInfo _)
{
if (_hasHitEnemy) return;
_hasHitEnemy = true;
// Pogo 弹跳:命中敌人后向上弹起
Move.Jump();
}
public override void OnStateUpdate()
{
// 着地时回到 Idle / Run
if (Move.IsGrounded)
{
Owner.TransitionTo(Owner.GetState<IdleState>());
}
}
private void OnClipEnd()
{
if (_exited) return;
if (!Move.IsGrounded)
Owner.TransitionTo(Owner.GetState<FallState>());
else
Owner.TransitionTo(Owner.GetState<IdleState>());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 35c14ecd9a2db5c48a95c015ac6128fd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,84 @@
using UnityEngine;
namespace BaseGames.Player.States
{
/// <summary>
/// 下落状态(对齐空洞骑士手感)。
/// - 郊狼跳CoyoteTimer > 0 时按跳跃 → 一段跳JumpState使用 JumpForce
/// - 二段跳CoyoteTimer 耗尽后按跳跃且 AirJumpsLeft > 0 → JumpState使用 DoubleJumpForce
/// - 空中冲刺HasAbility(AirDash) && HasAerialDash → AerialDashState。
/// - 增强下落重力FallGravityMult确保下落快于上升手感紧实。
/// </summary>
public class FallState : PlayerStateBase
{
public FallState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
if (AnimCfg?.Fall != null)
Anim.Play(AnimCfg.Fall);
}
public override void OnStateUpdate()
{
// ── 跳跃输入(郊狼跳 / 二段跳)────────────────────────────────────
if (Buffer.ConsumeJump())
{
if (Move.HasCoyoteTime)
{
// 郊狼跳:一段跳,使用 JumpForce
_owner.TransitionTo(_owner.GetState<JumpState>());
return;
}
if (Owner.AirJumpsLeft > 0)
{
// 二段跳:通过 SetDoubleJump 标记 JumpState 使用 DoubleJumpForce
Owner.UseAirJump();
Owner.GetState<JumpState>()?.SetDoubleJump(true);
_owner.TransitionTo(_owner.GetState<JumpState>());
return;
}
// 无跳跃机会输入已消耗静默忽略HK 相同行为)
}
// ── 空中冲刺────────────────────────────────────────────────────────
if (Buffer.ConsumeDash()
&& Stats != null && Stats.HasAbility(AbilityType.AirDash))
{
var aerialDash = Owner.GetState<AerialDashState>();
if (aerialDash != null && aerialDash.HasAerialDash)
{
_owner.TransitionTo(aerialDash);
return;
}
}
// ── 着地──────────────────────────────────────────────────────────
if (Move.IsGrounded)
{
Move.ZeroVelocity();
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
_owner.TransitionTo(_owner.GetState<RunState>());
else
_owner.TransitionTo(_owner.GetState<IdleState>());
return;
}
// 空中水平移动
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
}
public override void OnStateFixedUpdate()
{
// 增强下落重力FallGravityMult 对齐 HK下落比上升更快
if (Move.Rb.velocity.y < 0f)
{
float extraGrav = Physics2D.gravity.y * (Cfg.FallGravityMult - 1f) * Time.fixedDeltaTime;
Move.Rb.velocity = new Vector2(
Move.Rb.velocity.x,
Mathf.Max(Move.Rb.velocity.y + extraGrav, -Cfg.MaxFallSpeed));
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d81f2601a90beeb4382680e53e18be63
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,63 @@
using UnityEngine;
using BaseGames.Combat;
namespace BaseGames.Player.States
{
/// <summary>
/// 受击硬直状态(架构 05_PlayerModule §2
/// 由 PlayerController.TakeDamage 在玩家非无敌时触发;
/// 播放 Hurt 动画,施加击退,结束后回到 Idle 或 Fall。
/// </summary>
public class HurtState : PlayerStateBase
{
private float _timer;
private bool _ended;
public HurtState(PlayerController owner) : base(owner) { }
public void Initialize(BaseGames.Combat.DamageInfo info)
{
// 由 PlayerController.TakeDamage 传入伤害信息
if (info.KnockbackForce > 0.01f)
Move?.ApplyKnockback(info.KnockbackDirection, info.KnockbackForce);
}
public override void OnStateEnter()
{
// 持续时长从 PlayerAnimationConfigSO 读取,不同攻击可设置不同硬直
_timer = Owner.AnimConfig?.HurtDuration ?? 0.4f;
_ended = false;
Stats?.BeginInvincibility();
if (AnimCfg?.Hurt != null)
{
var state = Anim?.Play(AnimCfg.Hurt);
if (state != null)
state.Events(this).OnEnd = OnHurtEnd;
}
}
public override void OnStateUpdate()
{
_timer -= Time.deltaTime;
if (_timer <= 0f)
OnHurtEnd();
}
private void OnHurtEnd()
{
if (_ended) return;
_ended = true;
if (Stats != null && !Stats.IsAlive)
{
Owner.TransitionTo(Owner.GetState<DeadState>());
return;
}
if (Move.IsGrounded)
Owner.TransitionTo(Owner.GetState<IdleState>());
else
Owner.TransitionTo(Owner.GetState<FallState>());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f7192bcf89ed1f5419d40fa182ac66ba
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,46 @@
using UnityEngine;
namespace BaseGames.Player.States
{
/// <summary>闲置状态。默认入口状态,播放 Idle 动画。</summary>
public class IdleState : PlayerStateBase
{
public IdleState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
if (AnimCfg?.Idle != null)
Anim.Play(AnimCfg.Idle);
Move?.ZeroHorizontalVelocity();
// 落地时重置空中能力计数器
Owner.GetState<AerialDashState>()?.ResetAerialDashes();
Owner.ResetAirJumps();
}
public override void OnStateUpdate()
{
if (!Move.IsGrounded)
{
_owner.TransitionTo(_owner.GetState<FallState>());
return;
}
if (Buffer.ConsumeJump())
{
_owner.TransitionTo(_owner.GetState<JumpState>());
return;
}
// 地面冲刺:需解锁 Dash 能力,且冲刺不在冷却
if (Buffer.ConsumeDash()
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
&& Owner.GetState<DashState>() is { CanDash: true } dashState)
{
_owner.TransitionTo(dashState);
return;
}
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
{
_owner.TransitionTo(_owner.GetState<RunState>());
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 84b6b9fa502d3d34a8d7284831404d75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,84 @@
using UnityEngine;
namespace BaseGames.Player.States
{
/// <summary>
/// 跳跃状态(对齐空洞骑士手感)。
/// - 一段跳 / 郊狼跳OnStateEnter 时调用 Move.Jump()。
/// - 二段跳Monarch Wings 等效):上升或下落途中再按跳跃且 AirJumpsLeft > 0
/// 调用 Move.DoubleJump(),重播跳跃动画,不离开本状态(保持速度截断逻辑)。
/// - 空中冲刺:上升途中按冲刺且 HasAbility(AirDash) → AerialDashState。
/// - 变高跳:松开跳跃键触发 JumpCancelledEvent → CutJump()(系数 = JumpCutMultiplier
/// - _isDoubleJump由 FallState 在转换前通过 SetDoubleJump(true) 预设,
/// 使 OnStateEnter 对二段跳调用 Move.DoubleJump() 而非 Move.Jump()。
/// </summary>
public class JumpState : PlayerStateBase
{
private bool _isDoubleJump;
public JumpState(PlayerController owner) : base(owner) { }
/// <summary>
/// 由 FallState 在转换到 JumpState 前调用,标记本次进入为二段跳,
/// 以便 OnStateEnter 使用 DoubleJumpForce 而非 JumpForce。
/// </summary>
public void SetDoubleJump(bool isDouble) => _isDoubleJump = isDouble;
public override void OnStateEnter()
{
if (AnimCfg?.Jump != null)
Anim.Play(AnimCfg.Jump);
if (_isDoubleJump)
Move.DoubleJump();
else
Move.Jump();
_isDoubleJump = false; // 消耗标记
Input.JumpCancelledEvent += OnJumpCancelled;
}
public override void OnStateUpdate()
{
// 上升结束时转为下落
if (Move.Rb.velocity.y <= 0f)
{
_owner.TransitionTo(_owner.GetState<FallState>());
return;
}
// 空中冲刺(优先于二段跳:冲刺可保存二段跳机会)
if (Buffer.ConsumeDash()
&& Stats != null && Stats.HasAbility(AbilityType.AirDash))
{
var aerialDash = Owner.GetState<AerialDashState>();
if (aerialDash != null && aerialDash.HasAerialDash)
{
_owner.TransitionTo(aerialDash);
return;
}
}
// 二段跳:上升阶段即可触发(类比 HK Monarch Wings随时可二段跳
if (Buffer.ConsumeJump() && Owner.AirJumpsLeft > 0)
{
Owner.UseAirJump();
Move.DoubleJump();
// 留在 JumpState速度截断JumpCancelledEvent和落地检测逻辑继续生效
if (AnimCfg?.Jump != null) Anim?.Play(AnimCfg.Jump);
return;
}
// 水平移动HK 空中控制:与跑步同速)
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
}
public override void OnStateExit()
{
Input.JumpCancelledEvent -= OnJumpCancelled;
}
private void OnJumpCancelled() => Move.CutJump();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 643f44aee61bd684ebd893779e1122aa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,46 @@
namespace BaseGames.Player.States
{
/// <summary>
/// 弹反预备状态(架构 05_PlayerModule §2完整实现在 Week 6
/// 开启 ParrySystem 弹反窗口,播放 ParryStart 动画;
/// 成功弹反后 ParrySystem.ConsumeParry() 返回 trueHurtBox 不处理该次伤害。
/// </summary>
public class ParryState : PlayerStateBase
{
public ParryState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
// 开启弹反窗口HurtBox.ReceiveDamage 步骤 2 查询)
Owner.Parry?.OpenParryWindow();
// 停止移动
Move?.ZeroHorizontalVelocity();
// 播放弹反预备动画
if (AnimCfg?.ParryStart != null)
{
var state = Anim?.Play(AnimCfg.ParryStart);
if (state != null)
{
state.Events(this).OnEnd = OnParryEnd;
return;
}
}
// 无动画则立即结束
OnParryEnd();
}
public override void OnStateExit()
{
// 确保弹反窗口关闭(无论是否成功)
Owner.Parry?.CloseParryWindow();
}
private void OnParryEnd()
{
Owner.TransitionTo(Owner.GetState<IdleState>());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 29962783977c7e345bd6b2716c59c39b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,343 @@
using UnityEngine;
using System.Linq;
using Animancer;
using BaseGames.Core.Events;
using BaseGames.Input;
using BaseGames.Combat;
using BaseGames.Parry;
using BaseGames.Skills;
namespace BaseGames.Player.States
{
/// <summary>
/// 玩家主控制器(协调器)。位于 Player/States/ 程序集,以便引用所有具体状态类型。
/// 实现 IDamageable + IPoiseSource架构 06_CombatModule §13
/// 依赖注入:同节点组件由 RequireComponent + Awake 自动获取;跨节点引用通过 [SerializeField] 绑定。
/// </summary>
[DefaultExecutionOrder(-100)]
[RequireComponent(typeof(InputBuffer))]
[RequireComponent(typeof(PlayerMovement))]
[RequireComponent(typeof(PlayerStats))]
[RequireComponent(typeof(AnimancerComponent))]
public class PlayerController : MonoBehaviour, IDamageable, IPoiseSource
{
// ── 同节点组件(由 RequireComponent 保证存在Awake 中自动获取)─────
private PlayerMovement _movement;
private PlayerStats _stats;
private AnimancerComponent _animancer;
// ── 配置 SO ───────────────────────────────────────────────────────────
[Header("配置")]
[SerializeField] private PlayerMovementConfigSO _movementConfig;
[SerializeField] private PlayerAnimationConfigSO _animConfig;
[SerializeField] private InputReaderSO _inputReader;
[SerializeField] private FormConfigSO _formConfig;
// ── 战斗组件 ──────────────────────────────────────────────────────────
[Header("战斗")]
[SerializeField] private PlayerCombat _combat;
[SerializeField] private FormController _formController;
[SerializeField] private WeaponManager _weaponManager;
[SerializeField] private SkillManager _skillManager;
[SerializeField] private SpringSystem _springSystem;
[SerializeField] private ParrySystem _parrySystem;
[SerializeField] private HurtBox _hurtBox;
[SerializeField] private ShieldComponent _shield;
[SerializeField] private PlayerWallDetector _wallDetector;
// ── 事件频道 ──────────────────────────────────────────────────────────
[Header("事件频道")]
[SerializeField] private VoidEventChannelSO _onPlayerDied;
/// <summary>
/// Start() 时广播玩家 TransformEnemyBase / ProjectileManager 等订阅此频道)。
/// 替代每个敌人在 Awake 中独立 FindWithTag 的 O(n) 全场景扫描。
/// </summary>
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
// ── 运行时 ────────────────────────────────────────────────────────────
private InputBuffer _inputBuffer;
private bool _missingDependencyLogged;
private bool _dependenciesReady;
/// <summary>
/// 当前腾空可用的额外跳跃次数(二段跳)。
/// 由 IdleState/RunState.OnStateEnter 落地时通过 ResetAirJumps() 重置;
/// JumpState/FallState 判断 HasAbility(DoubleJump) 后消耗。
/// </summary>
private int _airJumpsLeft;
#if UNITY_EDITOR
[Header("调试")]
[SerializeField] private bool _debugValidateTransitions = true;
#endif
// Overlay LayerLayer 1供 SpringState / SoulSkill 等叠加层动画使用
// Base LayerLayer 0移动/攻击/受伤/死亡等全身状态动画
private AnimancerLayer _overlayLayer;
// ── 状态实例 ──────────────────────────────────────────────────────────
private PlayerStateBase _currentState;
private readonly System.Collections.Generic.Dictionary<System.Type, PlayerStateBase> _states = new();
// ── IDamageable 实现 ──────────────────────────────────────────────────
public bool IsAlive => _stats != null && _stats.IsAlive;
public bool IsInvincible => _stats != null && _stats.IsInvincible;
public int Defense => 0;
public void TakeDamage(DamageInfo info)
{
if (_stats == null) return;
_stats.TakeDamage(info.FinalDamage);
// 当前状态标记为无敌(如旧版冲刺状态),或 Stats 层无敌窗口仍激活
// (冲刺无敌帧 DashInvincibilityDuration 内:跳过受击硬直;窗口过期后可被打断)
if (_currentState?.IsInvincible == true || (_stats != null && _stats.IsInvincible)) return;
if (_stats.IsAlive)
{
GetState<HurtState>()?.Initialize(info);
TransitionTo(GetState<HurtState>());
}
else
{
TransitionTo(GetState<DeadState>());
_onPlayerDied?.Raise();
}
}
// ── IPoiseSource 实现(架构 06_CombatModule §13─────────────────────
/// <summary>
/// 玩家不拥有霸体,始终返回 <see cref="PoiseLevel.None"/>。
/// 设计决策:类似 Hollow Knight玩家依靠走位和弹反规避伤害而非硬吃。
/// 若未来需要临时霸体(如特定技能动作),请通过独立的覆盖标记实现,
/// 而非在此处引入状态,以保持接口语义清晰。
/// </summary>
public PoiseLevel GetCurrentPoiseLevel() => PoiseLevel.None;
// ── 公开属性(供状态类访问)──────────────────────────────────────────
public PlayerMovement Movement => _movement;
public PlayerStats Stats => _stats;
public AnimancerComponent Animancer => _animancer;
public PlayerMovementConfigSO MovConfig => _movementConfig;
public PlayerAnimationConfigSO AnimConfig => _animConfig;
public InputReaderSO Input => _inputReader;
public InputBuffer Buffer => _inputBuffer;
public PlayerCombat Combat => _combat;
public FormController Form => _formController;
public WeaponManager Weapon => _weaponManager;
public SkillManager Skill => _skillManager;
public SpringSystem Spring => _springSystem;
public ParrySystem Parry => _parrySystem;
public HurtBox HurtBox => _hurtBox;
public ShieldComponent Shield => _shield;
public PlayerWallDetector WallDetector => _wallDetector;
public bool IsGrounded => _movement != null && _movement.IsGrounded;
public int FacingDirection => _movement != null ? _movement.FacingDirection : 1;
// ── 空中跳跃 API二段跳────────────────────────────────────────────
public int AirJumpsLeft => _airJumpsLeft;
public void UseAirJump() => _airJumpsLeft = Mathf.Max(0, _airJumpsLeft - 1);
/// <summary>
/// 落地时重置空中跳跃次数(由 IdleState/RunState.OnStateEnter 调用)。
/// 若解锁 DoubleJump 则重置为 1否则为 0。
/// </summary>
public void ResetAirJumps() =>
_airJumpsLeft = _stats != null && _stats.HasAbility(AbilityType.DoubleJump) ? 1 : 0;
// ── Overlay Layer API供 SpringState / SoulSkill 等叠加动画使用)─────
/// <summary>
/// 在 Overlay LayerLayer 1播放动画叠加于当前 Base Layer 动画之上。
/// 适用于灵泉使用、魂技能等需要与移动动画并行的上半身动作。
/// </summary>
public AnimancerState PlayOnOverlay(AnimationClip clip)
=> _overlayLayer?.Play(clip);
/// <summary>停止 Overlay Layer 动画,淡出回 Base Layer。</summary>
public void StopOverlay(float fadeDuration = 0.1f)
=> _overlayLayer?.StartFade(0f, fadeDuration);
// ── Unity Lifecycle ───────────────────────────────────────────────────
private void Awake()
{
Debug.Assert(_movementConfig != null, "[PlayerController] _movementConfig 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
ResolveDependencies();
// 初始化 Animancer 双层动画Layer 0 = Base全身状态Layer 1 = Overlay叠加层
if (_animancer != null)
{
// 确保 Layer 1 存在AnimancerComponent 默认只有 Layer 0
while (_animancer.Layers.Count <= 1)
_animancer.Layers.Add();
_overlayLayer = _animancer.Layers[1];
}
// 注入 HurtBox 依赖
if (_hurtBox != null)
{
if (_shield != null) _hurtBox.SetShieldable(_shield);
if (_parrySystem != null) _hurtBox.SetParrySystem(_parrySystem);
_hurtBox.SetPoiseSource(this);
}
// 将唯一配置点_inputReader注入到 ParrySystem。
// ParrySystem 不再需要在 Inspector 单独配置 InputReaderSO。
if (_parrySystem != null)
_parrySystem.SetInputReader(_inputReader);
InitializeStates();
// 订阅 ParrySystem C# 事件
if (_parrySystem != null)
{
_parrySystem.OnParryActivated += OnParryActivated;
_parrySystem.OnParryConsumed += OnParryConsumedHandler;
}
}
private void OnDestroy()
{
if (_parrySystem != null)
{
_parrySystem.OnParryActivated -= OnParryActivated;
_parrySystem.OnParryConsumed -= OnParryConsumedHandler;
}
}
/// <summary>弹反输入激活时由 ParrySystem 触发 → 转换到 ParryState。</summary>
private void OnParryActivated()
{
if (_states.ContainsKey(typeof(ParryState)))
TransitionTo(GetState<ParryState>());
}
/// <summary>弹反命中成功时由 ParrySystem 触发 → 发放灵力并恢复护盾。</summary>
private void OnParryConsumedHandler(BaseGames.Parry.ParryInfo info)
{
_stats?.AddSoul(info.SoulGained);
_shield?.OnParrySuccess();
}
private void Start()
{
if (!HasRequiredStateDependencies())
return;
// 广播玩家 TransformEnemyBase / ProjectileManager 等订阅者将通过事件接收引用
// (必须在 Start 中调用,确保所有 Awake/OnEnable 订阅已就绪)
_onPlayerSpawned?.Raise(transform);
TransitionTo(GetState<IdleState>());
}
private void Update()
{
if (!HasRequiredStateDependencies())
return;
// 冲刺冷却计时
GetState<DashState>()?.TickCooldown(Time.deltaTime);
_currentState?.OnStateUpdate();
}
private void FixedUpdate()
{
_currentState?.OnStateFixedUpdate();
}
private void LateUpdate()
{
_movement?.UpdateFacing();
}
// ── 状态机 ────────────────────────────────────────────────────────────
public void TransitionTo(PlayerStateBase newState)
{
#if UNITY_EDITOR
if (_debugValidateTransitions && _currentState != null && newState != null)
{
var allowed = _currentState.ValidTransitions;
if (allowed.Count > 0 && !allowed.Contains(newState.GetType()))
Debug.LogWarning(
$"[PlayerController] 非预期转换: {_currentState.GetType().Name} → {newState.GetType().Name}",
this);
}
#endif
_currentState?.OnStateExit();
_currentState = newState;
_currentState?.OnStateEnter();
}
private void InitializeStates()
{
_states[typeof(IdleState)] = new IdleState(this);
_states[typeof(RunState)] = new RunState(this);
_states[typeof(JumpState)] = new JumpState(this);
_states[typeof(FallState)] = new FallState(this);
_states[typeof(AttackState)] = new AttackState(this);
_states[typeof(DashState)] = new DashState(this);
_states[typeof(AerialDashState)] = new AerialDashState(this);
_states[typeof(WallSlideState)] = new WallSlideState(this);
_states[typeof(WallJumpState)] = new WallJumpState(this);
_states[typeof(AirAttackState)] = new AirAttackState(this);
_states[typeof(DownAttackState)] = new DownAttackState(this);
_states[typeof(UpAttackState)] = new UpAttackState(this);
_states[typeof(HurtState)] = new HurtState(this);
_states[typeof(DeadState)] = new DeadState(this);
_states[typeof(SpringState)] = new SpringState(this);
_states[typeof(ParryState)] = new ParryState(this);
_states[typeof(SwimState)] = new SwimState(this);
}
/// <summary>
/// 按类型获取状态实例。未注册时返回 null供可选状态调用方安全使用
/// </summary>
public T GetState<T>() where T : PlayerStateBase
=> _states.TryGetValue(typeof(T), out var s) ? (T)s : null;
private void ResolveDependencies()
{
if (_movement == null)
_movement = GetComponent<PlayerMovement>();
if (_stats == null)
_stats = GetComponent<PlayerStats>();
if (_animancer == null)
_animancer = GetComponent<AnimancerComponent>();
if (_inputBuffer == null)
_inputBuffer = GetComponent<InputBuffer>();
// 将唯一配置点_inputReader注入到同一 GameObject 上的 InputBuffer。
// InputBuffer 不再需要在 Inspector 单独配置 InputReaderSO。
if (_inputBuffer != null)
_inputBuffer.Init(_inputReader);
}
private bool HasRequiredStateDependencies()
{
if (_dependenciesReady) return true;
bool ok = _movement != null && _animancer != null && _inputBuffer != null && _inputReader != null;
if (ok)
{
_dependenciesReady = true;
_missingDependencyLogged = false;
return true;
}
if (!_missingDependencyLogged)
{
Debug.LogError($"[PlayerController] Missing required dependencies. " +
$"Movement={(_movement != null ? "OK" : "NULL")}, " +
$"Animancer={(_animancer != null ? "OK" : "NULL")}, " +
$"InputBuffer={(_inputBuffer != null ? "OK" : "NULL")}, " +
$"InputReader={(_inputReader != null ? "OK" : "NULL")}");
_missingDependencyLogged = true;
}
return false;
}
// ── 状态访问器 ────────────────────────────────────────────────────────
// 使用 GetState<T>() 按类型获取任意状态实例,不再暴露具名属性。
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e20d2200567c4ca4d8fa1a047c7bbd58
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: -100
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,48 @@
using Animancer;
using BaseGames.Input;
using BaseGames.Player;
namespace BaseGames.Player.States
{
/// <summary>
/// 所有玩家状态的抽象基类。持有 PlayerController 引用并提供便捷属性访问。
/// 状态不继承 MonoBehaviour生命周期由 PlayerController 驱动。
/// </summary>
public abstract class PlayerStateBase
{
protected PlayerController _owner;
protected PlayerStateBase(PlayerController owner) => _owner = owner;
public virtual void OnStateEnter() { }
public virtual void OnStateUpdate() { }
public virtual void OnStateFixedUpdate() { }
public virtual void OnStateExit() { }
public virtual PlayerStateBase GetNextState() => null;
/// <summary>
/// 此状态期间是否应视为无敌(忽略伤害)。
/// 冲刺等状态 override 为 truePlayerController.TakeDamage 据此判断是否进入受击。
/// </summary>
public virtual bool IsInvincible => false;
#if UNITY_EDITOR
/// <summary>
/// 此状态允许转换到的目标类型白名单(仅 Editor 调试用)。
/// 返回空列表表示不限制任何转换。子状态按需 override 来声明合法出口。
/// </summary>
public virtual System.Collections.Generic.IReadOnlyList<System.Type> ValidTransitions
=> System.Array.Empty<System.Type>();
#endif
// ── 便捷属性 ──────────────────────────────────────────────────────────
protected PlayerController Owner => _owner;
protected InputReaderSO Input => _owner.Input;
protected InputBuffer Buffer => _owner.Buffer;
protected PlayerMovement Move => _owner.Movement;
protected PlayerStats Stats => _owner.Stats;
protected AnimancerComponent Anim => _owner.Animancer;
protected PlayerMovementConfigSO Cfg => _owner.MovConfig;
protected PlayerAnimationConfigSO AnimCfg => _owner.AnimConfig;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d0e192113c871fe44ba2d9d56c95c27e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,51 @@
using UnityEngine;
namespace BaseGames.Player.States
{
/// <summary>跑步状态。播放 Run 动画并驱动水平移动。</summary>
public class RunState : PlayerStateBase
{
public RunState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
if (AnimCfg?.Run != null)
Anim.Play(AnimCfg.Run);
// 落地时重置空中能力计数器(绝大多数情况被 IdleState 覆盖,但水平落地直接进入 RunState 时也需要)
Owner.GetState<AerialDashState>()?.ResetAerialDashes();
Owner.ResetAirJumps();
}
public override void OnStateUpdate()
{
if (!Move.IsGrounded)
{
_owner.TransitionTo(_owner.GetState<FallState>());
return;
}
if (Buffer.ConsumeJump())
{
_owner.TransitionTo(_owner.GetState<JumpState>());
return;
}
// 地面冲刺:需解锁 Dash 能力,且冲刺不在冷却
if (Buffer.ConsumeDash()
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
&& Owner.GetState<DashState>() is { CanDash: true } dashState)
{
_owner.TransitionTo(dashState);
return;
}
if (Mathf.Abs(Input.MoveInput.x) < 0.1f)
{
_owner.TransitionTo(_owner.GetState<IdleState>());
return;
}
}
public override void OnStateFixedUpdate()
{
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f695c81f3a6b0bc4eafc75125bbf47aa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,55 @@
namespace BaseGames.Player.States
{
/// <summary>
/// 使用灵泉(治疗)状态(架构 05_PlayerModule §2
/// 消耗 SpringCharge播放治愈动画动画结束后回到 Idle。
/// 需在地面且有充能才能进入PlayerController 负责条件检查)。
///
/// 动画在 Overlay LayerLayer 1播放叠加于 Base Layer 的 Idle/Run 之上,
/// 符合架构 05_PlayerModule §14 双层动画设计。
/// </summary>
public class SpringState : PlayerStateBase
{
public SpringState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
// 消耗灵泉充能并治疗PlayerStats.UseSpring 内部回复 HP
bool used = Stats?.UseSpring() ?? false;
if (!used)
{
// 无充能时立即退出
Owner.TransitionTo(Owner.GetState<IdleState>());
return;
}
// 停止移动
Move?.ZeroHorizontalVelocity();
// 在 Overlay LayerLayer 1播放灵泉动画叠加于 Base Layer 的 Idle 之上
if (AnimCfg?.UseSpring != null)
{
var state = Owner.PlayOnOverlay(AnimCfg.UseSpring);
if (state != null)
{
state.Events(this).OnEnd = OnSpringEnd;
return;
}
}
// 无动画则直接结束
OnSpringEnd();
}
public override void OnStateExit()
{
// 淡出叠加层,恢复纯 Base Layer 动画
Owner.StopOverlay(fadeDuration: 0.1f);
}
private void OnSpringEnd()
{
Owner.TransitionTo(Owner.GetState<IdleState>());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e0a4d3517cee2b24da050f85ffe13fdd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,85 @@
// Assets/Scripts/Player/States/SwimState.cs
// 游泳状态玩家在液体中时使用Architecture 21_LiquidPuzzleModule §5
// ⚠️ 遵循 PlayerStateBase 构造函数注入模式(非 MonoBehaviour
// ⚠️ 输入通过 Input.MoveInput / Input.JumpStartedEvent 访问(项目实际 API
using BaseGames.Player;
using BaseGames.World.Liquid;
using UnityEngine;
namespace BaseGames.Player.States
{
/// <summary>
/// 游泳状态:玩家在液体中时使用。
/// 需要 AbilityType.Swim 已解锁;若未解锁则由 WaterDangerState 触发溺水流程。
/// 由 PlayerController 在收到 EVT_LiquidEntered 后切换进入。
/// </summary>
public class SwimState : PlayerStateBase
{
private LiquidPhysicsConfigSO _currentPhysics;
private float _originalGravity;
public SwimState(PlayerController owner) : base(owner) { }
/// <summary>由 PlayerController 在切换状态前调用,注入当前液体区域的物理配置。</summary>
public void SetPhysicsConfig(LiquidPhysicsConfigSO config) => _currentPhysics = config;
public override void OnStateEnter()
{
_originalGravity = Move.Rb.gravityScale;
Move.SetGravityScale(_currentPhysics?.GravityScale ?? 0.3f);
if (AnimCfg?.SwimIdle != null)
Anim?.Play(AnimCfg.SwimIdle);
Input.JumpStartedEvent += OnJumpStarted;
}
public override void OnStateExit()
{
Input.JumpStartedEvent -= OnJumpStarted;
Move.SetGravityScale(_originalGravity);
}
public override void OnStateUpdate()
{
var input = Input.MoveInput;
var maxSpeed = _currentPhysics?.MaxSwimSpeed ?? 4f;
var accel = _currentPhysics?.SwimAcceleration ?? 8f;
var drag = _currentPhysics?.DragCoefficient ?? 3f;
var rb = Move.Rb;
if (input != Vector2.zero)
{
var targetVel = input * maxSpeed;
rb.velocity = Vector2.MoveTowards(rb.velocity, targetVel, accel * Time.deltaTime);
if (AnimCfg?.SwimMove != null)
Anim?.Play(AnimCfg.SwimMove);
}
else
{
// 水下浮力(持续向上的微弱力)
rb.AddForce(Vector2.up * (_currentPhysics?.BuoyancyForce ?? 0.5f), ForceMode2D.Force);
if (AnimCfg?.SwimIdle != null)
Anim?.Play(AnimCfg.SwimIdle);
}
// 施加水阻
rb.velocity *= 1f - drag * Time.deltaTime;
}
public override PlayerStateBase GetNextState()
{
// 离开液体区域由 PlayerController 订阅 EVT_LiquidExited 后切换到 FallState
return null;
}
private void OnJumpStarted()
{
// 跳跃键 = 跃出水面冲量
Move.Rb.AddForce(Vector2.up * (_currentPhysics?.SurfaceExitSpeed ?? 5f),
ForceMode2D.Impulse);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 199e23e0415d2d04ebc19174f11f3231
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
using BaseGames.Combat;
namespace BaseGames.Player.States
{
/// <summary>
/// 上劈状态(架构 05_PlayerModule §2
/// 激活 HitBoxUp结束后回到 Idle地面或 FallState空中
/// </summary>
public class UpAttackState : PlayerStateBase
{
public UpAttackState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Up);
var clip = Owner.Weapon?.ActiveWeapon?.upAttackClip;
if (clip != null && clip.Clip != null)
{
var state = Anim.Play(clip);
state.Events(this).OnEnd = OnClipEnd;
}
else if (AnimCfg?.UpAttack != null)
{
var state = Anim.Play(AnimCfg.UpAttack);
state.Events(this).OnEnd = OnClipEnd;
}
else
{
OnClipEnd();
}
}
public override void OnStateExit()
{
Owner.Combat?.DisableAllWeaponHitBoxes();
}
private void OnClipEnd()
{
if (Move.IsGrounded)
Owner.TransitionTo(Owner.GetState<IdleState>());
else
Owner.TransitionTo(Owner.GetState<FallState>());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3917a3a7a101db249ab296e784afea24
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,58 @@
using UnityEngine;
namespace BaseGames.Player.States
{
/// <summary>
/// 蹬墙跳状态(架构 05_PlayerModule §2
/// 从 WallSlideState 进入;施加背墙方向的水平 + 垂直速度;
/// 短暂锁定水平输入后转为 FallState。
/// </summary>
public class WallJumpState : PlayerStateBase
{
private float _inputLockTimer;
private int _wallDir;
public WallJumpState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
// 记录墙壁方向(跳跃反向)
_wallDir = Owner.WallDetector != null ? Owner.WallDetector.WallDirection : 0;
if (_wallDir == 0) _wallDir = -Owner.FacingDirection;
// 施加蹬墙跳速度
Move?.WallJump(_wallDir);
// 锁定水平输入
_inputLockTimer = Cfg.WallJumpInputLockDuration;
// 播放跳跃动画(复用跳跃动画)
if (AnimCfg?.Jump != null) Anim?.Play(AnimCfg.Jump);
Input.JumpCancelledEvent += OnJumpCancelled;
}
public override void OnStateExit()
{
Input.JumpCancelledEvent -= OnJumpCancelled;
}
public override void OnStateUpdate()
{
_inputLockTimer -= Time.deltaTime;
// 上升结束 → 下落
if (!Move.IsRising)
{
Owner.TransitionTo(Owner.GetState<FallState>());
return;
}
// 输入锁结束后允许水平控制
if (_inputLockTimer <= 0f && Mathf.Abs(Input.MoveInput.x) > 0.01f)
Move?.Move(Input.MoveInput.x * Cfg.RunSpeed);
}
private void OnJumpCancelled() => Move?.CutJump();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 351b042e66111474d8b152878313dc92
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,55 @@
using UnityEngine;
namespace BaseGames.Player.States
{
/// <summary>
/// 壁滑状态(架构 05_PlayerModule §2
/// 需有 PlayerWallDetector.IsTouchingWall == true 才能进入;
/// 限制下落速度为 WallSlideSpeed按跳跃则切换到 WallJumpState。
/// </summary>
public class WallSlideState : PlayerStateBase
{
public WallSlideState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
{
if (AnimCfg?.WallSlide != null)
Anim?.Play(AnimCfg.WallSlide);
Input.JumpStartedEvent += OnJumpPressed;
}
public override void OnStateExit()
{
Input.JumpStartedEvent -= OnJumpPressed;
}
public override void OnStateUpdate()
{
// 离开墙壁 → 下落
if (Owner.WallDetector == null || !Owner.WallDetector.IsTouchingWall)
{
Owner.TransitionTo(Owner.GetState<FallState>());
return;
}
// 着地 → 闲置
if (Move.IsGrounded)
{
Owner.TransitionTo(Owner.GetState<IdleState>());
return;
}
}
public override void OnStateFixedUpdate()
{
// 限制下落速度(壁滑缓慢下落)
Move?.ApplyWallSlide();
}
private void OnJumpPressed()
{
Owner.TransitionTo(Owner.GetState<WallJumpState>());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7578dea22a1b02d44970bcac667b1401
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: