LOADING

加载过慢请开启缓存 浏览器默认开启

MetroidvaniaLike 系列二:土狼时间、输入缓冲与动作打断

前言

做横版角色跳跃时,最直接的判断是“这一帧按下跳跃,并且角色在地面”。实际玩起来会出现一个问题:刚离开平台才按键,或者落地前一点按键,系统都不会起跳。

MetroidvaniaLike 里给这两种情况分别加了土狼时间和 Jump Buffer。它们本身只有几个计时器,麻烦的地方主要在消费时机,以及怎样和二段跳、墙滑、冲刺等状态一起工作。

这篇记录目前的实现思路和后续还需要补的状态优先级。

项目源码:qian488/MetroidvaniaLike

这个系列

  1. 用类状态机组织动作角色
  2. 本文:土狼时间、输入缓冲与动作打断
  3. 敌人 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 和技能冷却还需要各自的模块接住。