From 2a6a0b186163d186624ed6d3db9f9585e711641d Mon Sep 17 00:00:00 2001 From: Joywayer Date: Tue, 19 May 2026 12:12:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=8A=93=E5=A2=99?= =?UTF-8?q?=E9=AB=98=E5=BA=A6=E8=AE=B0=E5=BF=86=E3=80=81=E8=83=8C=E5=A2=99?= =?UTF-8?q?=E8=B7=B3/=E5=AF=B9=E5=A2=99=E8=B7=B3=E3=80=81=E8=B9=AC?= =?UTF-8?q?=E5=A2=99=E5=90=8E=E8=87=AA=E5=8A=A8=E6=8A=93=E5=A2=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PlayerController: 添加 wallGrabY/wallGrabDir/isPostWallJump 字段及 API - WallSlideState: 高度限制(超限强制下滑不可蹬跳)+ 静止悬挂 + 反向键释放 - WallJumpState: 区分背墙跳(WallJumpAwayForce)/对墙跳(WallJumpBackForce) 蹬墙后标记 PostWallJump 允许空中自动抓墙;恢复空中冲刺次数 - FallState/JumpState: 新增抓墙入口(朝向按键 OR PostWallJump 自动抓墙) - PlayerMovement.WallJump: 增加 jumpAway 参数区分两种跳跃力 - IdleState/RunState: 落地时重置抓墙记录和蹬墙跳标记 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Assets/_Game/Scripts/Player/PlayerMovement.cs | 23 +++++++-- .../_Game/Scripts/Player/States/FallState.cs | 14 +++++ .../_Game/Scripts/Player/States/IdleState.cs | 4 +- .../_Game/Scripts/Player/States/JumpState.cs | 14 +++++ .../Scripts/Player/States/PlayerController.cs | 41 +++++++++++++++ .../_Game/Scripts/Player/States/RunState.cs | 4 +- .../Scripts/Player/States/WallJumpState.cs | 43 ++++++++++++---- .../Scripts/Player/States/WallSlideState.cs | 51 ++++++++++++++++--- 8 files changed, 172 insertions(+), 22 deletions(-) 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()); } } }