角色能力,存档
This commit is contained in:
@@ -1,89 +0,0 @@
|
||||
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;
|
||||
|
||||
// 无敌帧:与地面冲刺共享同一无敌 CD(DashState._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);
|
||||
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)
|
||||
{
|
||||
Move?.SetGravityScale(Cfg.DefaultGravityScale);
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// 撞墙立即终止冲刺(碰到实体墙立即中止,避免压墙卡住)
|
||||
var wd = Owner.WallDetector;
|
||||
if (wd != null && wd.IsTouchingWall && wd.WallDirection == _facingDir)
|
||||
{
|
||||
Move?.SetGravityScale(Cfg.DefaultGravityScale);
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Move?.SetGravityScale(Cfg.DefaultGravityScale);
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 冲刺期间保持锁定方向速度(与 DashState 一致,使用 _facingDir)
|
||||
if (_timer > 0f)
|
||||
Move?.Dash(new Vector2(_facingDir, 0f), Cfg.DashSpeed);
|
||||
}
|
||||
|
||||
/// <summary>着地时重置空中冲刺次数(由 PlayerController 在着地时调用)。</summary>
|
||||
public void ResetAerialDashes()
|
||||
{
|
||||
_aerialDashesLeft = Cfg.MaxAerialDashes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38830e1649a9eb548b20540a737bdf09
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,34 +1,38 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 空中攻击状态(架构 05_PlayerModule §2)。
|
||||
/// 由 FallState / JumpState 接收攻击输入后转入;
|
||||
/// Animancer 帧事件驱动 HitBoxAir 激活;结束后回到 FallState。
|
||||
/// HitBox 时间由 ComboStepConfig.hitBoxEnter/Exit 驱动;结束后按 recoveryTime 延迟取消窗口。
|
||||
/// </summary>
|
||||
public class AirAttackState : PlayerStateBase
|
||||
{
|
||||
private float _recoveryEndTime;
|
||||
|
||||
public AirAttackState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
Owner.Combat?.SetComboSegmentSource(0);
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Air);
|
||||
var step = Owner.Weapon?.ActiveWeapon?.GetDirStep(AttackDirection.Air);
|
||||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||||
|
||||
var clip = Owner.Weapon?.ActiveWeapon?.airAttackClip;
|
||||
if (clip != null && clip.Clip != null)
|
||||
if (step?.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;
|
||||
var animState = Anim.Play(step.Value.clip);
|
||||
animState.Speed *= spd;
|
||||
|
||||
var events = animState.Events(this);
|
||||
events.OnEnd = OnClipEnd;
|
||||
|
||||
var s = step.Value;
|
||||
events.Add(s.hitBoxEnter, () => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Air, s.hitBoxId));
|
||||
events.Add(s.hitBoxExit, () => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||||
}
|
||||
else
|
||||
{
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Air);
|
||||
OnClipEnd();
|
||||
}
|
||||
}
|
||||
@@ -36,10 +40,24 @@ namespace BaseGames.Player.States
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
Move?.SetCancelWindowOpen(false);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
if (Time.time >= _recoveryEndTime)
|
||||
Move.SetCancelWindowOpen(true);
|
||||
}
|
||||
|
||||
private void OnClipEnd()
|
||||
{
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
var step = Owner.Weapon?.ActiveWeapon?.GetDirStep(AttackDirection.Air);
|
||||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||||
float recovery = (step?.recoveryTime ?? 0.05f) / spd;
|
||||
_recoveryEndTime = Time.time + recovery;
|
||||
Move.SetCancelWindowOpen(false);
|
||||
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +1,193 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 地面攻击状态(3 段连击)。
|
||||
/// 由 PlayerController 实例化,AttackEvent 触发切换。
|
||||
/// 通过 Animancer 帧事件驱动 HitBox 激活/关闭。
|
||||
/// 地面攻击状态(任意段连击)。
|
||||
/// 攻速倍率(Stats.AnimatorSpeedMultiplier)缩放 clip 播放速度及 recoveryTime/comboTimeout。
|
||||
/// 状态机流程:
|
||||
/// 播放动画 → [comboInputOpen] 接受输入 → [cancelWindowOpen] 允许跳跃/冲刺 →
|
||||
/// 动画结束 → 硬直(recoveryTime) → 连击等待(comboTimeout) → 无输入则回 Idle
|
||||
/// </summary>
|
||||
public class AttackState : PlayerStateBase
|
||||
{
|
||||
private int _comboIndex;
|
||||
private int _comboIndex;
|
||||
private bool _comboInputPending; // 连击窗口内已收到攻击输入
|
||||
private bool _comboWindowOpen; // 当前是否接受连击输入
|
||||
|
||||
// 动画结束后的两阶段计时
|
||||
private bool _waitingAfterAnim; // 是否在动画结束后等待阶段
|
||||
private float _recoveryEndTime; // 硬直结束时刻
|
||||
private float _comboTimeoutEnd; // 连击等待结束时刻
|
||||
|
||||
public AttackState(PlayerController owner) : base(owner) { }
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
_comboIndex = 0;
|
||||
_comboIndex = 0;
|
||||
_comboInputPending = false;
|
||||
_comboWindowOpen = false;
|
||||
_waitingAfterAnim = false;
|
||||
Input.AttackEvent += OnAttackInput;
|
||||
PlayAttackClip();
|
||||
Input.AttackEvent += OnAttackInput;
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
Input.AttackEvent -= OnAttackInput;
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
Move?.SetCancelWindowOpen(false);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
if (!_waitingAfterAnim)
|
||||
{
|
||||
// ── 动画播放中:只处理取消窗口(跳跃/冲刺打断)──────────────
|
||||
if (!Move.CancelWindowOpen) return;
|
||||
TryConsumeCancelInput();
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 动画结束后等待阶段 ─────────────────────────────────────────
|
||||
float now = Time.time;
|
||||
|
||||
// 硬直结束后开放取消窗口
|
||||
if (!Move.CancelWindowOpen && now >= _recoveryEndTime)
|
||||
Move.SetCancelWindowOpen(true);
|
||||
|
||||
// 有缓存的连击输入 → 立即推进
|
||||
if (_comboInputPending)
|
||||
{
|
||||
AdvanceCombo();
|
||||
return;
|
||||
}
|
||||
|
||||
// 连击超时 → 返回 Idle
|
||||
if (now >= _comboTimeoutEnd)
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// 恢复期结束后仍可跳跃/冲刺取消
|
||||
if (Move.CancelWindowOpen)
|
||||
TryConsumeCancelInput();
|
||||
}
|
||||
|
||||
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);
|
||||
_waitingAfterAnim = false;
|
||||
_comboWindowOpen = false;
|
||||
Move.SetCancelWindowOpen(false);
|
||||
|
||||
var weapon = Owner.Weapon?.ActiveWeapon;
|
||||
if (weapon == null)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning("[AttackState] 未找到 ActiveWeapon,请检查 WeaponManager 配置。");
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
|
||||
var step = weapon.GetGroundStep(_comboIndex);
|
||||
if (step.clip == null || step.clip.Clip == null)
|
||||
{
|
||||
UnityEngine.Debug.LogWarning($"[AttackState] 连击段 {_comboIndex} 动画未配置,请检查 {weapon.weaponId}。");
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
|
||||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||||
var animState = Anim.Play(step.clip);
|
||||
animState.Speed *= spd; // 在 ClipTransition 自身速度基础上叠加攻速
|
||||
|
||||
var events = animState.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());
|
||||
// HitBox 时间窗口(capture step by value for closure safety)
|
||||
var capturedStep = step;
|
||||
events.Add(capturedStep.hitBoxEnter, () =>
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Ground,
|
||||
capturedStep.hitBoxId, capturedStep.damageSource));
|
||||
events.Add(capturedStep.hitBoxExit, () => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||||
// 连击输入窗口
|
||||
if (capturedStep.comboInputOpen > 0f)
|
||||
events.Add(capturedStep.comboInputOpen, () => _comboWindowOpen = true);
|
||||
else
|
||||
_comboWindowOpen = true; // 0 = 立即开放
|
||||
|
||||
if (capturedStep.comboInputClose > 0f)
|
||||
events.Add(capturedStep.comboInputClose, () => _comboWindowOpen = false);
|
||||
|
||||
// 取消窗口(跳跃/冲刺)
|
||||
if (capturedStep.cancelWindowOpen > 0f)
|
||||
events.Add(capturedStep.cancelWindowOpen, () => Move.SetCancelWindowOpen(true));
|
||||
}
|
||||
|
||||
private void OnClipEnd()
|
||||
{
|
||||
Owner.Combat?.DisableAllWeaponHitBoxes();
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
_comboWindowOpen = false;
|
||||
Move.SetCancelWindowOpen(false);
|
||||
|
||||
// 如果已有缓存输入,直接推进(零延迟连击)
|
||||
if (_comboInputPending)
|
||||
{
|
||||
AdvanceCombo();
|
||||
return;
|
||||
}
|
||||
|
||||
// 进入动画后等待阶段
|
||||
var step = Owner.Weapon?.ActiveWeapon?.GetGroundStep(_comboIndex) ?? default;
|
||||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||||
float now = Time.time;
|
||||
|
||||
_waitingAfterAnim = true;
|
||||
_recoveryEndTime = now + step.recoveryTime / spd;
|
||||
_comboTimeoutEnd = _recoveryEndTime + step.comboTimeout / spd;
|
||||
}
|
||||
|
||||
private void AdvanceCombo()
|
||||
{
|
||||
int maxCombo = Owner.Weapon?.ActiveWeapon?.GroundComboCount ?? 1;
|
||||
if (_comboIndex < maxCombo - 1)
|
||||
{
|
||||
_comboIndex++;
|
||||
_comboInputPending = false;
|
||||
PlayAttackClip();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 已是最后一段,忽略多余输入,等待超时
|
||||
_comboInputPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryConsumeCancelInput()
|
||||
{
|
||||
if (Buffer.ConsumeJump())
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
var ds = Owner.GetState<DashState>();
|
||||
if (ds != null && ds.CanDash
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||||
&& Buffer.ConsumeDash())
|
||||
{
|
||||
Owner.TransitionTo(ds);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAttackInput()
|
||||
{
|
||||
// 连击段数从配置 SO 动态读取,无须修改代码即可支持任意段数连击
|
||||
int maxCombo = AnimCfg?.GroundAttacks?.Length ?? 1;
|
||||
if (_comboIndex < maxCombo - 1)
|
||||
{
|
||||
_comboIndex++;
|
||||
PlayAttackClip();
|
||||
}
|
||||
if (_comboWindowOpen || _waitingAfterAnim)
|
||||
_comboInputPending = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,15 @@ namespace BaseGames.Player.States
|
||||
private float _invincibilityCooldownTimer;
|
||||
private int _facingDir;
|
||||
|
||||
public bool CanDash => _cooldownTimer <= 0f;
|
||||
/// <summary>本次离地后是否已消耗过一次空中冲刺。落地或下劈命中(Pogo)时重置。</summary>
|
||||
private bool _airDashUsed;
|
||||
|
||||
public bool CanDash => _cooldownTimer <= 0f;
|
||||
/// <summary>空中冲刺可用条件:冷却就绪 且 本次离地内尚未冲刺过。</summary>
|
||||
public bool CanAirDash => _cooldownTimer <= 0f && !_airDashUsed;
|
||||
|
||||
/// <summary>重置空中冲刺次数(落地或 Pogo 时由 IdleState/RunState/DownAttackState 调用)。</summary>
|
||||
public void ResetAirDash() => _airDashUsed = false;
|
||||
|
||||
/// <summary>
|
||||
/// 无敌帧是否已冷却,即本次冲刺可以获得无敌。
|
||||
@@ -44,11 +52,20 @@ namespace BaseGames.Player.States
|
||||
_facingDir = Owner.FacingDirection;
|
||||
_timer = Cfg.DashDuration;
|
||||
|
||||
// 空中冲刺:记录本次离地已使用冲刺(地面冲刺不消耗,仅空中限制一次)
|
||||
if (!Move.IsGrounded)
|
||||
_airDashUsed = true;
|
||||
|
||||
// 无敌帧:
|
||||
// 条件 1:已解锁 InvincibleDash
|
||||
// 条件 2:无敌冷却已就绪(防止 spam 冲刺连序无敌)
|
||||
// 窗口时长 = DashInvincibilityDuration < DashDuration,冲刺后段无保护
|
||||
if (Stats != null && Stats.HasAbility(AbilityType.InvincibleDash) && CanGrantInvincibility)
|
||||
// 在设置冷却计时器前捕获,供后续动画选择使用
|
||||
bool isInvincibleDash = Stats != null
|
||||
&& Stats.HasAbility(AbilityType.InvincibleDash)
|
||||
&& CanGrantInvincibility;
|
||||
|
||||
if (isInvincibleDash)
|
||||
{
|
||||
Stats.BeginInvincibility(Cfg.DashInvincibilityDuration);
|
||||
_invincibilityCooldownTimer = Cfg.DashInvincibilityCooldown;
|
||||
@@ -58,8 +75,11 @@ namespace BaseGames.Player.States
|
||||
Move?.SetGravityScale(0f);
|
||||
Move?.Dash(new Vector2(_facingDir, 0f), Cfg.DashSpeed);
|
||||
|
||||
// 播放冲刺动画
|
||||
if (AnimCfg?.Dash != null) Anim?.Play(AnimCfg.Dash);
|
||||
// 播放冲刺动画:无敌冲刺使用专属 Clip,留空时回退到普通冲刺 Clip
|
||||
var dashClip = (isInvincibleDash && AnimCfg?.DashInvincible != null)
|
||||
? AnimCfg.DashInvincible
|
||||
: AnimCfg?.Dash;
|
||||
if (dashClip != null) Anim?.Play(dashClip);
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
@@ -71,10 +91,19 @@ namespace BaseGames.Player.States
|
||||
return;
|
||||
}
|
||||
|
||||
// 撞墙立即终止冲刺(碰到实体墙立即中止,避免压墙卡住)
|
||||
var wd = Owner.WallDetector;
|
||||
if (wd != null && wd.IsTouchingWall && wd.WallDirection == _facingDir)
|
||||
EndDash();
|
||||
// 跳跃可取消冲刺:冲刺期间按跳跃立即中断并起跳。
|
||||
// 空中冲刺时若有剩余空中跳跃次数,消耗一次并使用二段跳力度。
|
||||
if (Buffer.ConsumeJump())
|
||||
{
|
||||
if (!Move.IsGrounded && Owner.AirJumpsLeft > 0)
|
||||
{
|
||||
Owner.UseAirJump();
|
||||
Owner.GetState<JumpState>()?.SetDoubleJump(true);
|
||||
}
|
||||
Owner.TransitionTo(Owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
// 注:碰墙时不中止冲刺,完成完整冲刺时长(物理阻止位移,但计时继续)
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
@@ -93,6 +122,8 @@ namespace BaseGames.Player.States
|
||||
|
||||
private void EndDash()
|
||||
{
|
||||
// 双轴速度归零,防止冲刺结束时角色带着 DashSpeed 冲出平台边缘后继续向前飞行。
|
||||
Move?.ZeroVelocity();
|
||||
if (Move != null && Move.IsGrounded)
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
else
|
||||
|
||||
@@ -23,21 +23,22 @@ namespace BaseGames.Player.States
|
||||
if (Owner.Combat != null)
|
||||
Owner.Combat.OnDownHitConfirmed += OnDownHitConfirmed;
|
||||
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Down);
|
||||
var step = Owner.Weapon?.ActiveWeapon?.GetDirStep(AttackDirection.Down);
|
||||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||||
|
||||
var clip = Owner.Weapon?.ActiveWeapon?.downAttackClip;
|
||||
if (clip != null && clip.Clip != null)
|
||||
if (step?.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;
|
||||
var animState = Anim.Play(step.Value.clip);
|
||||
animState.Speed *= spd;
|
||||
var events = animState.Events(this);
|
||||
events.OnEnd = OnClipEnd;
|
||||
var s = step.Value;
|
||||
events.Add(s.hitBoxEnter, () => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Down, s.hitBoxId));
|
||||
events.Add(step.Value.hitBoxExit, () => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||||
}
|
||||
else
|
||||
{
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Down);
|
||||
OnClipEnd();
|
||||
}
|
||||
|
||||
@@ -58,7 +59,9 @@ namespace BaseGames.Player.States
|
||||
{
|
||||
if (_hasHitEnemy) return;
|
||||
_hasHitEnemy = true;
|
||||
// Pogo 弹跳:命中敌人后向上弹起
|
||||
// Pogo 弹跳:命中敌人后向上弹起,同时重置空中能力(等同落地效果)
|
||||
Owner.ResetAirJumps();
|
||||
Owner.GetState<DashState>()?.ResetAirDash();
|
||||
Move.Jump();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ namespace BaseGames.Player.States
|
||||
/// <summary>
|
||||
/// 下落状态。
|
||||
/// - 郊狼跳:CoyoteTimer > 0 时按跳跃 → 一段跳(JumpState,使用 JumpForce)。
|
||||
/// - 二段跳:CoyoteTimer 耗尽后按跳跃且 AirJumpsLeft > 0 → JumpState(使用 DoubleJumpForce)。
|
||||
/// - 空中冲刺:HasAbility(AirDash) && HasAerialDash → AerialDashState。
|
||||
/// - 空中跳跃:CoyoteTimer 耗尽后按跳跃且 AirJumpsLeft > 0 → JumpState(使用 DoubleJumpForce)。
|
||||
/// - 冲刺:HasAbility(Dash) && DashState.CanAirDash → DashState(地面与空中统一,空中限一次)。
|
||||
/// - 抓墙:贴墙时按下朝向墙壁的方向键 → WallSlideState。
|
||||
/// - 增强下落重力(FallGravityMult)确保下落快于上升,手感紧实。
|
||||
/// </summary>
|
||||
public class FallState : PlayerStateBase
|
||||
@@ -22,7 +23,8 @@ namespace BaseGames.Player.States
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
// ── 跳跃输入(郊狼跳 / 二段跳)────────────────────────────────────
|
||||
if (Buffer.ConsumeJump())
|
||||
// 先确认有可用跳跃机会,再消耗缓冲,避免无操作时静默吃掉输入
|
||||
if ((Move.HasCoyoteTime || Owner.AirJumpsLeft > 0) && Buffer.ConsumeJump())
|
||||
{
|
||||
if (Move.HasCoyoteTime)
|
||||
{
|
||||
@@ -30,30 +32,40 @@ namespace BaseGames.Player.States
|
||||
_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;
|
||||
}
|
||||
// 无跳跃机会:输入已消耗,静默忽略(无可用跳跃机会时静默消耗输入缓冲)
|
||||
// 二段跳:通过 SetDoubleJump 标记 JumpState 使用 DoubleJumpForce
|
||||
Owner.UseAirJump();
|
||||
Owner.GetState<JumpState>()?.SetDoubleJump(true);
|
||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 空中冲刺────────────────────────────────────────────────────────
|
||||
if (Buffer.ConsumeDash()
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.AirDash))
|
||||
// ── 冲刺(地面/空中统一使用 DashState)────────────────────────────
|
||||
// 先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入
|
||||
var dashState = Owner.GetState<DashState>();
|
||||
if (dashState != null && dashState.CanAirDash
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||||
&& Buffer.ConsumeDash())
|
||||
{
|
||||
var aerialDash = Owner.GetState<AerialDashState>();
|
||||
if (aerialDash != null && aerialDash.HasAerialDash)
|
||||
{
|
||||
_owner.TransitionTo(aerialDash);
|
||||
return;
|
||||
}
|
||||
_owner.TransitionTo(dashState);
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 着地──────────────────────────────────────────────────────────
|
||||
// ── 空中攻击:Move Y > 0 → 上劈;Y < -0.5 且解锁下劈 → 下劈;其余 → 空中攻击 ──
|
||||
if (Buffer.ConsumeAttack())
|
||||
{
|
||||
if (Input.MoveInput.y > 0.5f)
|
||||
_owner.TransitionTo(_owner.GetState<UpAttackState>());
|
||||
else if (Input.MoveInput.y < -0.5f
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.DownSlash))
|
||||
_owner.TransitionTo(_owner.GetState<DownAttackState>());
|
||||
else
|
||||
_owner.TransitionTo(_owner.GetState<AirAttackState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 着地回退(主逻辑已移至 OnStateFixedUpdate,此处仅作极端情况保险)────────────
|
||||
// 正常情况下状态在 FixedUpdate 中已转换,Update 执行时 _currentState 已是 IdleState/RunState,
|
||||
// 此段不会被执行。仅在初始帧等 FixedUpdate 尚未运行时作补充保障。
|
||||
if (Move.IsGrounded)
|
||||
{
|
||||
Move.ZeroVelocity();
|
||||
@@ -68,13 +80,55 @@ namespace BaseGames.Player.States
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 空中水平移动:有输入时立即覆盖至目标速度;无输入时施加空气阻力保留动量
|
||||
// ── 着地检测(在 FixedUpdate 内与 CheckGrounded 同帧执行)────────────────────
|
||||
// PlayerMovement.FixedUpdate(-200) 先于 PlayerController.FixedUpdate(-100) 执行,
|
||||
// 此处读到的 IsGrounded 已是本物理帧最新结果,不存在跨帧读到过期值的问题。
|
||||
// 落地检测放在 OnStateUpdate(Update 阶段)时,若低帧率导致同帧内多次 FixedUpdate,
|
||||
// 最后一次 FixedUpdate 的 depenetration 弹回可能使 _isGrounded 在 Update 时为 false,
|
||||
// 进而导致无法着地。
|
||||
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);
|
||||
{
|
||||
int inputDir = Input.MoveInput.x > 0 ? 1 : -1;
|
||||
var wd = Owner.WallDetector;
|
||||
if (wd != null && wd.IsTouchingWall && wd.WallDirection == inputDir)
|
||||
{
|
||||
// 按下朝墙方向键 + 在墙上(且未著地)→ 进入抓墙状态
|
||||
var wss = Owner.GetState<WallSlideState>();
|
||||
if (wss != null && !Move.IsGrounded)
|
||||
{
|
||||
wss.PrepareEnter(inputDir);
|
||||
Owner.TransitionTo(wss);
|
||||
return;
|
||||
}
|
||||
Move.ZeroHorizontalVelocity();
|
||||
}
|
||||
else if (wd != null && wd.HasPartialContact(inputDir))
|
||||
{
|
||||
// 仅部分射线命中(如检测点高于矮墙顶部),停止施加朝墙方向的水平速度,
|
||||
// 防止角色边角被卡在墙顶而无法继续下落。
|
||||
Move.ZeroHorizontalVelocity();
|
||||
}
|
||||
else
|
||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
||||
}
|
||||
else
|
||||
Move.ApplyAirDrag(Cfg.AirDragFactor);
|
||||
Move.ZeroHorizontalVelocity();
|
||||
|
||||
// 增强下落重力(FallGravityMult:下落比上升更快,手感更紧实)
|
||||
// 着地时已由上方提前 return,此处无需额外判断 IsGrounded,
|
||||
// 避免持续下压速度与 depenetration 形成振荡导致 IsGrounded 持续为 false
|
||||
if (Move.Rb.velocity.y < 0f)
|
||||
{
|
||||
float extraGrav = Physics2D.gravity.y * (Cfg.FallGravityMult - 1f) * Time.fixedDeltaTime;
|
||||
|
||||
@@ -13,8 +13,9 @@ namespace BaseGames.Player.States
|
||||
Anim.Play(AnimCfg.Idle);
|
||||
Move?.ZeroHorizontalVelocity();
|
||||
// 落地时重置空中能力计数器
|
||||
Owner.GetState<AerialDashState>()?.ResetAerialDashes();
|
||||
Owner.ResetAirJumps();
|
||||
Owner.GetState<DashState>()?.ResetAirDash();
|
||||
Owner.GetState<WallSlideState>()?.ResetWallGrab();
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
@@ -29,12 +30,22 @@ namespace BaseGames.Player.States
|
||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
// 地面冲刺:需解锁 Dash 能力,且冲刺不在冷却
|
||||
if (Buffer.ConsumeDash()
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||||
&& Owner.GetState<DashState>() is { CanDash: true } dashState)
|
||||
// 地面攻击:Move Y > 0 → 上劈;其余 → 普通连击
|
||||
if (Buffer.ConsumeAttack())
|
||||
{
|
||||
_owner.TransitionTo(dashState);
|
||||
if (Input.MoveInput.y > 0.5f)
|
||||
_owner.TransitionTo(_owner.GetState<UpAttackState>());
|
||||
else
|
||||
_owner.TransitionTo(_owner.GetState<AttackState>());
|
||||
return;
|
||||
}
|
||||
// 地面冲刺:先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入
|
||||
var ds = Owner.GetState<DashState>();
|
||||
if (ds != null && ds.CanDash
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||||
&& Buffer.ConsumeDash())
|
||||
{
|
||||
_owner.TransitionTo(ds);
|
||||
return;
|
||||
}
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.1f)
|
||||
@@ -42,5 +53,14 @@ namespace BaseGames.Player.States
|
||||
_owner.TransitionTo(_owner.GetState<RunState>());
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 离地检测(与 CheckGrounded 同帧在 FixedUpdate 中执行,立即响应离地事件)
|
||||
// OnStateUpdate 中仍保留同样的检测作为跨帧补充,两者不会发生双重转换:
|
||||
// 本帧若在 FixedUpdate 中转换到 FallState,Update 调用的将是 FallState.OnStateUpdate()。
|
||||
if (!Move.IsGrounded)
|
||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,12 @@ namespace BaseGames.Player.States
|
||||
Anim.Play(AnimCfg.Jump);
|
||||
|
||||
if (_isDoubleJump)
|
||||
{
|
||||
Move.DoubleJump();
|
||||
// 优先播放专属空中跳跃动画,未配置时回退到普通跳跃动画
|
||||
var airJumpClip = AnimCfg?.AirJump ?? AnimCfg?.Jump;
|
||||
if (airJumpClip != null) Anim?.Play(airJumpClip);
|
||||
}
|
||||
else
|
||||
Move.Jump();
|
||||
|
||||
@@ -47,40 +52,88 @@ namespace BaseGames.Player.States
|
||||
return;
|
||||
}
|
||||
|
||||
// 空中冲刺(优先于二段跳:冲刺可保存二段跳机会)
|
||||
if (Buffer.ConsumeDash()
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.AirDash))
|
||||
// 冲刺(地面/空中统一使用 DashState,空中限一次,优先于二段跳:冲刺可保存二段跳机会)
|
||||
// 先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入
|
||||
var dashState = Owner.GetState<DashState>();
|
||||
if (dashState != null && dashState.CanAirDash
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||||
&& Buffer.ConsumeDash())
|
||||
{
|
||||
var aerialDash = Owner.GetState<AerialDashState>();
|
||||
if (aerialDash != null && aerialDash.HasAerialDash)
|
||||
{
|
||||
_owner.TransitionTo(aerialDash);
|
||||
return;
|
||||
}
|
||||
_owner.TransitionTo(dashState);
|
||||
return;
|
||||
}
|
||||
|
||||
// 二段跳:上升阶段即可触发(上升途中任意时刻可二段跳)
|
||||
if (Buffer.ConsumeJump() && Owner.AirJumpsLeft > 0)
|
||||
// 二段跳:上升阶段即可触发(先确认有次数再消耗缓冲,避免静默消耗)
|
||||
if (Owner.AirJumpsLeft > 0 && Buffer.ConsumeJump())
|
||||
{
|
||||
Owner.UseAirJump();
|
||||
Move.DoubleJump();
|
||||
// 留在 JumpState:速度截断(JumpCancelledEvent)和落地检测逻辑继续生效
|
||||
if (AnimCfg?.Jump != null) Anim?.Play(AnimCfg.Jump);
|
||||
// 播放空中跳跃动画,未配置时回退到 Jump
|
||||
var airJumpClip = AnimCfg?.AirJump ?? AnimCfg?.Jump;
|
||||
if (airJumpClip != null) Anim?.Play(airJumpClip);
|
||||
return;
|
||||
}
|
||||
// 空中攻击:Move Y > 0 → 上劈;Y < -0.5 且解锁下劈 → 下劈;其余 → 空中攻击
|
||||
if (Buffer.ConsumeAttack())
|
||||
{
|
||||
if (Input.MoveInput.y > 0.5f)
|
||||
_owner.TransitionTo(_owner.GetState<UpAttackState>());
|
||||
else if (Input.MoveInput.y < -0.5f
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.DownSlash))
|
||||
_owner.TransitionTo(_owner.GetState<DownAttackState>());
|
||||
else
|
||||
_owner.TransitionTo(_owner.GetState<AirAttackState>());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 空中水平移动:有输入时立即覆盖至目标速度;无输入时施加空气阻力保留动量
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
||||
// ── 顶点悬停:第一判断 |vy|,动态切换重力缩放系数 ────────────────
|
||||
// |垂直速度| 低于顶点阈值时,重力缩减至 ApexGravityMultiplier 倍,
|
||||
// 产生角色在跳跃顶点起起“滒空”的手感,属于高重力平台游戏的标志性特征。
|
||||
// 转入 FallState 时 OnStateExit 会恢复默认重力,不会漏到 FallState。
|
||||
float absVY = Mathf.Abs(Move.Rb.velocity.y);
|
||||
if (absVY < Cfg.ApexThreshold)
|
||||
Move.SetGravityScale(Cfg.DefaultGravityScale * Cfg.ApexGravityMultiplier);
|
||||
else
|
||||
Move.ApplyAirDrag(Cfg.AirDragFactor);
|
||||
Move.SetGravityScale(Cfg.DefaultGravityScale);
|
||||
|
||||
// ── 空中水平移动(朝向墙壁时停止施力,防止贴墙悬停)──────────────
|
||||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
||||
{
|
||||
int inputDir = Input.MoveInput.x > 0 ? 1 : -1;
|
||||
var wd = Owner.WallDetector;
|
||||
if (wd != null && wd.IsTouchingWall && wd.WallDirection == inputDir)
|
||||
{
|
||||
// 按下朝墙方向键 + 在墙上(且未著地)→ 进入抓墙状态
|
||||
var wss = Owner.GetState<WallSlideState>();
|
||||
if (wss != null && !Move.IsGrounded)
|
||||
{
|
||||
wss.PrepareEnter(inputDir);
|
||||
Owner.TransitionTo(wss);
|
||||
return;
|
||||
}
|
||||
Move.ZeroHorizontalVelocity();
|
||||
}
|
||||
else if (wd != null && wd.HasPartialContact(inputDir))
|
||||
{
|
||||
// 仅部分射线命中(如检测点高于矮墙顶部),停止施加朝墙方向的水平速度,
|
||||
// 防止角色边角被卡在墙顶而无法继续下落。
|
||||
Move.ZeroHorizontalVelocity();
|
||||
}
|
||||
else
|
||||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
||||
}
|
||||
else
|
||||
Move.ZeroHorizontalVelocity();
|
||||
}
|
||||
|
||||
public override void OnStateExit()
|
||||
{
|
||||
// 顶点悬停可能已降低重力,离开本状态时必须恢复;
|
||||
// 否则进入 FallState/DashState 等后续状态时重力仍低于默认值。
|
||||
Move.SetGravityScale(Cfg.DefaultGravityScale);
|
||||
Input.JumpCancelledEvent -= OnJumpCancelled;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,13 @@ namespace BaseGames.Player.States
|
||||
#if UNITY_EDITOR
|
||||
[Header("调试")]
|
||||
[SerializeField] private bool _debugValidateTransitions = true;
|
||||
|
||||
[Header("── 运行时状态 ──")]
|
||||
[SerializeField] private string _dbg_CurrentState;
|
||||
[SerializeField] private bool _dbg_IsGrounded;
|
||||
[SerializeField] private int _dbg_AirJumpsLeft;
|
||||
[SerializeField] private bool _dbg_CanDash;
|
||||
[SerializeField] private bool _dbg_IsInvincible;
|
||||
#endif
|
||||
|
||||
// Overlay Layer(Layer 1):供 SpringState / SoulSkill 等叠加层动画使用
|
||||
@@ -139,10 +146,12 @@ namespace BaseGames.Player.States
|
||||
public void UseAirJump() => _airJumpsLeft = Mathf.Max(0, _airJumpsLeft - 1);
|
||||
/// <summary>
|
||||
/// 落地时重置空中跳跃次数(由 IdleState/RunState.OnStateEnter 调用)。
|
||||
/// 若解锁 DoubleJump 则重置为 1,否则为 0。
|
||||
/// 若解锁 DoubleJump 则重置为 MovConfig.MaxAirJumps,否则为 0。
|
||||
/// </summary>
|
||||
public void ResetAirJumps() =>
|
||||
_airJumpsLeft = _stats != null && _stats.HasAbility(AbilityType.DoubleJump) ? 1 : 0;
|
||||
_airJumpsLeft = (_stats != null && _stats.HasAbility(AbilityType.DoubleJump))
|
||||
? (_movementConfig != null ? _movementConfig.MaxAirJumps : 1)
|
||||
: 0;
|
||||
|
||||
// ── Overlay Layer API(供 SpringState / SoulSkill 等叠加动画使用)─────
|
||||
/// <summary>
|
||||
@@ -192,6 +201,10 @@ namespace BaseGames.Player.States
|
||||
_parrySystem.OnParryActivated += OnParryActivated;
|
||||
_parrySystem.OnParryConsumed += OnParryConsumedHandler;
|
||||
}
|
||||
|
||||
// 订阅灵泉使用输入
|
||||
if (_inputReader != null)
|
||||
_inputReader.UseSpringEvent += OnUseSpring;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@@ -201,6 +214,9 @@ namespace BaseGames.Player.States
|
||||
_parrySystem.OnParryActivated -= OnParryActivated;
|
||||
_parrySystem.OnParryConsumed -= OnParryConsumedHandler;
|
||||
}
|
||||
|
||||
if (_inputReader != null)
|
||||
_inputReader.UseSpringEvent -= OnUseSpring;
|
||||
}
|
||||
|
||||
/// <summary>弹反输入激活时由 ParrySystem 触发 → 转换到 ParryState。</summary>
|
||||
@@ -217,6 +233,15 @@ namespace BaseGames.Player.States
|
||||
_shield?.OnParrySuccess();
|
||||
}
|
||||
|
||||
/// <summary>灵泉输入:地面且有剩余充能时转入 SpringState 使用一次。</summary>
|
||||
private void OnUseSpring()
|
||||
{
|
||||
if (_stats == null || _stats.CurrentSpringCharges <= 0) return;
|
||||
if (_movement != null && !_movement.IsGrounded) return;
|
||||
if (_states.ContainsKey(typeof(SpringState)))
|
||||
TransitionTo(GetState<SpringState>());
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (!HasRequiredStateDependencies())
|
||||
@@ -238,6 +263,14 @@ namespace BaseGames.Player.States
|
||||
GetState<DashState>()?.TickCooldown(Time.deltaTime);
|
||||
|
||||
_currentState?.OnStateUpdate();
|
||||
|
||||
#if UNITY_EDITOR
|
||||
_dbg_CurrentState = _currentState?.GetType().Name ?? "None";
|
||||
_dbg_IsGrounded = _movement != null && _movement.IsGrounded;
|
||||
_dbg_AirJumpsLeft = _airJumpsLeft;
|
||||
_dbg_CanDash = GetState<DashState>()?.CanDash ?? false;
|
||||
_dbg_IsInvincible = _stats != null && _stats.IsInvincible;
|
||||
#endif
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
@@ -276,7 +309,6 @@ namespace BaseGames.Player.States
|
||||
_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);
|
||||
|
||||
@@ -12,8 +12,9 @@ namespace BaseGames.Player.States
|
||||
if (AnimCfg?.Run != null)
|
||||
Anim.Play(AnimCfg.Run);
|
||||
// 落地时重置空中能力计数器(绝大多数情况被 IdleState 覆盖,但水平落地直接进入 RunState 时也需要)
|
||||
Owner.GetState<AerialDashState>()?.ResetAerialDashes();
|
||||
Owner.ResetAirJumps();
|
||||
Owner.GetState<DashState>()?.ResetAirDash();
|
||||
Owner.GetState<WallSlideState>()?.ResetWallGrab();
|
||||
}
|
||||
|
||||
public override void OnStateUpdate()
|
||||
@@ -28,12 +29,22 @@ namespace BaseGames.Player.States
|
||||
_owner.TransitionTo(_owner.GetState<JumpState>());
|
||||
return;
|
||||
}
|
||||
// 地面冲刺:需解锁 Dash 能力,且冲刺不在冷却
|
||||
if (Buffer.ConsumeDash()
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||||
&& Owner.GetState<DashState>() is { CanDash: true } dashState)
|
||||
// 地面攻击:Move Y > 0 → 上劈;其余 → 普通连击
|
||||
if (Buffer.ConsumeAttack())
|
||||
{
|
||||
_owner.TransitionTo(dashState);
|
||||
if (Input.MoveInput.y > 0.5f)
|
||||
_owner.TransitionTo(_owner.GetState<UpAttackState>());
|
||||
else
|
||||
_owner.TransitionTo(_owner.GetState<AttackState>());
|
||||
return;
|
||||
}
|
||||
// 地面冲刺:先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入
|
||||
var ds = Owner.GetState<DashState>();
|
||||
if (ds != null && ds.CanDash
|
||||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||||
&& Buffer.ConsumeDash())
|
||||
{
|
||||
_owner.TransitionTo(ds);
|
||||
return;
|
||||
}
|
||||
if (Mathf.Abs(Input.MoveInput.x) < 0.1f)
|
||||
@@ -46,6 +57,13 @@ namespace BaseGames.Player.States
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 离地检测(与 CheckGrounded 同帧在 FixedUpdate 中执行,立即响应离地事件)
|
||||
if (!Move.IsGrounded)
|
||||
{
|
||||
_owner.TransitionTo(_owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
|
||||
float inputX = Input.MoveInput.x;
|
||||
if (Mathf.Abs(inputX) > 0.1f)
|
||||
Move.Move(inputX * Cfg.RunSpeed);
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 上劈状态(架构 05_PlayerModule §2)。
|
||||
/// 激活 HitBoxUp;结束后回到 Idle(地面)或 FallState(空中)。
|
||||
/// HitBox 由 ComboStepConfig 时间窗口驱动;结束后回到 Idle(地面)或 FallState(空中)。
|
||||
/// </summary>
|
||||
public class UpAttackState : PlayerStateBase
|
||||
{
|
||||
@@ -12,21 +12,27 @@ namespace BaseGames.Player.States
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Up);
|
||||
|
||||
var clip = Owner.Weapon?.ActiveWeapon?.upAttackClip;
|
||||
if (clip != null && clip.Clip != null)
|
||||
// 上劈反嵈:空中施加向下微小冲量,增强出招手感(地面无效)
|
||||
if (!Move.IsGrounded && Move?.Rb != null)
|
||||
Move.Rb.velocity = new UnityEngine.Vector2(Move.Rb.velocity.x, Move.Rb.velocity.y - 3f);
|
||||
|
||||
var step = Owner.Weapon?.ActiveWeapon?.GetDirStep(AttackDirection.Up);
|
||||
float spd = Stats?.AnimatorSpeedMultiplier ?? 1f;
|
||||
|
||||
if (step?.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;
|
||||
var animState = Anim.Play(step.Value.clip);
|
||||
animState.Speed *= spd;
|
||||
var events = animState.Events(this);
|
||||
events.OnEnd = OnClipEnd;
|
||||
var s = step.Value;
|
||||
events.Add(s.hitBoxEnter, () => Owner.Combat?.EnableWeaponHitBox(AttackDirection.Up, s.hitBoxId));
|
||||
events.Add(s.hitBoxExit, () => Owner.Combat?.DisableAllWeaponHitBoxes());
|
||||
}
|
||||
else
|
||||
{
|
||||
Owner.Combat?.EnableWeaponHitBox(AttackDirection.Up);
|
||||
OnClipEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,31 +3,56 @@ using UnityEngine;
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 蹬墙跳状态(架构 05_PlayerModule §2)。
|
||||
/// 从 WallSlideState 进入;施加背墙方向的水平 + 垂直速度;
|
||||
/// 短暂锁定水平输入后转为 FallState。
|
||||
/// 蹬墙跳状态(架构 05_PlayerModule §2,自定义蹬墙跳设计)。
|
||||
///
|
||||
/// 从 WallSlideState 进入(正常模式下按跳跃键触发)。
|
||||
/// 分为两种子类型,由 PrepareEnter 时的水平输入决定:
|
||||
/// - 背墙跳(Jump Away):无输入或反方向输入 → 远离墙壁斜上方弹出(WallJumpAwayForce)。
|
||||
/// - 对墙跳(Jump Toward):朝向墙壁方向输入 → 沿墙壁方向斜上方弹出(WallJumpTowardForce)。
|
||||
///
|
||||
/// 公共规则:
|
||||
/// 视为第一段跳(不消耗空中跳跃次数);支持可变高度(提前松键截断);
|
||||
/// 上升结束后转 FallState。
|
||||
/// </summary>
|
||||
public class WallJumpState : PlayerStateBase
|
||||
{
|
||||
private float _inputLockTimer;
|
||||
private int _wallDir;
|
||||
private bool _isAwayJump; // true = 背墙跳;false = 对墙跳
|
||||
|
||||
public WallJumpState(PlayerController owner) : base(owner) { }
|
||||
|
||||
/// <summary>
|
||||
/// 由 WallSlideState 在调用 TransitionTo 之前调用。
|
||||
/// wallDir:抓墙方向(+1 右墙 / -1 左墙)。
|
||||
/// moveInputX:触发跳跃时的水平输入值。
|
||||
/// </summary>
|
||||
public void PrepareEnter(int wallDir, float moveInputX)
|
||||
{
|
||||
_wallDir = wallDir;
|
||||
// 无输入或输入方向与墙壁反向 → 背墙跳;输入方向与墙壁同向 → 对墙跳
|
||||
int inputDir = moveInputX > 0.1f ? 1 : (moveInputX < -0.1f ? -1 : 0);
|
||||
_isAwayJump = (inputDir == 0 || inputDir != _wallDir);
|
||||
}
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
// 记录墙壁方向(跳跃反向)
|
||||
_wallDir = Owner.WallDetector != null ? Owner.WallDetector.WallDirection : 0;
|
||||
if (_wallDir == 0) _wallDir = -Owner.FacingDirection;
|
||||
// 施加对应类型的速度
|
||||
if (_isAwayJump)
|
||||
Move?.WallJumpAway(_wallDir);
|
||||
else
|
||||
Move?.WallJumpToward(_wallDir);
|
||||
|
||||
// 施加蹬墙跳速度
|
||||
Move?.WallJump(_wallDir);
|
||||
// 蹬墙跳视为第一段跳,重置空中跳跃次数(使二段跳可再次使用)
|
||||
Owner.ResetAirJumps();
|
||||
|
||||
// 锁定水平输入
|
||||
_inputLockTimer = Cfg.WallJumpInputLockDuration;
|
||||
|
||||
// 播放跳跃动画(复用跳跃动画)
|
||||
if (AnimCfg?.Jump != null) Anim?.Play(AnimCfg.Jump);
|
||||
// 播放蹬墙跳动画:背墙跳/对墙跳使用各自专属 Clip,留空时回退到 Jump 动画
|
||||
var wallJumpClip = _isAwayJump
|
||||
? (AnimCfg?.WallJumpAway ?? AnimCfg?.Jump)
|
||||
: (AnimCfg?.WallJumpToward ?? AnimCfg?.Jump);
|
||||
if (wallJumpClip != null) Anim?.Play(wallJumpClip);
|
||||
|
||||
Input.JumpCancelledEvent += OnJumpCancelled;
|
||||
}
|
||||
@@ -59,3 +84,4 @@ namespace BaseGames.Player.States
|
||||
private void OnJumpCancelled() => Move?.CutJump();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,58 @@ using UnityEngine;
|
||||
namespace BaseGames.Player.States
|
||||
{
|
||||
/// <summary>
|
||||
/// 壁滑状态(架构 05_PlayerModule §2)。
|
||||
/// 需有 PlayerWallDetector.IsTouchingWall == true 才能进入;
|
||||
/// 限制下落速度为 WallSlideSpeed;按跳跃则切换到 WallJumpState。
|
||||
/// 抓墙状态(自定义设计,架构 05_PlayerModule §2)。
|
||||
///
|
||||
/// 触发条件:空中贴墙时玩家按下朝向墙壁的方向键(由 FallState/JumpState 检测并调用 PrepareEnter)。
|
||||
/// 维持条件:进入后无需持续按键;主动按下反方向键或落地时解除。
|
||||
///
|
||||
/// 高度记忆机制(防止单面墙反复爬升):
|
||||
/// - 首次抓墙(或切换到另一侧墙壁)时记录 _wallGrabY。
|
||||
/// - 若当前 Y > _wallGrabY + 容差 → 受限模式:持续下滑,不可蹬墙跳。
|
||||
/// - 若当前 Y ≤ _wallGrabY + 容差 → 正常模式:静止悬挂,可触发蹬墙跳。
|
||||
/// - 重置时机:① 落地时(由 IdleState/RunState 调用 ResetWallGrab);
|
||||
/// ② 切换到另一侧墙壁时(OnStateEnter 内自动判断)。
|
||||
/// </summary>
|
||||
public class WallSlideState : PlayerStateBase
|
||||
{
|
||||
// ── 运行时状态 ────────────────────────────────────────────────────────
|
||||
/// <summary>首次抓住该侧墙壁时记录的 Y 坐标。</summary>
|
||||
private float _wallGrabY = float.MinValue;
|
||||
/// <summary>上次记录 wallGrabY 时的墙壁方向(+1 右墙 / -1 左墙)。</summary>
|
||||
private int _lastGrabDir = 0;
|
||||
/// <summary>本次进入时确认的墙壁方向(由 PrepareEnter 设置)。</summary>
|
||||
private int _wallDir = 0;
|
||||
/// <summary>当前是否处于正常模式(可蹬墙跳)。受限模式(isRestricted)时不可跳。</summary>
|
||||
private bool _canJump = false;
|
||||
|
||||
public WallSlideState(PlayerController owner) : base(owner) { }
|
||||
|
||||
/// <summary>
|
||||
/// 由 FallState / JumpState 在调用 TransitionTo 之前调用,传入已确认的墙壁方向。
|
||||
/// </summary>
|
||||
public void PrepareEnter(int wallDir) => _wallDir = wallDir;
|
||||
|
||||
/// <summary>
|
||||
/// 落地时由 IdleState / RunState 调用,重置高度记忆,允许下次抓同侧墙时重新计算。
|
||||
/// </summary>
|
||||
public void ResetWallGrab()
|
||||
{
|
||||
_wallGrabY = float.MinValue;
|
||||
_lastGrabDir = 0;
|
||||
}
|
||||
|
||||
public override void OnStateEnter()
|
||||
{
|
||||
// 若切换到另一侧墙壁,重置高度记录(视为全新的墙壁)
|
||||
if (_wallDir != _lastGrabDir)
|
||||
{
|
||||
_wallGrabY = Owner.transform.position.y;
|
||||
_lastGrabDir = _wallDir;
|
||||
}
|
||||
|
||||
// 计算当前是否处于正常模式
|
||||
UpdateCanJump();
|
||||
|
||||
if (AnimCfg?.WallSlide != null)
|
||||
Anim?.Play(AnimCfg.WallSlide);
|
||||
|
||||
@@ -26,8 +68,10 @@ namespace BaseGames.Player.States
|
||||
|
||||
public override void OnStateUpdate()
|
||||
{
|
||||
var wd = Owner.WallDetector;
|
||||
|
||||
// 离开墙壁 → 下落
|
||||
if (Owner.WallDetector == null || !Owner.WallDetector.IsTouchingWall)
|
||||
if (wd == null || !wd.IsTouchingWall)
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
return;
|
||||
@@ -39,17 +83,52 @@ namespace BaseGames.Player.States
|
||||
Owner.TransitionTo(Owner.GetState<IdleState>());
|
||||
return;
|
||||
}
|
||||
|
||||
// 主动按反方向键 → 脱离(松墙下落)
|
||||
float mx = Input.MoveInput.x;
|
||||
if (Mathf.Abs(mx) > 0.1f)
|
||||
{
|
||||
int inputDir = mx > 0f ? 1 : -1;
|
||||
if (inputDir != _wallDir)
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<FallState>());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 每帧刷新正常/受限状态
|
||||
UpdateCanJump();
|
||||
}
|
||||
|
||||
public override void OnStateFixedUpdate()
|
||||
{
|
||||
// 限制下落速度(壁滑缓慢下落)
|
||||
Move?.ApplyWallSlide();
|
||||
if (_canJump)
|
||||
// 正常模式:静止悬挂,阻止向下速度
|
||||
Move?.ZeroVerticalVelocity();
|
||||
else
|
||||
// 受限模式:持续下滑
|
||||
Move?.ApplyWallSlide();
|
||||
}
|
||||
|
||||
// ── 内部 ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void UpdateCanJump()
|
||||
{
|
||||
float tolerance = Cfg?.WallGrabHeightTolerance ?? 0.05f;
|
||||
_canJump = Owner.transform.position.y <= _wallGrabY + tolerance;
|
||||
}
|
||||
|
||||
private void OnJumpPressed()
|
||||
{
|
||||
Owner.TransitionTo(Owner.GetState<WallJumpState>());
|
||||
// 受限模式禁止蹬墙跳
|
||||
if (!_canJump) return;
|
||||
|
||||
var wjs = Owner.GetState<WallJumpState>();
|
||||
if (wjs == null) return;
|
||||
|
||||
wjs.PrepareEnter(_wallDir, Input.MoveInput.x);
|
||||
Owner.TransitionTo(wjs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user