前言
做横版角色跳跃时,最直接的判断是“这一帧按下跳跃,并且角色在地面”。实际玩起来会出现一个问题:刚离开平台才按键,或者落地前一点按键,系统都不会起跳。
MetroidvaniaLike 里给这两种情况分别加了土狼时间和 Jump Buffer。它们本身只有几个计时器,麻烦的地方主要在消费时机,以及怎样和二段跳、墙滑、冲刺等状态一起工作。
这篇记录目前的实现思路和后续还需要补的状态优先级。
项目源码:qian488/MetroidvaniaLike。
这个系列
- 用类状态机组织动作角色
- 本文:土狼时间、输入缓冲与动作打断
- 敌人 AI、属性与技能系统
最开始的严格判断
严格跳跃条件通常是:
if (jumpPressedThisFrame && isGrounded)
Jump();
它要求“按键帧”和“接地帧”完全重叠。玩家在平台边缘晚按几十毫秒,或者落地前早按几十毫秒,都会失败。
项目为跳跃加入两个计时窗口。
土狼时间
玩家接地时刷新计时器,离地后递减:
if (isGrounded)
coyoteTimer = coyoteDuration;
else
coyoteTimer -= Time.deltaTime;
if (jumpPressed && coyoteTimer > 0f)
{
Jump();
coyoteTimer = 0f;
}
它允许玩家刚刚走出平台边缘后仍然起跳。计时器只影响跳跃条件;Fall、重力和动画继续读取实际接地状态。
窗口太短时改善不明显,太长时会出现明显空中起跳。可以在固定平台边缘场景反复测试,并在调试面板显示计时器,观察实际触发时机。
Jump Buffer
玩家在空中按下跳跃时,不立即执行,而是保存短暂请求:
if (jumpPressed)
jumpBufferTimer = jumpBufferDuration;
else
jumpBufferTimer -= Time.deltaTime;
if (isGrounded && jumpBufferTimer > 0f)
{
Jump();
jumpBufferTimer = 0f;
}
土狼时间和 Jump Buffer 是镜像关系:
土狼时间:有效状态先发生,输入稍后到
输入缓冲:输入先发生,有效状态稍后到
动作消费计时器后立即清零,避免一次输入触发两次跳跃。
输入缓冲还能用在哪里
同一模式可以用于攻击连段:玩家在当前攻击收尾前按下下一段攻击,将输入保留到连段窗口开放。
但不同动作需要不同缓冲策略:
- 跳跃:通常只保留最近一次;
- 连段:可能要记录动作类型;
- 冲刺:要考虑冷却和体力;
- 交互:目标离开范围后应立即失效。
项目后续可以把输入请求定义为类型、时间戳和过期条件的组合,再由当前状态选择是否消费。这样可以分别控制跳跃、攻击和冲刺的缓冲策略。
松开按键时缩短跳跃
只有固定跳高的角色很难精确落点。常见做法是在上升阶段提前松开跳跃键时削减向上速度:
if (jumpReleased && rigidbody.velocity.y > 0f)
{
rigidbody.velocity = new Vector2(
rigidbody.velocity.x,
rigidbody.velocity.y * jumpCutMultiplier);
}
轻点得到短跳,长按得到高跳。jumpCutMultiplier、重力和起跳初速度共同决定最终轨迹,需要放在同一个测试场景中调节。
如果使用新版本 Rigidbody2D API,具体速度属性可能不同,但设计原则不变:只在上升阶段削减,避免松键影响下落。
墙滑和墙跳为什么单独做状态
墙滑通常需要:
- 角色在空中且贴墙;
- 垂直速度被限制;
- 输入离墙时退出;
- 按跳跃后获得远离墙体的水平速度;
- 短时间内不应被普通空中移动立即拉回墙面。
如果墙跳后的第一帧又由普通空中控制覆盖水平速度,角色会看起来“贴着墙原地跳”。因此墙跳状态需要短暂控制锁定或独立的输入影响曲线,再交还给 Fall。
状态机的价值在这里很具体:墙跳期间谁拥有速度写入权是明确的。
冲刺、攻击和受伤怎样抢状态
动作增加以后,需要明确每个状态接受哪些切换请求。当前规则可以分成几类:
| 当前状态 | 请求动作 | 常见处理 |
|---|---|---|
| 普通移动 | 跳跃/攻击/冲刺 | 允许 |
| 普通攻击前摇 | 冲刺 | 按设计允许取消或拒绝 |
| 受伤硬直 | 普通移动 | 拒绝 |
| 死亡 | 所有普通动作 | 拒绝 |
| 空中 | 普通跳跃 | 仅有二段跳资格时允许 |
高优先级规则应集中表达,例如 Dead 只允许进入复活或结算状态。这样可以避免各状态分别实现后出现矛盾。
攻击取消则属于游戏设计选择。它需要明确:
- 哪一帧开始可取消;
- 哪些动作能取消;
- 是否消耗体力或技能次数;
- 输入是在窗口外丢弃还是缓冲。
这些规则可以由状态属性或配置描述。Animation Event 只发送命中帧、结束帧等事实事件。
输入在哪一帧读取
输入通常在 Update 采样,物理在 FixedUpdate 执行。直接只在 FixedUpdate 读取“本帧按下”可能漏掉发生在两个物理步之间的短输入。
一种稳妥链路是:
Update:采集按下/松开,写入输入缓冲
状态 Update:决定是否消费请求并切换状态
FixedUpdate:根据当前状态施加物理结果
无论采用哪种方案,都要保证单一动作不会在两个循环中重复消费。
调试时看哪些数据
手感最终需要人试玩,但底层不变量仍可测试:
- 离地后窗口内跳跃成功,窗口外失败;
- 落地前缓冲输入在落地时只触发一次;
- 松键只在上升阶段削减速度;
- 墙跳锁定期间普通水平输入不会立即覆盖速度;
- Hurt 和 Dead 不会被普通请求打断;
- 同一帧多个请求有确定优先顺序。
调试时建议显示:接地、贴墙、当前状态、coyote timer、buffer timer、当前速度和最近一次切换原因。比起凭感觉反复改参数,这能快速判断是检测、计时还是优先级错误。
写在最后
- 土狼时间和输入缓冲处理了按键与物理事件之间的短暂时间差。
- 土狼时间保留资格,Jump Buffer 保留意图,两者需要消费和过期机制。
- 变量跳跃让松键也参与控制轨迹。
- 墙跳和冲刺需要明确速度所有权,避免被普通移动覆盖。
- 动作增加后,需要同步维护打断优先级和输入消费顺序。
下一篇看敌人、属性和技能。状态机只管行为以后,伤害、Buff 和技能冷却还需要各自的模块接住。