帧率和显示器刷新率导致延迟

framerate and monitor refresh rate causes lag

我一直在为游戏开发在 html5 和 canvas 上做一些测试编码,并且 运行 遇到了一个我无法通过的错误。当我 运行 一个带有 requestAnimationFrame 的基本动画循环时发生的事情,对象的速度被更新,运动是平滑的,但是后来我 运行 带有显示器的计算机上的脚本有一个144Hz刷新率(从60Hz到144Hz),让我的梦想跌入深渊

所以我开始阅读 delta time 以及它如何解决游戏中 fps 的问题,它确实有效,但不如预期。

function update(timestamp = Date.now()){
    if(!previous) previous = timestamp;
    dt = (timestamp - previous) / 1000;
    fps = 1000 / (timestamp - previous);
    previous = timestamp;
...
   window.requestAnimationFrame(update);
}
window.requestAnimationFrame(update);

我用 requestAnimationFrame 调用更新,但它也可以在没有 (timestamp = Date.now()) 的情况下完成,得到正确的信息 fps => 143.78... dt = 0.006978...

    this.vx = 2;

...

    this.x += this.vx * dT * fps;
    this.y += this.vy * dT * fps;
    this.vy += gravity;
...

计算检查 144 和 60 Hz 显示器(2 * 0.0069 * 144 = 2.001 和 2 * 0.016 * 60 = 2.001),但是当你 运行 它在 60Hz 时有太多延迟监视器。 2px 的移动并不像它应该的那样平滑;这让我想到了我的问题,这个问题有解决办法吗?

如果你已经有一个为 60Hz 制作的工作代码并且你想修复它以在任何帧速率下工作,最简单的方法是将你当前的绝对值转换为以 px per ms 表示的速度值(或秒,无关紧要)。

例如在你的情况下,你会做

const expectedFrameRate = 60;
obj.vx = 2 * (expectedFrameRate / 1000); // px per ms

将所有这些值转换为速度后,只需将其乘以自上一帧以来经过的时间即可:

function animate(timestamp) {
  const dt = (timestamp - previous);
  previous = timestamp;
  
  update(dt);
  requestAnimationFrame(animate);
}
function update(dt) {
  // ...
  obj.x += obj.vx * dt; // no matter how it long it took
                        // to render the last frame
                        // it will be on the correct position
  // ...

这是一个小演示,展示了“基于速度”的动画如何无论计时器的精度如何都能保持正确的位置:

const canvases = document.querySelectorAll("canvas");
canvases.forEach( (c) => c.height = 60);
const expectedFrameRate = 60;

class Obj {
  constructor(canvas, color) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.color = color;
    this.y = 0;
    this.x = 0;
    this.width = 60;
    this.height = 60;
    this.vx = 2;
    this.xPerMs = 2 * (expectedFrameRate / 1000);
  }
  draw() {
    const { ctx, color, x, y, width, height } = this;
    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    ctx.fillStyle = color;
    ctx.fillRect(x, y, width, height);
  }
};

// red is simple x += vx
// completely dependent on the framerate
// will be late if switching tab or if the browser can't keep up @60FPS
{
  const canvas = canvases[0];
  const obj = new Obj(canvas, "red");
  const anim = (timestamp) => {
    obj.x = (obj.x + obj.vx) % canvas.width;
    obj.draw();
    requestAnimationFrame(anim);
  };
  requestAnimationFrame(anim);
}
// green uses speed, inside rAF callback
// smooth and correct
{
  const canvas = canvases[1];
  const obj = new Obj(canvas, "green");
  let previous = document?.timeline?.currentTime || performance.now();
  const anim = (timestamp) => {
    const dt = timestamp - previous;
    previous = timestamp;
    obj.x = (obj.x + obj.xPerMs * dt) % canvas.width;
    obj.draw();
    requestAnimationFrame(anim);
  };
  requestAnimationFrame(anim);
}
// blue uses speed, inside random timeout callback
// expect hiccups, but "correct" overall position
{
  const canvas = canvases[2];
  const obj = new Obj(canvas, "blue");
  let previous = performance.now();
  const anim = () => {
    const timestamp = performance.now();
    const dt = timestamp - previous;
    previous = timestamp;
    obj.x = (obj.x + obj.xPerMs * dt) % canvas.width;
    obj.draw();
    setTimeout(anim, Math.random() * 100);
  };
  setTimeout(anim, Math.random() * 100);
}
canvas { border: 1px solid; display: block }
<canvas></canvas>
<canvas></canvas>
<canvas></canvas>