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` | ✅ | 玩家投射物命中地形 |