Files
zeling_v2/Assets/_Game/Scripts/Player/States/JumpState.cs
Joywayer 06048c966a feat: Add HurtBoxOwnerGuard to prevent multiple damage registrations from the same HitBox activation
- Implemented HurtBoxOwnerGuard to ensure that multiple HurtBoxes on the same character do not register damage multiple times during a single HitBox activation.
- Added custom editor for HitBox to facilitate the creation of shape colliders with HitBoxColliderProxy.
- Introduced PhysicsPerceptionSystem for enemy perception, supporting multiple detection modes including RangeCircle, BatchLOS, FanCast, and BoxCast.
- Created EnemyPatrolZone to define patrol and chase areas for enemies, allowing for shared zones among multiple enemies.
- Added BD_IsOutsideZone conditional task for Behavior Designer to check if an enemy or player is outside a defined patrol zone.
2026-06-02 16:10:44 +08:00

230 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
// 最小跳跃窗口:窗口内松键记录 _cutPending窗口结束时统一执行 CutJump
// 保证短按始终在相同 vy 时截断 → 最小跳跃高度完全一致,消除帧级手感抖动。
private float _minJumpTimer;
private bool _cutPending;
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; // 消耗标记
ResetMinJumpWindow();
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();
ResetMinJumpWindow(); // 二段跳重置最小跳跃窗口
// 播放空中跳跃动画,未配置时回退到 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 时执行 CutJump → 一致的最小跳跃高度。
if (_minJumpTimer > 0f)
{
_minJumpTimer -= Time.fixedDeltaTime;
if (_minJumpTimer <= 0f)
{
// 窗口到期已收到松键事件或当前键未按住InputBuffer 延迟导致进入时键已释放)
if (_cutPending || !Input.IsJumpHeld)
Move.CutJump();
_cutPending = false;
}
}
// ── 顶点悬停:第一判断 |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;
// PlayerWallDetectororder 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()
{
if (_minJumpTimer > 0f)
_cutPending = true; // 窗口内:推迟到窗口结束时执行,保证一致的截断时机
else
Move.CutJump(); // 窗口已过(长按):立即截断 → 变高跳
}
private void ResetMinJumpWindow()
{
_minJumpTimer = Cfg.MinJumpTime;
_cutPending = false;
}
}
}