处理可变刷新率需求的其他方法
存在其他解决问题的方法。
一种常见的技术是以恒定的频率更新模拟,然后绘制尽可能多的(或尽可能少的)实际帧。更新方法可以继续循环,而不用考虑用户看到的内容。绘图方法可以查看最后的更新以及发生的时间。由于绘制知道何时表示,以及上次更新的模拟时间,它可以预测为用户绘制一个合理的框架。这是否比官方更新循环更频繁(甚至更不频繁)无关紧要。更新方法设置检查点,并且像系统允许的那样频繁地,渲染方法画出周围的时间。在 Web 标准中分离更新有很多种方法:
在 requestAnimationFrame() 中绘制,并在 setInterval() 或 setTimeout() 中更新。
即使在未聚焦或最小化的情况下,使用处理器时间,也可能是主线程,并且可能是传统游戏循环的工件(但是很简单)。
在 requestAnimationFrame() 中绘制,并在 Web Worker 的 setInterval() 或 setTimeout() 中对其进行更新。
这与上述相同,除了更新不会使主线程(主线程也没有)。这是一个更复杂的解决方案,并且对于简单更新可能会有太多的开销。
在 requestAnimationFrame() 中绘制,并使用它来戳一个包含更新方法的 Web Worker,其中包含要计算的刻度数(如果有的话)。
这个睡眠直到 requestAnimationFrame() 被调用并且不会污染主线程,加上你不依赖于老式的方法。再次,这比以前的两个选项更复杂一些,并且开始每个更新将被阻止,直到浏览器决定启动 rAF 回调。
这些方法中的每一种都有类似的权衡:
用户可以跳过渲染帧或根据其性能插入额外的帧。
你可以指望所有用户以相同的固定频率更新非修饰性的变量,而不会卡顿。
程序比我们前面看到的基本循环要复杂得多。
用户输入完全被忽略,直到下次更新(即使用户具有快速设备)。
强制插帧具有性能损失。
单独的更新和绘图方法可能类似于下面的示例。为了演示,该示例基于第三点,只是没有使用 Web Worker 以提高可读性(老实说,也包含可写性)。
警告:这个例子需要进行单独的技术审查。
js/*
* 以分号开头是为了以防此示例上方的代码行依赖于自动分号插入(ASI)。
* 浏览器可能会意外地认为整个示例从上一行继续。
* 如果前一行不为空或终止,则前面的分号标志着新行的开始。
*
* 我们还假设 MyGame 是以前定义的。
*
* MyGame.lastRender 跟踪上一次提供的 requestAnimationFrame 时间戳。
* MyGame.lastTick 跟踪上次更新时间。始终以 tickLength 递增。
* MyGame.tickLength 是游戏状态更新的频率。这里是 20 Hz(50ms)。
*
* timeSinceTick 是 requestAnimationFrame 回调和上一次更新之间的时间。
* numTicks 是这两个呈现帧之间应该发生的更新次数。
*
* render() 传入 tFrame, 因为 render 方法可能需要计算
* tFrame 距离最近的更新已经过去了多久,通过外推的方式
* 来获得场景数据。(对于快速设备,render 方法是纯装饰性的)。
* 用以绘制场景。
*
* update() 根据给定时间点计算游戏状态。通常需要用 tickLength
* 作为循环参数,递增更新。来保证游戏状态的严谨。传入 DOMHighResTimeStamp
* 代表当前时间。(除非需要增加暂停功能,传入的时间应该总是
* 上次更新时间 + 游戏的 tick 间隔。)
*
* setInitialState() 执行在运行主循环之前需要的任何任务。
* 它只是一个你可能添加的通用示例函数。
*/
;(() => {
function main(tFrame) {
MyGame.stopMain = window.requestAnimationFrame(main);
const nextTick = MyGame.lastTick + MyGame.tickLength;
let numTicks = 0;
// 如果 tFrame < nextTick,则需要更新 0 个 tick(对于 numTicks,默认为 0)。
// 如果 tFrame = nextTick,则需要更新 1 tick(等等)。
// 备注:正如我们在总结中提到的那样,你应该跟踪 numTicks 的大小。
// 如果它很大,要么你的游戏是卡住了,要么机器无法跟上。
if (tFrame > nextTick) {
const timeSinceTick = tFrame - MyGame.lastTick;
numTicks = Math.floor(timeSinceTick / MyGame.tickLength);
}
queueUpdates(numTicks);
render(tFrame);
MyGame.lastRender = tFrame;
}
function queueUpdates(numTicks) {
for (let i = 0; i < numTicks; i++) {
MyGame.lastTick += MyGame.tickLength; // 现在 lastTick 应是这一时间。
update(MyGame.lastTick);
}
}
MyGame.lastTick = performance.now();
MyGame.lastRender = MyGame.lastTick; // 假装第一次绘制是在第一次更新。
MyGame.tickLength = 50; // 这将使你的模拟运行在 20Hz(50ms)
setInitialState();
main(performance.now()); // 开始循环
})();
另一个选择是简单地做一些事情不那么频繁。如果你的更新循环的一部分难以计算但对时间不敏感,则可以考虑缩小其频率,理想情况下,在延长的时间段内将其扩展成块。这是一个隐含的例子,在火炮博物馆的炮兵游戏中,他们调整垃圾发生率来优化垃圾回收。显然,清理资源不是时间敏感的(特别是如果整理比垃圾本身更具破坏性)。
这也可能适用于你自己的一些任务。那些是当可用资源成为关注点时的好候选人。