diff --git a/Assets/_Game/Scripts/Player/PlayerMovement.cs b/Assets/_Game/Scripts/Player/PlayerMovement.cs
index bfe47b2..3ce90b4 100644
--- a/Assets/_Game/Scripts/Player/PlayerMovement.cs
+++ b/Assets/_Game/Scripts/Player/PlayerMovement.cs
@@ -180,13 +180,26 @@ namespace BaseGames.Player
}
///
- /// 蹬墙跳:对墙方向施加相反水平力 + 向上力。
- /// wallDir = +1 (右墙) 或 -1 (左墙),跳跃方向与之相反。
+ /// 蹬墙跳:根据跳跃类型施加不同速度。
+ /// wallDir = +1 (右墙) 或 -1 (左墙)。
+ /// jumpAway = true:背墙跳(朝远离墙壁方向),施加 WallJumpAwayForceX/Y;
+ /// jumpAway = false:对墙跳(朝墙壁方向),施加 WallJumpBackForceX + WallJumpForceY。
///
- public void WallJump(int wallDir)
+ public void WallJump(int wallDir, bool jumpAway)
{
- float forceX = -wallDir * _config.WallJumpForceX;
- float forceY = _config.WallJumpForceY;
+ float forceX, forceY;
+ if (jumpAway)
+ {
+ // 背墙跳:远离墙壁方向弹出
+ forceX = -wallDir * _config.WallJumpAwayForceX;
+ forceY = _config.WallJumpAwayForceY;
+ }
+ else
+ {
+ // 对墙跳:偏向垂直向上,水平分量朝向墙壁
+ forceX = wallDir * _config.WallJumpBackForceX;
+ forceY = _config.WallJumpForceY;
+ }
_rb.velocity = new Vector2(forceX, forceY);
_coyoteTimer = 0f;
}
diff --git a/Assets/_Game/Scripts/Player/States/FallState.cs b/Assets/_Game/Scripts/Player/States/FallState.cs
index 87b509a..ebb0eb1 100644
--- a/Assets/_Game/Scripts/Player/States/FallState.cs
+++ b/Assets/_Game/Scripts/Player/States/FallState.cs
@@ -64,6 +64,20 @@ namespace BaseGames.Player.States
return;
}
+ // ── 抓墙:贴墙 + 朝向墙壁按键,或蹬墙跳后的自动抓墙──────────────
+ var wd = Owner.WallDetector;
+ if (wd != null && wd.IsTouchingWall)
+ {
+ int wallDir = wd.WallDirection;
+ bool pressingTowardWall = Mathf.Abs(Input.MoveInput.x) > 0.01f
+ && (int)Mathf.Sign(Input.MoveInput.x) == wallDir;
+ if (pressingTowardWall || Owner.IsPostWallJump)
+ {
+ _owner.TransitionTo(_owner.GetState());
+ return;
+ }
+ }
+
}
public override void OnStateFixedUpdate()
diff --git a/Assets/_Game/Scripts/Player/States/IdleState.cs b/Assets/_Game/Scripts/Player/States/IdleState.cs
index aecb9bc..16c72a4 100644
--- a/Assets/_Game/Scripts/Player/States/IdleState.cs
+++ b/Assets/_Game/Scripts/Player/States/IdleState.cs
@@ -12,9 +12,11 @@ namespace BaseGames.Player.States
if (AnimCfg?.Idle != null)
Anim.Play(AnimCfg.Idle);
Move?.ZeroHorizontalVelocity();
- // 落地时重置空中能力计数器
+ // 落地时重置空中能力计数器及抓墙记录
Owner.GetState()?.ResetAerialDashes();
Owner.ResetAirJumps();
+ Owner.ResetWallGrab();
+ Owner.SetPostWallJump(false);
}
public override void OnStateUpdate()
diff --git a/Assets/_Game/Scripts/Player/States/JumpState.cs b/Assets/_Game/Scripts/Player/States/JumpState.cs
index eb26f23..a1a46e8 100644
--- a/Assets/_Game/Scripts/Player/States/JumpState.cs
+++ b/Assets/_Game/Scripts/Player/States/JumpState.cs
@@ -68,6 +68,20 @@ namespace BaseGames.Player.States
if (AnimCfg?.Jump != null) Anim?.Play(AnimCfg.Jump);
return;
}
+
+ // ── 抓墙:贴墙 + 朝向墙壁按键,或蹬墙跳后的自动抓墙──────────────
+ var wd = Owner.WallDetector;
+ if (wd != null && wd.IsTouchingWall && !Move.IsGrounded)
+ {
+ int wallDir = wd.WallDirection;
+ bool pressingTowardWall = Mathf.Abs(Input.MoveInput.x) > 0.01f
+ && (int)Mathf.Sign(Input.MoveInput.x) == wallDir;
+ if (pressingTowardWall || Owner.IsPostWallJump)
+ {
+ _owner.TransitionTo(_owner.GetState());
+ return;
+ }
+ }
}
public override void OnStateFixedUpdate()
diff --git a/Assets/_Game/Scripts/Player/States/PlayerController.cs b/Assets/_Game/Scripts/Player/States/PlayerController.cs
index 890f794..0489803 100644
--- a/Assets/_Game/Scripts/Player/States/PlayerController.cs
+++ b/Assets/_Game/Scripts/Player/States/PlayerController.cs
@@ -144,6 +144,47 @@ namespace BaseGames.Player.States
public void ResetAirJumps() =>
_airJumpsLeft = _stats != null && _stats.HasAbility(AbilityType.DoubleJump) ? 1 : 0;
+ // ── 抓墙高度记忆 API ──────────────────────────────────────────────────
+ // wallGrabY:首次抓住某面墙壁时记录的 Y 坐标;用于防止单面墙无限向上爬。
+ // wallGrabDir:当前记录所属墙壁方向(+1=右墙,-1=左墙,0=无)。
+ // isPostWallJump:蹬墙跳后未落地标记,允许靠近墙壁时自动抓墙。
+ private float _wallGrabY = float.NegativeInfinity;
+ private int _wallGrabDir = 0;
+ private bool _isPostWallJump;
+
+ public float WallGrabY => _wallGrabY;
+ public int WallGrabDir => _wallGrabDir;
+ public bool IsPostWallJump => _isPostWallJump;
+
+ ///
+ /// 进入 WallSlideState 时调用。
+ /// 不同墙壁(dir != _wallGrabDir)→ 重置并记录新高度;
+ /// 同一面墙壁 → 保留原记录(防止攀爬重置)。
+ ///
+ public void RecordWallGrab(int dir, float y)
+ {
+ if (dir != _wallGrabDir)
+ {
+ _wallGrabDir = dir;
+ _wallGrabY = y;
+ }
+ // 同一面墙:保留 _wallGrabY,不更新
+ }
+
+ /// 落地时重置抓墙记录(由 IdleState/RunState.OnStateEnter 调用)。
+ public void ResetWallGrab()
+ {
+ _wallGrabY = float.NegativeInfinity;
+ _wallGrabDir = 0;
+ }
+
+ ///
+ /// 设置蹬墙跳后自动抓墙标记。
+ /// true:由 WallJumpState.OnStateEnter 设置;
+ /// false:由 IdleState/RunState.OnStateEnter(落地)或 WallSlideState.OnStateEnter(已消耗)清除。
+ ///
+ public void SetPostWallJump(bool value) => _isPostWallJump = value;
+
// ── Overlay Layer API(供 SpringState / SoulSkill 等叠加动画使用)─────
///
/// 在 Overlay Layer(Layer 1)播放动画,叠加于当前 Base Layer 动画之上。
diff --git a/Assets/_Game/Scripts/Player/States/RunState.cs b/Assets/_Game/Scripts/Player/States/RunState.cs
index 60d6edc..7540079 100644
--- a/Assets/_Game/Scripts/Player/States/RunState.cs
+++ b/Assets/_Game/Scripts/Player/States/RunState.cs
@@ -11,9 +11,11 @@ namespace BaseGames.Player.States
{
if (AnimCfg?.Run != null)
Anim.Play(AnimCfg.Run);
- // 落地时重置空中能力计数器(绝大多数情况被 IdleState 覆盖,但水平落地直接进入 RunState 时也需要)
+ // 落地时重置空中能力计数器及抓墙记录(水平落地直接进入 RunState 时)
Owner.GetState()?.ResetAerialDashes();
Owner.ResetAirJumps();
+ Owner.ResetWallGrab();
+ Owner.SetPostWallJump(false);
}
public override void OnStateUpdate()
diff --git a/Assets/_Game/Scripts/Player/States/WallJumpState.cs b/Assets/_Game/Scripts/Player/States/WallJumpState.cs
index f157006..57e0b58 100644
--- a/Assets/_Game/Scripts/Player/States/WallJumpState.cs
+++ b/Assets/_Game/Scripts/Player/States/WallJumpState.cs
@@ -4,31 +4,45 @@ namespace BaseGames.Player.States
{
///
/// 蹬墙跳状态(架构 05_PlayerModule §2)。
- /// 从 WallSlideState 进入;施加背墙方向的水平 + 垂直速度;
- /// 短暂锁定水平输入后转为 FallState。
+ /// 从 WallSlideState 进入;根据输入方向区分背墙跳与对墙跳;
+ /// 蹬墙后标记 PostWallJump,允许空中靠近墙壁时自动抓墙(无需方向键);
+ /// 输入锁结束后如贴墙则自动进入 WallSlideState;否则上升结束后转为 FallState。
///
public class WallJumpState : PlayerStateBase
{
private float _inputLockTimer;
private int _wallDir;
+ private bool _jumpAway; // true = 背墙跳,false = 对墙跳
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);
+ // 根据输入方向判断跳跃类型:
+ // 无输入或背离墙壁方向 → 背墙跳
+ // 朝向墙壁方向 → 对墙跳
+ float moveX = Input.MoveInput.x;
+ bool pressingTowardWall = Mathf.Abs(moveX) > 0.01f
+ && (int)Mathf.Sign(moveX) == _wallDir;
+ _jumpAway = !pressingTowardWall;
- // 锁定水平输入
+ // 施加蹬墙跳速度
+ Move?.WallJump(_wallDir, _jumpAway);
+
+ // 锁定水平输入,防止立即覆盖跳跃速度
_inputLockTimer = Cfg.WallJumpInputLockDuration;
- // 播放跳跃动画(复用跳跃动画)
- if (AnimCfg?.Jump != null) Anim?.Play(AnimCfg.Jump);
+ // 标记蹬墙跳后自动抓墙(在 FallState/WallJumpState 中消耗)
+ Owner.SetPostWallJump(true);
+ // 蹬墙成功后立即恢复空中冲刺次数
+ Owner.GetState()?.ResetAerialDashes();
+
+ if (AnimCfg?.Jump != null) Anim?.Play(AnimCfg.Jump);
Input.JumpCancelledEvent += OnJumpCancelled;
}
@@ -39,7 +53,18 @@ namespace BaseGames.Player.States
public override void OnStateUpdate()
{
- // 上升结束 → 下落
+ // 输入锁结束后检查是否贴墙:自动抓墙(优先于下落判断)
+ if (_inputLockTimer <= 0f)
+ {
+ var wd = Owner.WallDetector;
+ if (wd != null && wd.IsTouchingWall && !Move.IsGrounded)
+ {
+ Owner.TransitionTo(Owner.GetState());
+ return;
+ }
+ }
+
+ // 上升结束 → 下落(isPostWallJump 标记保留,FallState 中继续支持自动抓墙)
if (!Move.IsRising)
{
Owner.TransitionTo(Owner.GetState());
diff --git a/Assets/_Game/Scripts/Player/States/WallSlideState.cs b/Assets/_Game/Scripts/Player/States/WallSlideState.cs
index a69c75f..5b88ff9 100644
--- a/Assets/_Game/Scripts/Player/States/WallSlideState.cs
+++ b/Assets/_Game/Scripts/Player/States/WallSlideState.cs
@@ -3,12 +3,17 @@ using UnityEngine;
namespace BaseGames.Player.States
{
///
- /// 壁滑状态(架构 05_PlayerModule §2)。
- /// 需有 PlayerWallDetector.IsTouchingWall == true 才能进入;
- /// 限制下落速度为 WallSlideSpeed;按跳跃则切换到 WallJumpState。
+ /// 抓墙状态(架构 05_PlayerModule §2)。
+ /// 进入条件:空中贴墙时,按下朝向墙壁的方向键(或蹬墙跳后的自动抓墙)。
+ /// 高度限制:若当前 Y > wallGrabY(首次抓该墙的高度),强制下滑且不可蹬墙跳;
+ /// 若当前 Y ≤ wallGrabY,静止悬挂,可蹬墙跳。
+ /// 释放条件:主动按下反方向键或落地。
///
public class WallSlideState : PlayerStateBase
{
+ private bool _canWallJump;
+ private int _wallDir;
+
public WallSlideState(PlayerController owner) : base(owner) { }
public override void OnStateEnter()
@@ -16,6 +21,16 @@ namespace BaseGames.Player.States
if (AnimCfg?.WallSlide != null)
Anim?.Play(AnimCfg.WallSlide);
+ // 记录当前墙壁方向
+ _wallDir = Owner.WallDetector != null ? Owner.WallDetector.WallDirection : -Owner.FacingDirection;
+ if (_wallDir == 0) _wallDir = -Owner.FacingDirection;
+
+ // 记录首次抓墙高度(不同墙壁才重置)
+ Owner.RecordWallGrab(_wallDir, Owner.transform.position.y);
+
+ // 消耗蹬墙跳后的自动抓墙标记
+ Owner.SetPostWallJump(false);
+
Input.JumpStartedEvent += OnJumpPressed;
}
@@ -39,17 +54,41 @@ namespace BaseGames.Player.States
Owner.TransitionTo(Owner.GetState());
return;
}
+
+ // 主动按下反方向键 → 松开墙壁下落
+ float moveX = Input.MoveInput.x;
+ if (Mathf.Abs(moveX) > 0.01f && (int)Mathf.Sign(moveX) == -_wallDir)
+ {
+ Owner.TransitionTo(Owner.GetState());
+ return;
+ }
}
public override void OnStateFixedUpdate()
{
- // 限制下落速度(壁滑缓慢下落)
- Move?.ApplyWallSlide();
+ // 每帧重新判断是否允许蹬墙跳(随下滑高度动态变化)
+ float currentY = Owner.transform.position.y;
+ _canWallJump = currentY <= Owner.WallGrabY + Cfg.WallGrabMaxHeightGain;
+
+ if (_canWallJump)
+ {
+ // 高度合法:静止悬挂(冻结垂直速度)
+ var vel = Move.Rb.velocity;
+ if (vel.y < 0f)
+ Move.Rb.velocity = new Vector2(vel.x, 0f);
+ }
+ else
+ {
+ // 高度超限:强制下滑
+ Move?.ApplyWallSlide();
+ }
}
private void OnJumpPressed()
{
- Owner.TransitionTo(Owner.GetState());
+ // 仅高度合法时才允许蹬墙跳
+ if (_canWallJump)
+ Owner.TransitionTo(Owner.GetState());
}
}
}