为什么我的 144hz 显示器上的节流 requestAnimationFrame 运行 60FPS 不是?

Why isn't my throttled requestAnimationFrame running 60FPS on my 144hz monitor?

我想让我的 requestAnimationFrame 运行 60FPS 无论显示器刷新率如何,这样游戏在每个显示器上都会 运行 以相同的速度。
虽然它似乎有效,但当我尝试使用 chrome DevTools 性能选项卡时,我可以看到 FPS 就像 -> 48, 72, 48, 72....

这是我的 rAF 节流版本

let start;
let elapsed;
function startAnimating(timestamp) {
    const fpsInterval = 1000 / 60;

    if (start === undefined) {
        start = timestamp
    }

    elapsed = timestamp - start;


    if (elapsed >= fpsInterval) {
       start = timestamp - (elapsed % fpsInterval);
       move();
    }

    requestAnimationFrame(startAnimating);

}

TL;DR 是,requestAnimationFrame() 随时运行。你不能真正节流它。如果您想尝试这样做,您应该改用 setTimeout()setInternal()。他们将被称为更接近您的节流速率,通常为 +/- 4 毫秒。

或者你可以做你正在做的事情,如果你愿意的话,这就是你采取行动时的节流阀。您会在 DevTools 中看到正在调用 requestAnimationFrame() 的任何内容,但您的函数只会以 60FPS 或与其最匹配的任何速率执行 move()。但是,这可能会造成一些笨拙,因为如果 requestAnimationFrame() 每次调用之间的间隔与您的目标 FPS 不一致,您最终可能会得到比您想要的慢得多的有效 FP。

通常更好的方法是根据自上次调用以来的时间增量来缩放所有更改,例如移动:

 let lastTime = Date.now();
 
 function startAnimating() {
   const now = Date.now();
   const delta = now - lastTime;
   lastTime = now;

   move(delta); // delta is the ms since the last call

   requestAnimationFrame(delta);
 }

 function move(delta) {
   character.moveBy(speedInPxPerSecond * (delta / 1000));
 }

 startAnimating();

所以,如果你想让你的角色每秒移动 100 像素,如果你将 100 乘以 delta / 1000,你最终会得到一个精确(或非常非常接近)每秒移动 100 像素的角色,无论您的 FPS 是 10、30、60 还是 1000。

...so the game would run at the same speed on every monitor.

恕我直言,这不是最好的方法。 (不只是你,人们总是会犯这个错误。:-))无论你的 rAF 回调是在 30Hz、60Hz、144Hz 或任何。应该编写动画和 time-based 游戏逻辑的方式是查看回调的当前时间并确定在那一刻要渲染的内容。 rAF 回调应该 而不是 驱动游戏时钟。这应该基于实际时钟(例如,Date.now()performance.now())。

例如,这是一个错误完成的简单动画(基于对 rAF 的调用)。如果您的刷新率不是 60Hz,除了它会 运行 以错误的速度之外,浏览器会忙一分钟并且无法更新屏幕:

// Make the block go left to right in five seconds
// 5sec = 5,000ms. 5sec at 60Hz is 300 callbacks
// 100% / 300 = 0.333333334% per callback.
const block = document.getElementById("block");
let start = Date.now();
updateBlock();

let timerHandle = 0;
busyBrowser();

function busyBrowser() {
    timerHandle = setTimeout(() => {
      const stop = Date.now() + 100;
      while (Date.now() < stop); // NEVER DO THIS FOR REAL
      busyBrowser();
    }, 230);
}
function updateBlock() {
    let left = parseFloat(block.style.left || "0");
    left = Math.min(100, left + 0.333333334);
    block.style.left = left + "%";
    if (left === 100) {
        console.log(`Done after ${(Date.now() - start) / 1000} seconds`);
        clearInterval(timerHandle);
    } else {
        requestAnimationFrame(updateBlock);
    }
}
#channel {
    position: relative;
    height: 1rem;
}
#block {
    position: absolute;
    left: 0;
    top: 0;
    height: 1rem;
}
Should take five seconds to go left to right.
<div id="channel">
<div id="block">X</div>
</div>

在我的刷新率为 100Hz 的系统上,这需要四秒钟,因为它在两个方面是错误的:

  1. 我的刷新率是 100Hz,而不是 60Hz,但代码假设它是 60Hz。
  2. 有时浏览器忙于做“其他事情”(我的忙循环)而无法调用 rAF

四秒的唯一原因是延迟(#2);没有他们,就是三秒。

相反,rAF 中的代码应该查看时间,并根据当时的位置进行渲染:

// Make the block go left to right in five seconds
// 5sec = 5,000ms. 5sec at 60Hz is 300 callbacks
// 100% / 300 = 0.333333334% per callback.
const block = document.getElementById("block");
let start = Date.now();
let stop = start + 5000;
updateBlock();
let timerHandle = 0;
busyBrowser();

function busyBrowser() {
    timerHandle = setTimeout(() => {
      const stop = Date.now() + 100;
      while (Date.now() < stop); // NEVER DO THIS FOR REAL
      busyBrowser();
    }, 230);
}
function updateBlock() {
    const elapsed = Date.now() - start;
    let left = Math.min(
        100,
        elapsed * 0.02 // 100 / 5000 = 0.2% per ms
    );
    block.style.left = left + "%";
    if (left === 100) {
        console.log(`Done after ${elapsed / 1000} seconds`);
        clearInterval(timerHandle);
    } else {
        requestAnimationFrame(updateBlock);
    }
}
#channel {
    position: relative;
    height: 1rem;
}
#block {
    position: absolute;
    left: 0;
    top: 0;
    height: 1rem;
}
Should take five seconds to go left to right.
<div id="channel">
<div id="block">X</div>
</div>

尽管我的刷新率更快并且浏览器间歇性繁忙,但在我的机器上五秒钟就完成了。