PlayerMovementConfigSO.cs

新增 WallHangSpeed = 1f:正常抓墙(低于等于 wallGrabY,可蹬墙跳区间)的缓慢下滑速度
WallSlideSpeed 语义调整为:受限模式(高于 wallGrabY)的较快下滑速度
PlayerMovement.cs

ApplyWallSlide() 改为 ApplyWallSlide(float speed),由调用方传入对应速度
WallSlideState.cs

OnStateFixedUpdate:正常模式用 WallHangSpeed,受限模式用 WallSlideSpeed(两档清晰分离)
恢复反方向键脱离:脱离时同样调用 StartWallCoyote,wall coyote 窗口内仍能触发蹬墙跳
更新类头注释完整描述脱离方式和下滑速度档位Two wall slide improvements:
This commit is contained in:
2026-05-21 15:58:46 +08:00
parent 20b207ed58
commit fcd3e2dcdd
6 changed files with 106 additions and 26 deletions

View File

@@ -17,8 +17,14 @@ namespace BaseGames.Editor
/// · PlayerHitBox ↔ EnemyHurtBox → 应碰撞(玩家攻击伤害敌人)
/// · EnemyHitBox ↔ PlayerHurtBox → 应碰撞(敌人攻击伤害玩家)
/// · EnemyHitBox ↔ EnemyHurtBox → 应碰撞敌人可互相伤害HitBox 运行时排除自身根节点)
/// · Player ↔ Platform → 应碰撞(玩家站在平台上)
/// · Enemy ↔ Platform → 应碰撞(敌人站在平台上)
/// · Player ↔ Platform → 应碰撞(玩家站在实体平台上)
/// · PlayerOneWayPlatform → 应碰撞(玩家站在单向平台上)
/// · 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, "玩家投射物命中地形"),

View File

@@ -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;
// ── 墙壁土狼时间 ──────────────────────────────────────────────────────
/// <summary>
/// 由 WallSlideState 在切换到 FallState 时调用:启动墙壁土狼计时器。
/// 窗口内HasWallCoyoteTime == true按跳跃键仍可触发蹬墙跳。
/// wallDir+1 = 右墙 / -1 = 左墙。
/// </summary>
public void StartWallCoyote(int wallDir)
{
_wallCoyoteTimer = _config.WallCoyoteTime;
_wallCoyoteDir = wallDir;
}
/// <summary>消耗墙壁土狼时间,防止同一帧被多次触发。</summary>
public void ConsumeWallCoyote() => _wallCoyoteTimer = 0f;
// ── 冲刺 ──────────────────────────────────────────────────────────────
/// <summary>
/// 施加冲刺速度DashState/DashState 调用)。
@@ -210,12 +233,12 @@ namespace BaseGames.Player
}
/// <summary>
/// 壁滑:将垂直速度限制为 -WallSlideSpeed受限抓墙时向下缓慢滑动)。
/// WallSlideState.OnStateFixedUpdate 在受限模式下每帧调用
/// 壁滑:将垂直速度限制为 -speed每帧调用以约束最大下滑速度)。
/// WallSlideState.OnStateFixedUpdate 根据正常/受限模式传入不同速度
/// </summary>
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);
}

View File

@@ -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。")]

View File

@@ -22,6 +22,20 @@ namespace BaseGames.Player.States
public override void OnStateUpdate()
{
// ── 墙壁土狼跳(优先于地面土狼跳 / 二段跳)──────────────────────────
// 离墙后 WallCoyoteTime 秒内按跳跃,视为有效蹬墙跳(背墙跳 / 对墙跳)
if (Move.HasWallCoyoteTime && Buffer.ConsumeJump())
{
var wjs = Owner.GetState<WallJumpState>();
if (wjs != null)
{
Move.ConsumeWallCoyote();
wjs.PrepareEnter(Move.WallCoyoteDir, Input.MoveInput.x);
Owner.TransitionTo(wjs);
return;
}
}
// ── 跳跃输入(郊狼跳 / 二段跳)────────────────────────────────────
// 先确认有可用跳跃机会,再消耗缓冲,避免无操作时静默吃掉输入
if ((Move.HasCoyoteTime || Owner.AirJumpsLeft > 0) && Buffer.ConsumeJump())

View File

@@ -6,7 +6,17 @@ namespace BaseGames.Player.States
/// 抓墙状态(自定义设计,架构 05_PlayerModule §2
///
/// 触发条件:空中贴墙时玩家按下朝向墙壁的方向键(由 FallState/JumpState 检测并调用 PrepareEnter
/// 维持条件:进入后无需持续按键;主动按下反方向键或落地时解除
/// 维持条件:进入后无需持续按键;按下↓键或落地时解除;跳跃键触发蹬墙跳后由 WallJumpState 接管
///
/// 脱离方式:
/// - 跳跃键 → 蹬墙跳WallJumpState由 OnJumpPressed 响应)
/// - ↓ 键 / 反方向键 → 主动脱离,进入 FallStatewall 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<FallState>());
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<FallState>());
return;
}
Move.StartWallCoyote(_wallDir);
Owner.TransitionTo(Owner.GetState<FallState>());
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<FallState>());
return;
}
// 每帧刷新正常/受限状态
UpdateCanJump();
}
public override void OnStateFixedUpdate()
{
if (_canJump)
// 正常模式:静止悬挂,阻止向下速度
Move?.ZeroVerticalVelocity();
// 正常模式(低于等于 wallGrabY可蹬墙跳缓慢下滑给玩家操作窗口
Move?.ApplyWallSlide(Cfg.WallHangSpeed);
else
// 受限模式:持续下滑
Move?.ApplyWallSlide();
// 受限模式(高于 wallGrabY较快下滑不可蹬墙跳
Move?.ApplyWallSlide(Cfg.WallSlideSpeed);
}
// ── 内部 ──────────────────────────────────────────────────────────────

View File

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