diff --git a/Assets/_Game/Scripts/Editor/Tools/Physics2DLayerReport.cs b/Assets/_Game/Scripts/Editor/Tools/Physics2DLayerReport.cs index b04d30d..fb8e3b5 100644 --- a/Assets/_Game/Scripts/Editor/Tools/Physics2DLayerReport.cs +++ b/Assets/_Game/Scripts/Editor/Tools/Physics2DLayerReport.cs @@ -17,8 +17,14 @@ namespace BaseGames.Editor /// · PlayerHitBox ↔ EnemyHurtBox → 应碰撞(玩家攻击伤害敌人) /// · EnemyHitBox ↔ PlayerHurtBox → 应碰撞(敌人攻击伤害玩家) /// · EnemyHitBox ↔ EnemyHurtBox → 应碰撞(敌人可互相伤害,HitBox 运行时排除自身根节点) - /// · Player ↔ Platform → 应碰撞(玩家站在平台上) - /// · Enemy ↔ Platform → 应碰撞(敌人站在平台上) + /// · Player ↔ Platform → 应碰撞(玩家站在实体平台上) + /// · Player ↔ OneWayPlatform → 应碰撞(玩家站在单向平台上) + /// · Player ↔ MovingOneWayPlatform → 应碰撞(玩家站在移动单向平台上) + /// · Player ↔ MidHeightOneWayPlatform → 应碰撞(玩家站在半高单向平台上) + /// · Enemy ↔ Platform → 应碰撞(敌人站在实体平台上) + /// · Enemy ↔ OneWayPlatform → 应碰撞(敌人站在单向平台上) + /// · Enemy ↔ MovingOneWayPlatform → 应碰撞(敌人站在移动单向平台上) + /// · Enemy ↔ MidHeightOneWayPlatform → 应碰撞(敌人站在半高单向平台上) /// · PlayerProjectile ↔ EnemyHurtBox → 应碰撞(玩家投射物伤害敌人) /// · PlayerProjectile ↔ PlayerHurtBox → 应忽略(玩家投射物不自伤) /// · PlayerProjectile ↔ Platform → 应碰撞(玩家投射物命中地形) @@ -41,8 +47,14 @@ namespace BaseGames.Editor new("PlayerHitBox", "EnemyHurtBox", true, "玩家攻击伤害敌人"), new("EnemyHitBox", "PlayerHurtBox", true, "敌人攻击伤害玩家"), new("EnemyHitBox", "EnemyHurtBox", true, "敌人可互相伤害(HitBox 运行时排除自身根节点)"), - new("Player", "Platform", true, "玩家站在平台上"), - new("Enemy", "Platform", true, "敌人站在平台上"), + new("Player", "Platform", true, "玩家站在实体平台上"), + new("Player", "OneWayPlatform", true, "玩家站在单向平台上(PlatformEffector2D 控制单向穿透)"), + new("Player", "MovingOneWayPlatform", true, "玩家站在移动单向平台上"), + new("Player", "MidHeightOneWayPlatform", true, "玩家站在半高单向平台上"), + new("Enemy", "Platform", true, "敌人站在实体平台上"), + new("Enemy", "OneWayPlatform", true, "敌人站在单向平台上"), + new("Enemy", "MovingOneWayPlatform", true, "敌人站在移动单向平台上"), + new("Enemy", "MidHeightOneWayPlatform", true, "敌人站在半高单向平台上"), new("PlayerProjectile", "EnemyHurtBox", true, "玩家投射物伤害敌人"), new("PlayerProjectile", "PlayerHurtBox", false, "玩家投射物不自伤"), new("PlayerProjectile", "Platform", true, "玩家投射物命中地形"), diff --git a/Assets/_Game/Scripts/Player/PlayerMovement.cs b/Assets/_Game/Scripts/Player/PlayerMovement.cs index 95251d3..8475d44 100644 --- a/Assets/_Game/Scripts/Player/PlayerMovement.cs +++ b/Assets/_Game/Scripts/Player/PlayerMovement.cs @@ -24,6 +24,8 @@ namespace BaseGames.Player // ── 运行时状态 ──────────────────────────────────────────────────────── private Rigidbody2D _rb; private float _coyoteTimer; + private float _wallCoyoteTimer; + private int _wallCoyoteDir; // 离墙时记录的墙壁方向(+1 右 / -1 左) private bool _isGrounded; // Update 中调用 ZeroHorizontalVelocity 后设置此标记; // 下一个 FixedUpdate(-200,先于状态机 -100)读取并清零, @@ -47,6 +49,7 @@ namespace BaseGames.Player [SerializeField] private bool _dbg_IsGrounded; [SerializeField] private bool _dbg_OnOneWayPlatform; [SerializeField] private bool _dbg_HasCoyoteTime; + [SerializeField] private bool _dbg_HasWallCoyoteTime; [SerializeField] private bool _dbg_IsWallLeft; [SerializeField] private bool _dbg_IsWallRight; [SerializeField] private bool _dbg_CancelWindowOpen; @@ -55,6 +58,8 @@ namespace BaseGames.Player public bool IsGrounded => _isGrounded; public bool HasCoyoteTime => _coyoteTimer > 0f; + public bool HasWallCoyoteTime => _wallCoyoteTimer > 0f; + public int WallCoyoteDir => _wallCoyoteDir; public bool IsWallLeft => _isWallLeft; public bool IsWallRight => _isWallRight; public bool OnOneWayPlatform => _onOneWayPlatform; @@ -93,12 +98,15 @@ namespace BaseGames.Player else _coyoteTimer = Mathf.Max(0f, _coyoteTimer - Time.fixedDeltaTime); + _wallCoyoteTimer = Mathf.Max(0f, _wallCoyoteTimer - Time.fixedDeltaTime); + #if UNITY_EDITOR _dbg_VelocityX = _rb.velocity.x; _dbg_VelocityY = _rb.velocity.y; _dbg_IsGrounded = _isGrounded; _dbg_OnOneWayPlatform = _onOneWayPlatform; _dbg_HasCoyoteTime = _coyoteTimer > 0f; + _dbg_HasWallCoyoteTime = _wallCoyoteTimer > 0f; _dbg_IsWallLeft = _isWallLeft; _dbg_IsWallRight = _isWallRight; _dbg_CancelWindowOpen = _cancelWindowOpen; @@ -198,6 +206,21 @@ namespace BaseGames.Player // ── 取消窗口 ────────────────────────────────────────────────────────── public void SetCancelWindowOpen(bool open) => _cancelWindowOpen = open; + // ── 墙壁土狼时间 ────────────────────────────────────────────────────── + /// + /// 由 WallSlideState 在切换到 FallState 时调用:启动墙壁土狼计时器。 + /// 窗口内(HasWallCoyoteTime == true)按跳跃键仍可触发蹬墙跳。 + /// wallDir:+1 = 右墙 / -1 = 左墙。 + /// + public void StartWallCoyote(int wallDir) + { + _wallCoyoteTimer = _config.WallCoyoteTime; + _wallCoyoteDir = wallDir; + } + + /// 消耗墙壁土狼时间,防止同一帧被多次触发。 + public void ConsumeWallCoyote() => _wallCoyoteTimer = 0f; + // ── 冲刺 ────────────────────────────────────────────────────────────── /// /// 施加冲刺速度(DashState/DashState 调用)。 @@ -210,12 +233,12 @@ namespace BaseGames.Player } /// - /// 壁滑:将垂直速度限制为 -WallSlideSpeed(受限抓墙时向下缓慢滑动)。 - /// WallSlideState.OnStateFixedUpdate 在受限模式下每帧调用。 + /// 壁滑:将垂直速度限制为 -speed(每帧调用以约束最大下滑速度)。 + /// WallSlideState.OnStateFixedUpdate 根据正常/受限模式传入不同速度。 /// - public void ApplyWallSlide() + public void ApplyWallSlide(float speed) { - float targetY = -_config.WallSlideSpeed; + float targetY = -speed; if (_rb.velocity.y < targetY) _rb.velocity = new Vector2(_rb.velocity.x, targetY); } diff --git a/Assets/_Game/Scripts/Player/PlayerMovementConfigSO.cs b/Assets/_Game/Scripts/Player/PlayerMovementConfigSO.cs index 02ef3fd..4d882c6 100644 --- a/Assets/_Game/Scripts/Player/PlayerMovementConfigSO.cs +++ b/Assets/_Game/Scripts/Player/PlayerMovementConfigSO.cs @@ -57,10 +57,16 @@ namespace BaseGames.Player [Header("抓墙 / 壁滑")] [Tooltip("受限抓墙时(高于 wallGrabY)的下滑速度(单位/秒)。推荐 2。")] public float WallSlideSpeed = 2f; + [Tooltip("正常抓墙时(低于等于 wallGrabY,可蹬墙跳区间)的缓慢下滑速度(单位/秒)。\n" + + "设为 0 = 完全静止悬挂;推荐 1,轻微下滑更有手感。")] + public float WallHangSpeed = 1f; public float WallRayLength = 0.55f; public float WallRayOffsetY = 0.2f; [Tooltip("抓墙高度容差:当前 Y 不超过 wallGrabY + 此值时视为未抬升,防止浮点抖动误判。")] public float WallGrabHeightTolerance = 0.05f; + [Tooltip("离开墙面后仍可触发蹬墙跳的缓冲时长(秒)。" + + "类比地面土狼时间,主动按↓脱离或墙面消失后,此窗口内按跳跃仍视为有效的蹬墙跳。推荐 0.12。")] + public float WallCoyoteTime = 0.12f; [Header("蹬墙跳 — 背墙跳(Jump Away,远离墙壁斜上方)")] [Tooltip("背墙跳水平速度(远离墙壁方向)。推荐 14。")] diff --git a/Assets/_Game/Scripts/Player/States/FallState.cs b/Assets/_Game/Scripts/Player/States/FallState.cs index c0a67ac..1ea8419 100644 --- a/Assets/_Game/Scripts/Player/States/FallState.cs +++ b/Assets/_Game/Scripts/Player/States/FallState.cs @@ -22,6 +22,20 @@ namespace BaseGames.Player.States public override void OnStateUpdate() { + // ── 墙壁土狼跳(优先于地面土狼跳 / 二段跳)────────────────────────── + // 离墙后 WallCoyoteTime 秒内按跳跃,视为有效蹬墙跳(背墙跳 / 对墙跳) + if (Move.HasWallCoyoteTime && Buffer.ConsumeJump()) + { + var wjs = Owner.GetState(); + if (wjs != null) + { + Move.ConsumeWallCoyote(); + wjs.PrepareEnter(Move.WallCoyoteDir, Input.MoveInput.x); + Owner.TransitionTo(wjs); + return; + } + } + // ── 跳跃输入(郊狼跳 / 二段跳)──────────────────────────────────── // 先确认有可用跳跃机会,再消耗缓冲,避免无操作时静默吃掉输入 if ((Move.HasCoyoteTime || Owner.AirJumpsLeft > 0) && Buffer.ConsumeJump()) diff --git a/Assets/_Game/Scripts/Player/States/WallSlideState.cs b/Assets/_Game/Scripts/Player/States/WallSlideState.cs index a5a5f65..322cde8 100644 --- a/Assets/_Game/Scripts/Player/States/WallSlideState.cs +++ b/Assets/_Game/Scripts/Player/States/WallSlideState.cs @@ -6,7 +6,17 @@ namespace BaseGames.Player.States /// 抓墙状态(自定义设计,架构 05_PlayerModule §2)。 /// /// 触发条件:空中贴墙时玩家按下朝向墙壁的方向键(由 FallState/JumpState 检测并调用 PrepareEnter)。 - /// 维持条件:进入后无需持续按键;主动按下反方向键或落地时解除。 + /// 维持条件:进入后无需持续按键;按下↓键或落地时解除;跳跃键触发蹬墙跳后由 WallJumpState 接管。 + /// + /// 脱离方式: + /// - 跳跃键 → 蹬墙跳(WallJumpState,由 OnJumpPressed 响应) + /// - ↓ 键 / 反方向键 → 主动脱离,进入 FallState;wall coyote 窗口内仍可触发蹬墙跳 + /// - 离开墙面 → 自然下落(FallState) + /// - 着地 → 闲置(IdleState) + /// + /// 下滑速度分为两档(均在 PlayerMovementConfigSO 配置): + /// - 正常模式(低于等于 wallGrabY,可蹬墙跳)→ WallHangSpeed(缓慢下滑) + /// - 受限模式(高于 wallGrabY,不可蹬墙跳) → WallSlideSpeed(较快下滑) /// /// 高度记忆机制(防止单面墙反复爬升): /// - 首次抓墙(或切换到另一侧墙壁)时记录 _wallGrabY。 @@ -73,9 +83,10 @@ namespace BaseGames.Player.States { var wd = Owner.WallDetector; - // 离开墙壁 → 下落 + // 离开墙壁 → 启动墙壁土狼时间后下落 if (wd == null || !wd.IsTouchingWall) { + Move.StartWallCoyote(_wallDir); Owner.TransitionTo(Owner.GetState()); return; } @@ -87,30 +98,35 @@ namespace BaseGames.Player.States return; } - // 主动按反方向键 → 脱离(松墙下落) - float mx = Input.MoveInput.x; - if (Mathf.Abs(mx) > 0.1f) + // 按下方向键 → 启动墙壁土狼时间后主动脱离,自然下落 + if (Input.MoveInput.y < -0.5f) { - int inputDir = mx > 0f ? 1 : -1; - if (inputDir != _wallDir) - { - Owner.TransitionTo(Owner.GetState()); - return; - } + Move.StartWallCoyote(_wallDir); + Owner.TransitionTo(Owner.GetState()); + return; + } + + // 按反方向键 → 启动墙壁土狼时间后脱离 + // wall coyote 存在时,离墙后短窗口内仍可触发蹬墙跳,不会误双跳 + float mx = Input.MoveInput.x; + if (Mathf.Abs(mx) > 0.1f && (mx > 0f ? 1 : -1) != _wallDir) + { + Move.StartWallCoyote(_wallDir); + Owner.TransitionTo(Owner.GetState()); + return; } - // 每帧刷新正常/受限状态 UpdateCanJump(); } public override void OnStateFixedUpdate() { if (_canJump) - // 正常模式:静止悬挂,阻止向下速度 - Move?.ZeroVerticalVelocity(); + // 正常模式(低于等于 wallGrabY,可蹬墙跳):缓慢下滑,给玩家操作窗口 + Move?.ApplyWallSlide(Cfg.WallHangSpeed); else - // 受限模式:持续下滑 - Move?.ApplyWallSlide(); + // 受限模式(高于 wallGrabY):较快下滑,不可蹬墙跳 + Move?.ApplyWallSlide(Cfg.WallSlideSpeed); } // ── 内部 ────────────────────────────────────────────────────────────── diff --git a/Docs/Standards/LayerSpec.md b/Docs/Standards/LayerSpec.md index d09e147..a31acd8 100644 --- a/Docs/Standards/LayerSpec.md +++ b/Docs/Standards/LayerSpec.md @@ -50,6 +50,9 @@ | Layer 名称 | 挂载对象 | 用途说明 | |---|---|---| | `Platform` | 静态地面、移动平台、Tilemap 地形、障碍物 | 玩家与敌人站立的实体平台;`PlayerMovement` 的 `_groundLayer`、`PlayerWallDetector` 的 `_wallLayer`(默认)均包含此 Layer;投射物命中后销毁 | +| `OneWayPlatform` | 单向平台(静态) | 玩家可从下方穿过、从上方站立的平台;挂载 `PhantomPlate`(`PlatformEffector2D` + `IDropThrough`);`_groundLayer` 必须包含此 Layer 才能检测 `OnOneWayPlatform` 状态 | +| `MovingOneWayPlatform` | 单向平台(移动) | 会移动的单向平台;行为同 `OneWayPlatform`,独立层便于移动脚本、AI 路径查询按层筛选 | +| `MidHeightOneWayPlatform` | 单向平台(半高) | 半腰高度的单向平台;角色可从侧方穿越;行为同 `OneWayPlatform` | | `Wall` | 墙壁碰撞体(垂直面) | 玩家攀墙检测(`PlayerWallDetector` 默认掩码包含 `Wall` 和 `Platform`);若地形使用统一的 Tilemap 则墙壁可合并到 `Platform` Layer | ### 2.4 触发区域 @@ -81,8 +84,14 @@ | `PlayerHitBox` | `EnemyHurtBox` | ✅ | 玩家攻击伤害敌人 | | `EnemyHitBox` | `PlayerHurtBox` | ✅ | 敌人攻击伤害玩家 | | `EnemyHitBox` | `EnemyHurtBox` | ✅ | 敌人可互相伤害(HitBox 运行时排除自身根节点) | -| `Player` | `Platform` | ✅ | 玩家站在平台上 | -| `Enemy` | `Platform` | ✅ | 敌人站在平台上 | +| `Player` | `Platform` | ✅ | 玩家站在实体平台上 | +| `Player` | `OneWayPlatform` | ✅ | 玩家站在单向平台上(PlatformEffector2D 控制单向穿透) | +| `Player` | `MovingOneWayPlatform` | ✅ | 玩家站在移动单向平台上 | +| `Player` | `MidHeightOneWayPlatform` | ✅ | 玩家站在半高单向平台上 | +| `Enemy` | `Platform` | ✅ | 敌人站在实体平台上 | +| `Enemy` | `OneWayPlatform` | ✅ | 敌人站在单向平台上 | +| `Enemy` | `MovingOneWayPlatform` | ✅ | 敌人站在移动单向平台上 | +| `Enemy` | `MidHeightOneWayPlatform` | ✅ | 敌人站在半高单向平台上 | | `PlayerProjectile` | `EnemyHurtBox` | ✅ | 玩家投射物伤害敌人 | | `PlayerProjectile` | `PlayerHurtBox` | ❌ | 玩家投射物不自伤 | | `PlayerProjectile` | `Platform` | ✅ | 玩家投射物命中地形 |