197 lines
9.8 KiB
C#
197 lines
9.8 KiB
C#
using UnityEngine;
|
||
|
||
namespace BaseGames.Player.States
|
||
{
|
||
/// <summary>
|
||
/// 跳跃状态。
|
||
/// - 一段跳 / 郊狼跳:OnStateEnter 时调用 Move.Jump()。
|
||
/// - 二段跳(二段跳能力解锁后可用):上升或下落途中再按跳跃且 AirJumpsLeft > 0,
|
||
/// 调用 Move.DoubleJump(),重播跳跃动画,不离开本状态(保持速度截断逻辑)。
|
||
/// - 空中冲刺:上升途中按冲刺且 HasAbility(AirDash) → DashState。
|
||
/// - 变高跳:松开跳跃键触发 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();
|
||
// 优先播放专属空中跳跃动画,未配置时回退到普通跳跃动画
|
||
var airJumpClip = AnimCfg?.AirJump ?? AnimCfg?.Jump;
|
||
if (airJumpClip != null) Anim?.Play(airJumpClip);
|
||
}
|
||
else
|
||
Move.Jump();
|
||
|
||
_isDoubleJump = false; // 消耗标记
|
||
Input.JumpCancelledEvent += OnJumpCancelled;
|
||
// 开启上升阶段贴墙 vy 保护:防止物理摩擦降低跳跃最高点
|
||
Move.SetPreserveVyOnWallContact(true);
|
||
}
|
||
|
||
public override void OnStateUpdate()
|
||
{
|
||
// 上升结束时转为下落。
|
||
// 例外:按住朝墙方向且射线已检测到墙时,物理摩擦可能将 vy 瞬间压到 ≤ 0,
|
||
// 此时不触发 FallState,让后续抓墙检测在 vy 稳定后接管状态转换,
|
||
// 防止因一帧摩擦导致跳跃高度降低并错误进入 FallState。
|
||
if (Move.Rb.velocity.y <= 0f)
|
||
{
|
||
bool pressingTowardDetectedWall = false;
|
||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
||
{
|
||
int inputDir = Input.MoveInput.x > 0 ? 1 : -1;
|
||
bool cwRay = inputDir > 0 ? Move.IsWallRight : Move.IsWallLeft;
|
||
var wd0 = Owner.WallDetector;
|
||
pressingTowardDetectedWall = cwRay
|
||
|| (wd0 != null && wd0.IsTouchingWall && wd0.WallDirection == inputDir);
|
||
}
|
||
if (!pressingTowardDetectedWall)
|
||
{
|
||
_owner.TransitionTo(_owner.GetState<FallState>());
|
||
return;
|
||
}
|
||
}
|
||
|
||
// ── 下冲刺(下 + 冲刺 → 向下冲刺,优先于普通冲刺)──────────────────────
|
||
// 按住下方向 + 冲刺键,且已解锁 DownDash 能力、空中冲刺次数未耗尽
|
||
var dashState = Owner.GetState<DashState>();
|
||
if (dashState != null && dashState.CanDashMidAir
|
||
&& Input.MoveInput.y < -0.5f
|
||
&& Stats != null && Stats.HasAbility(AbilityType.DownDash)
|
||
&& Buffer.ConsumeDash())
|
||
{
|
||
_owner.TransitionTo(_owner.GetState<DownDashState>());
|
||
return;
|
||
}
|
||
|
||
// 冲刺(地面/空中统一使用 DashState,空中限一次,优先于二段跳:冲刺可保存二段跳机会)
|
||
// 先确认能力与冷却均满足,再消耗缓冲,避免无操作时静默吃掉输入
|
||
if (dashState != null && dashState.CanDashMidAir
|
||
&& Stats != null && Stats.HasAbility(AbilityType.Dash)
|
||
&& Buffer.ConsumeDash())
|
||
{
|
||
_owner.TransitionTo(dashState);
|
||
return;
|
||
}
|
||
|
||
// 二段跳:上升阶段即可触发(先确认有次数再消耗缓冲,避免静默消耗)
|
||
if (Owner.AirJumpsLeft > 0 && Buffer.ConsumeJump())
|
||
{
|
||
Owner.UseAirJump();
|
||
Move.DoubleJump();
|
||
// 播放空中跳跃动画,未配置时回退到 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;
|
||
}
|
||
|
||
// ── 抓墙:贴墙 + 朝向墙壁按键,或蹬墙跳后的自动抓墙──────────────
|
||
// 仅在上升结束后(vy ≤ 0)才进入抓墙状态;上升阶段阻止转换以保留顶点重力缩减,
|
||
// 避免贴墙按方向键导致跳跃最大高度降低。
|
||
var wd = Owner.WallDetector;
|
||
if (wd != null && wd.IsTouchingWall && !Move.IsGrounded && !Move.IsRising)
|
||
{
|
||
int wallDir = wd.WallDirection;
|
||
bool pressingTowardWall = Mathf.Abs(Input.MoveInput.x) > 0.01f
|
||
&& (int)Mathf.Sign(Input.MoveInput.x) == wallDir;
|
||
if ((pressingTowardWall || Owner.IsPostWallJump)
|
||
&& Stats != null && Stats.HasAbility(AbilityType.WallCling))
|
||
{
|
||
_owner.TransitionTo(_owner.GetState<WallSlideState>());
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
public override void OnStateFixedUpdate()
|
||
{
|
||
// ── 顶点悬停:第一判断 |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.SetGravityScale(Cfg.DefaultGravityScale);
|
||
|
||
// ── 空中水平移动(朝向墙壁时停止施力,防止贴墙悬停)──────────────
|
||
if (Mathf.Abs(Input.MoveInput.x) > 0.01f)
|
||
{
|
||
int inputDir = Input.MoveInput.x > 0 ? 1 : -1;
|
||
var wd = Owner.WallDetector;
|
||
// PlayerWallDetector(order 0)在状态机(order -100)之后执行,
|
||
// 其 IsTouchingWall / HasPartialContact 对状态机而言是上一帧的结果。
|
||
// PlayerMovement.CheckWalls()(order -200)在状态机之前执行,
|
||
// IsWallLeft / IsWallRight 是当帧最新结果,可提前一帧停止施力,
|
||
// 防止角色以 RunSpeed 压入墙面后物理引擎的摩擦力降低上升速度。
|
||
bool currentFrameWall = inputDir > 0 ? Move.IsWallRight : Move.IsWallLeft;
|
||
if (wd != null && wd.IsTouchingWall && wd.WallDirection == inputDir)
|
||
{
|
||
// 按下朝墙方向键 + 在墙上(且未著地)→ 进入抓墙状态
|
||
// 仅在上升结束后(vy ≤ 0)才进入抓墙状态;上升阶段只停止水平施力,
|
||
// 保留顶点重力缩减逻辑,防止贴墙按键导致跳跃最大高度降低。
|
||
var wss = Owner.GetState<WallSlideState>();
|
||
if (wss != null && !Move.IsGrounded && !Move.IsRising)
|
||
{
|
||
wss.PrepareEnter(inputDir);
|
||
Owner.TransitionTo(wss);
|
||
return;
|
||
}
|
||
Move.ZeroHorizontalVelocity();
|
||
}
|
||
else if (currentFrameWall || (wd != null && wd.HasPartialContact(inputDir)))
|
||
{
|
||
// 当帧单射线已检测到墙(PlayerMovement -200 先于状态机 -100 执行),
|
||
// 或上一帧部分射线命中——停止施力,防止以 RunSpeed 压入墙面产生摩擦力。
|
||
Move.ZeroHorizontalVelocity();
|
||
}
|
||
else
|
||
Move.Move(Input.MoveInput.x * Cfg.RunSpeed);
|
||
}
|
||
else
|
||
Move.ZeroHorizontalVelocity();
|
||
}
|
||
|
||
public override void OnStateExit()
|
||
{
|
||
// 顶点悬停可能已降低重力,离开本状态时必须恢复;
|
||
// 否则进入 FallState/DashState 等后续状态时重力仍低于默认值。
|
||
Move.SetGravityScale(Cfg.DefaultGravityScale);
|
||
Move.SetPreserveVyOnWallContact(false);
|
||
Input.JumpCancelledEvent -= OnJumpCancelled;
|
||
}
|
||
|
||
private void OnJumpCancelled() => Move.CutJump();
|
||
}
|
||
}
|