画一条蛇不难。做成游戏后,一个很早要想清楚的问题是:屏幕每刷新一次,蛇就要走一格吗?

画面和规则,用两只钟

如果直接在 requestAnimationFrame 里前进一步,蛇的速度就会受屏幕刷新率影响。我的处理是:绘制可以尽量跟上屏幕,规则更新则按游戏自己的步长进行。

当前项目把规则放在 SnackEngine 中。引擎只关心蛇在哪里、食物有没有被吃掉、这一步是否碰撞;页面拿到状态,再把它画到 Canvas 上。这样改蛇的配色时,不需要同时修改碰撞规则。

function frame(t) {
  const dt = Math.min(t - (lastTime || t), 80);
  lastTime = t;
  if (e.status === 'running') {
    accumulator += dt;
    while (accumulator >= e.interval && e.status === 'running') {
      accumulator -= e.interval;
      e.step();
      for (const event of e.events) onEvent(event);
      updateUI();
    }
  }
  draw(t, dt);
  requestAnimationFrame(frame);
}

把零碎的时间攒起来

累加器 accumulator 保存还没有用掉的时间。一次绘制间隔不够走一步,就先攒着;攒够 e.interval,才更新一格,并减去这一步消耗的时间。这里的步长会随着吃到的食物数量变化,但不会直接跟屏幕帧率绑定。

代码还把单次传入的 dt 限制在 80 毫秒。它是一个取舍:切回标签页时,不要为了补上离开的时间,一口气把蛇推进很多格。因此,这里追求的是可玩的节奏,而不是在后台精确追赶现实时间。

暂停,要停哪些东西?

游戏暂停时,规则引擎不再前进;蛇、牵线、跳大和斧头使用的游戏时间也一起停住。页面仍然可以继续绘制,因此暂停浮层和背景不需要跟着整页冻结。语音的暂停由语音管理器单独处理。

这次记住的一件事

先区分“状态应该怎样变化”和“状态应该怎样显示”,后面加效果会轻松很多。圆框眼镜、粉蓝蛇身、定身时的提示,都可以在同一套规则之上慢慢调整。