多轮审查和修复
This commit is contained in:
72
Assets/Scripts/Player/States/AerialDashState.cs
Normal file
72
Assets/Scripts/Player/States/AerialDashState.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
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;
|
||||
|
||||
public AerialDashState(PlayerController owner) : base(owner)
|
||||
{
|
||||
_aerialDashesLeft = 1;
|
||||
}
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
_aerialDashesLeft = Mathf.Max(0, _aerialDashesLeft - 1);
|
||||
_facingDir = Owner.FacingDirection;
|
||||
_timer = Cfg.DashDuration;
|
||||
|
||||
// 无敌帧
|
||||
Stats?.BeginInvincibility(Cfg.DashDuration + 0.05f);
|
||||
|
||||
// 关闭重力,施加冲刺速度(空中冲刺不改变垂直速度)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/AerialDashState.cs.meta
Normal file
11
Assets/Scripts/Player/States/AerialDashState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38830e1649a9eb548b20540a737bdf09
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
46
Assets/Scripts/Player/States/AirAttackState.cs
Normal file
46
Assets/Scripts/Player/States/AirAttackState.cs
Normal 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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/AirAttackState.cs.meta
Normal file
11
Assets/Scripts/Player/States/AirAttackState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0414d90465115124bbbfb05141a405f4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -39,23 +39,27 @@ namespace BaseGames.Player.States
|
||||
var events = state.Events(this);
|
||||
events.OnEnd = OnClipEnd;
|
||||
|
||||
// HitBox 由 Animancer 归一化时间事件驱动
|
||||
events.Add(0.3f,
|
||||
() => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground));
|
||||
events.Add(0.6f,
|
||||
() => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||||
// 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()
|
||||
{
|
||||
Input.AttackEvent -= OnAttackInput;
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
Owner.TryTransitionState(Owner.IdleState);
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
}
|
||||
|
||||
private void OnAttackInput()
|
||||
{
|
||||
if (_comboIndex < 2)
|
||||
// 连击段数从配置 SO 动态读取,无须修改代码即可支持任意段数连击
|
||||
int maxCombo = AnimCfg?.GroundAttacks?.Length ?? 1;
|
||||
if (_comboIndex < maxCombo - 1)
|
||||
{
|
||||
_comboIndex++;
|
||||
PlayAttackClip();
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
"BaseGames.Combat",
|
||||
"BaseGames.Parry",
|
||||
"BaseGames.Feedback",
|
||||
"Kybernetik.Animancer"
|
||||
"Kybernetik.Animancer",
|
||||
"BaseGames.World",
|
||||
"BaseGames.Skills"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"overrideReferences": false,
|
||||
|
||||
77
Assets/Scripts/Player/States/DashState.cs
Normal file
77
Assets/Scripts/Player/States/DashState.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
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;
|
||||
private int _facingDir;
|
||||
|
||||
public bool CanDash => _cooldownTimer <= 0f;
|
||||
|
||||
/// <summary>冲刺状态期间应视为无敌,PlayerController 据此跳过受击硬直。</summary>
|
||||
public override bool IsInvincible => true;
|
||||
|
||||
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;
|
||||
|
||||
// 无敌帧
|
||||
Stats?.BeginInvincibility(Cfg.DashDuration + 0.05f);
|
||||
|
||||
// 关闭重力,施加冲刺速度
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/DashState.cs.meta
Normal file
11
Assets/Scripts/Player/States/DashState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 907f53feec688524b87e472f34d46ab3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
39
Assets/Scripts/Player/States/DeadState.cs
Normal file
39
Assets/Scripts/Player/States/DeadState.cs
Normal 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() { }
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/DeadState.cs.meta
Normal file
11
Assets/Scripts/Player/States/DeadState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1761709fa9be931418e37b2387899077
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
70
Assets/Scripts/Player/States/DownAttackState.cs
Normal file
70
Assets/Scripts/Player/States/DownAttackState.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
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;
|
||||
|
||||
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;
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
}
|
||||
|
||||
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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/DownAttackState.cs.meta
Normal file
11
Assets/Scripts/Player/States/DownAttackState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35c14ecd9a2db5c48a95c015ac6128fd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -18,7 +18,7 @@ namespace BaseGames.Player.States
|
||||
// 郊狼时间跳跃
|
||||
if (Buffer.ConsumeJump() && Move.HasCoyoteTime)
|
||||
{
|
||||
_owner.TransitionTo(_owner.JumpState);
|
||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,21 +27,21 @@ namespace BaseGames.Player.States
|
||||
{
|
||||
Move.ZeroVelocity();
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
|
||||
_owner.TransitionTo(_owner.RunState);
|
||||
_owner.TransitionTo(_owner.GetState<RunState>());
|
||||
else
|
||||
_owner.TransitionTo(_owner.IdleState);
|
||||
_owner.TransitionTo(_owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// 空中水平移动
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
||||
Move.Move(Input.MoveInput.x * (Cfg != null ? Cfg.RunSpeed : 7f));
|
||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 增强下落重力
|
||||
if (Cfg != null && Move.Rb.velocity.y < 0f)
|
||||
if (Move.Rb.velocity.y < 0f)
|
||||
{
|
||||
float extraGrav = Physics2D.gravity.y * (Cfg.FallGravityMult - 1f) * Time.fixedDeltaTime;
|
||||
Move.Rb.velocity = new Vector2(
|
||||
|
||||
63
Assets/Scripts/Player/States/HurtState.cs
Normal file
63
Assets/Scripts/Player/States/HurtState.cs
Normal 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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/HurtState.cs.meta
Normal file
11
Assets/Scripts/Player/States/HurtState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7192bcf89ed1f5419d40fa182ac66ba
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -12,23 +12,25 @@ namespace BaseGames.Player.States
|
||||
if (AnimCfg?.Idle != null)
|
||||
Anim.Play(AnimCfg.Idle);
|
||||
Move?.ZeroHorizontalVelocity();
|
||||
// 着地时重置空中冲刺次数
|
||||
Owner.GetState<AerialDashState>()?.ResetAerialDashes();
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
if (!Move.IsGrounded)
|
||||
{
|
||||
_owner.TransitionTo(_owner.FallState);
|
||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
if (Buffer.ConsumeJump())
|
||||
{
|
||||
_owner.TransitionTo(_owner.JumpState);
|
||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
|
||||
{
|
||||
_owner.TransitionTo(_owner.RunState);
|
||||
_owner.TransitionTo(_owner.GetState<RunState>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ namespace BaseGames.Player.States
|
||||
// 上升结束时转为下落
|
||||
if (Move.Rb.velocity.y <= 0f)
|
||||
{
|
||||
_owner.TransitionTo(_owner.FallState);
|
||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
// 水平移动
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
||||
Move.Move(Input.MoveInput.x * (Cfg != null ? Cfg.RunSpeed : 7f));
|
||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
|
||||
46
Assets/Scripts/Player/States/ParryState.cs
Normal file
46
Assets/Scripts/Player/States/ParryState.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 弹反预备状态(架构 05_PlayerModule §2,完整实现在 Week 6)。
|
||||
/// 开启 ParrySystem 弹反窗口,播放 ParryStart 动画;
|
||||
/// 成功弹反后 ParrySystem.ConsumeParry() 返回 true,HurtBox 不处理该次伤害。
|
||||
/// </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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/ParryState.cs.meta
Normal file
11
Assets/Scripts/Player/States/ParryState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 29962783977c7e345bd6b2716c59c39b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,112 +1,219 @@
|
||||
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:IsInvincible/Defense 委托 PlayerStats,TakeDamage 委托 _stats。
|
||||
/// 依赖注入:所有子系统通过 [SerializeField] 字段在 Inspector 中绑定。
|
||||
/// 实现 IDamageable + IPoiseSource(架构 06_CombatModule §13)。
|
||||
/// 依赖注入:同节点组件由 RequireComponent + Awake 自动获取;跨节点引用通过 [SerializeField] 绑定。
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-100)]
|
||||
[RequireComponent(typeof(InputBuffer))]
|
||||
public class PlayerController : MonoBehaviour, IDamageable
|
||||
[RequireComponent(typeof(PlayerMovement))]
|
||||
[RequireComponent(typeof(PlayerStats))]
|
||||
[RequireComponent(typeof(AnimancerComponent))]
|
||||
public class PlayerController : MonoBehaviour, IDamageable, IPoiseSource
|
||||
{
|
||||
// ── 移动 & 数值 ───────────────────────────────────────────────────────
|
||||
[Header("核心组件")]
|
||||
[SerializeField] private PlayerMovement _movement;
|
||||
[SerializeField] private PlayerStats _stats;
|
||||
[SerializeField] private AnimancerComponent _animancer;
|
||||
// ── 同节点组件(由 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 PlayerStatsSO _statsConfig; // 数值基准(HP/弹簧等初始化用)
|
||||
[SerializeField] private FormConfigSO _formConfig; // Phase 2:三形态切换参数
|
||||
[SerializeField] private InputReaderSO _inputReader;
|
||||
[SerializeField] private PlayerStatsSO _statsConfig;
|
||||
[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 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;
|
||||
[SerializeField] private IntEventChannelSO _onHPChanged;
|
||||
|
||||
[SerializeField] private IntEventChannelSO _onHPChanged; /// <summary>
|
||||
/// Start() 时广播玩家 Transform(EnemyBase / ProjectileManager 等订阅此频道)。
|
||||
/// 替代每个敌人在 Awake 中独立 FindWithTag 的 O(n) 全场景扫描。
|
||||
/// </summary>
|
||||
[SerializeField] private TransformEventChannelSO _onPlayerSpawned;
|
||||
// ── 运行时 ────────────────────────────────────────────────────────────
|
||||
private InputBuffer _inputBuffer;
|
||||
private bool _missingDependencyLogged;
|
||||
private bool _dependenciesReady;
|
||||
#if UNITY_EDITOR
|
||||
[Header("调试")]
|
||||
[SerializeField] private bool _debugValidateTransitions = true;
|
||||
#endif
|
||||
|
||||
// Overlay Layer(Layer 1):供 SpringState / SoulSkill 等叠加层动画使用
|
||||
// Base Layer(Layer 0):移动/攻击/受伤/死亡等全身状态动画
|
||||
private AnimancerLayer _overlayLayer;
|
||||
|
||||
// ── 状态实例 ──────────────────────────────────────────────────────────
|
||||
private PlayerStateBase _currentState;
|
||||
|
||||
private IdleState _idleState;
|
||||
private RunState _runState;
|
||||
private JumpState _jumpState;
|
||||
private FallState _fallState;
|
||||
private AttackState _attackState;
|
||||
private readonly System.Collections.Generic.Dictionary<System.Type, PlayerStateBase> _states = new();
|
||||
|
||||
// ── IDamageable 实现 ──────────────────────────────────────────────────
|
||||
public bool IsInvincible => _stats != null && _stats.IsInvincible;
|
||||
public int Defense => 0; // Phase 2:从 PlayerStatsSO 读取
|
||||
public int Defense => 0;
|
||||
|
||||
public void TakeDamage(DamageInfo info)
|
||||
{
|
||||
if (_stats == null) return;
|
||||
_stats.TakeDamage(info.FinalDamage);
|
||||
// Phase 2:若非 DashState,切换 HurtState
|
||||
|
||||
// 当前状态标记为无敌(如冲刺)则跳过受击硬直
|
||||
if (_currentState?.IsInvincible == true) return;
|
||||
|
||||
if (_stats.IsAlive)
|
||||
{
|
||||
GetState<HurtState>()?.Initialize(info);
|
||||
TransitionTo(GetState<HurtState>());
|
||||
}
|
||||
else
|
||||
{
|
||||
TransitionTo(GetState<DeadState>());
|
||||
_onPlayerDied?.Raise();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 公开属性(供状态类访问)──────────────────────────────────────────
|
||||
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;
|
||||
// ── IPoiseSource 实现(架构 06_CombatModule §13)─────────────────────
|
||||
/// <summary>
|
||||
/// 玩家不拥有霸体,始终返回 <see cref="PoiseLevel.None"/>。
|
||||
/// 设计决策:类似 Hollow Knight,玩家依靠走位和弹反规避伤害,而非硬吃。
|
||||
/// 若未来需要临时霸体(如特定技能动作),请通过独立的覆盖标记实现,
|
||||
/// 而非在此处引入状态,以保持接口语义清晰。
|
||||
/// </summary>
|
||||
public PoiseLevel GetCurrentPoiseLevel() => PoiseLevel.None;
|
||||
|
||||
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 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;
|
||||
|
||||
// ── Overlay Layer API(供 SpringState / SoulSkill 等叠加动画使用)─────
|
||||
/// <summary>
|
||||
/// 在 Overlay Layer(Layer 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()
|
||||
{
|
||||
_inputBuffer = GetComponent<InputBuffer>();
|
||||
Debug.Assert(_movementConfig != null, "[PlayerController] _movementConfig 未赋值,请在 Inspector 中指定 PlayerMovementConfigSO。", this);
|
||||
ResolveDependencies();
|
||||
|
||||
// 注入 HurtBox 依赖(Phase 1:只注入护盾;弹反/霸体 Phase 2)
|
||||
if (_hurtBox != null && _shield != null)
|
||||
_hurtBox.SetShieldable(_shield);
|
||||
// 初始化 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);
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
TransitionTo(_idleState);
|
||||
if (!HasRequiredStateDependencies())
|
||||
return;
|
||||
|
||||
// 广播玩家 Transform:EnemyBase / ProjectileManager 等订阅者将通过事件接收引用
|
||||
// (必须在 Start 中调用,确保所有 Awake/OnEnable 订阅已就绪)
|
||||
_onPlayerSpawned?.Raise(transform);
|
||||
|
||||
TransitionTo(GetState<IdleState>());
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!HasRequiredStateDependencies())
|
||||
return;
|
||||
|
||||
// 冲刺冷却计时
|
||||
GetState<DashState>()?.TickCooldown(Time.deltaTime);
|
||||
|
||||
_currentState?.OnStateUpdate();
|
||||
}
|
||||
|
||||
@@ -123,29 +230,89 @@ namespace BaseGames.Player.States
|
||||
// ── 状态机 ────────────────────────────────────────────────────────────
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>尝试切换状态(供状态内部的条件转换使用)。</summary>
|
||||
public void TryTransitionState(PlayerStateBase newState)
|
||||
=> TransitionTo(newState);
|
||||
|
||||
private void InitializeStates()
|
||||
{
|
||||
_idleState = new IdleState(this);
|
||||
_runState = new RunState(this);
|
||||
_jumpState = new JumpState(this);
|
||||
_fallState = new FallState(this);
|
||||
_attackState = new AttackState(this);
|
||||
_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 必须在 Inspector 中赋值(SerializeField)
|
||||
// 已移除 Resources.FindObjectsOfTypeAll 全资产扫描回退
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
// ── 状态访问器 ────────────────────────────────────────────────────────
|
||||
public IdleState IdleState => _idleState;
|
||||
public RunState RunState => _runState;
|
||||
public JumpState JumpState => _jumpState;
|
||||
public FallState FallState => _fallState;
|
||||
public AttackState AttackState => _attackState;
|
||||
// 使用 GetState<T>() 按类型获取任意状态实例,不再暴露具名属性。
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
executionOrder: -100
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
|
||||
@@ -18,6 +18,22 @@ namespace BaseGames.Player.States
|
||||
public virtual void OnStateUpdate() { }
|
||||
public virtual void OnStateFixedUpdate() { }
|
||||
public virtual void OnStateExit() { }
|
||||
public virtual PlayerStateBase GetNextState() => null;
|
||||
|
||||
/// <summary>
|
||||
/// 此状态期间是否应视为无敌(忽略伤害)。
|
||||
/// 冲刺等状态 override 为 true,PlayerController.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;
|
||||
|
||||
@@ -17,26 +17,26 @@ namespace BaseGames.Player.States
|
||||
{
|
||||
if (!Move.IsGrounded)
|
||||
{
|
||||
_owner.TransitionTo(_owner.FallState);
|
||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
if (Buffer.ConsumeJump())
|
||||
{
|
||||
_owner.TransitionTo(_owner.JumpState);
|
||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
if (Mathf.Abs(Input.MoveInput.x) < 0.1f)
|
||||
{
|
||||
_owner.TransitionTo(_owner.IdleState);
|
||||
_owner.TransitionTo(_owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
|
||||
Move.Move(Input.MoveInput.x * (Cfg != null ? Cfg.RunSpeed : 7f));
|
||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
Move.Move(Input.MoveInput.x * (Cfg != null ? Cfg.RunSpeed : 7f));
|
||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
55
Assets/Scripts/Player/States/SpringState.cs
Normal file
55
Assets/Scripts/Player/States/SpringState.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用灵泉(治疗)状态(架构 05_PlayerModule §2)。
|
||||
/// 消耗 SpringCharge,播放治愈动画;动画结束后回到 Idle。
|
||||
/// 需在地面且有充能才能进入(PlayerController 负责条件检查)。
|
||||
///
|
||||
/// 动画在 Overlay Layer(Layer 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 Layer(Layer 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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/SpringState.cs.meta
Normal file
11
Assets/Scripts/Player/States/SpringState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0a4d3517cee2b24da050f85ffe13fdd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
85
Assets/Scripts/Player/States/SwimState.cs
Normal file
85
Assets/Scripts/Player/States/SwimState.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/SwimState.cs.meta
Normal file
11
Assets/Scripts/Player/States/SwimState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 199e23e0415d2d04ebc19174f11f3231
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
47
Assets/Scripts/Player/States/UpAttackState.cs
Normal file
47
Assets/Scripts/Player/States/UpAttackState.cs
Normal 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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/UpAttackState.cs.meta
Normal file
11
Assets/Scripts/Player/States/UpAttackState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3917a3a7a101db249ab296e784afea24
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
58
Assets/Scripts/Player/States/WallJumpState.cs
Normal file
58
Assets/Scripts/Player/States/WallJumpState.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/WallJumpState.cs.meta
Normal file
11
Assets/Scripts/Player/States/WallJumpState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 351b042e66111474d8b152878313dc92
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
55
Assets/Scripts/Player/States/WallSlideState.cs
Normal file
55
Assets/Scripts/Player/States/WallSlideState.cs
Normal 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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/Scripts/Player/States/WallSlideState.cs.meta
Normal file
11
Assets/Scripts/Player/States/WallSlideState.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7578dea22a1b02d44970bcac667b1401
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user