我应该如何并行使用 requestAnimationFrame 和 setTimeout 来制作更好的游戏循环?
How should I use requestAnimationFrame and setTimeout in parallel to make a better game loop?
我的目标是创建一个高效的游戏循环,使用 requestAnimationFrame
更新显示 canvas 和 setTimeout
更新游戏逻辑。我的问题是我应该将所有绘图操作放在 requestAnimationFrame
循环中还是只放在更新 html canvas 的主绘图操作中?
我所说的 "all the drawing operations" 是所有的缓冲。例如,我会将所有精灵绘制到缓冲区,然后将缓冲区绘制到主 canvas。一方面,如果我将所有缓冲都放入 requestAnimationFrame
,我就不会在每次逻辑更新时浪费 cpu 绘图,另一方面,绘图是 cpu 繁重的,可能会导致requestAniomationFrame
等待所有这些操作完成...将逻辑更新与绘图分开的目的是 requestAnimationFrame
不会因非绘图处理而陷入困境。
有没有人对这种创建游戏循环的方法有任何经验?不要说 "just put it all in requestAnimationFrame
," 因为这会减慢渲染速度。我相信将逻辑与绘图分开是可行的方法。这是我正在谈论的示例:
/* The drawing loop. */
function render(time_stamp_){//First parameter of RAF callback is timestamp.
window.requestAnimationFrame(render);
/* Draw all my sprites in the render function? */
/* Or should I move this to the logic loop? */
for (var i=sprites.length-1;i>-1;i--){
sprites[i].drawTo(buffer);
}
/* Update the on screen canvas. */
display.drawImage(buffer.canvas,0,0,100,100,0,0,100,100);
}
/* The logic loop. */
function update(){
window.setTimeout(update,20);
/* Update all my sprites. */
for (var i=sprites.length-1;i>-1;i--){
sprites[i].update();
}
}
谢谢!
编辑:
我决定与网络工作者一起将游戏逻辑与绘图完全分开,据我所知,这必须发生在 DOM 加载的主脚本中。
据我了解你的问题,重点是
- 我想尽可能频繁地刷新屏幕
- 有一些我不想在每次屏幕刷新时都执行的昂贵操作。当然,这意味着还有其他东西要刷新,如果没有,前面的点也没用
- 我没有,也不可能有指示需要前面操作的标志。请注意,这是明智的做法,其他选项只是在不可能的情况下的替代选择
在您的代码中,您已决定每秒执行此操作 20 次。
在这种情况下,我会设置一个时间戳,指示此操作何时完成。
在requestAnimationFrame代码中,测试这个时间戳是否老化超过1/20秒,然后执行代码。
因此,我从未找到分离逻辑和绘图的好方法,因为 JavaScript 使用单线程。无论我做什么,绘制函数的执行都可能妨碍逻辑,反之亦然。我所做的是找到一种方法来尽可能及时地执行它们,同时确保不断更新逻辑并使用 requestAnimation Frame 优化绘图。如果设备太慢而无法以所需的帧速率绘制,则该系统设置为插入动画以弥补跳过的帧。无论如何,这是我的代码。
var engine = {
/* FUNCTIONS. */
/* Starts the engine. */
/* interval_ is the number of milliseconds to wait between updating the logic. */
start : function(interval_) {
/* The accumulated_time is how much time has passed between the last logic update and the most recent call to render. */
var accumulated_time = interval_;
/* The current time is the current time of the most recent call to render. */
var current_time = undefined;
/* The amount of time between the second most recent call to render and the most recent call to render. */
var elapsed_time = undefined;
/* You need a reference to this in order to keep track of timeout and requestAnimationFrame ids inside the loop. */
var handle = this;
/* The last time render was called, as in the time that the second most recent call to render was made. */
var last_time = Date.now();
/* Here are the functions to be looped. */
/* They loop by setting up callbacks to themselves inside their own execution, thus creating a string of endless callbacks unless intentionally stopped. */
/* Each function is defined and called immediately using those fancy parenthesis. This keeps the functions totally private. Any scope above them won't know they exist! */
/* You want to call the logic function first so the drawing function will have something to work with. */
(function logic() {
/* Set up the next callback to logic to perpetuate the loop! */
handle.timeout = window.setTimeout(logic, interval_);
/* This is all pretty much just used to add onto the accumulated time since the last update. */
current_time = Date.now();
/* Really, I don't even need an elapsed time variable. I could just add the computation right onto accumulated time and save some allocation. */
elapsed_time = current_time - last_time;
last_time = current_time;
accumulated_time += elapsed_time;
/* Now you want to update once for every time interval_ can fit into accumulated_time. */
while (accumulated_time >= interval_) {
/* Update the logic!!!!!!!!!!!!!!!! */
red_square.update();
accumulated_time -= interval_;
}
})();
/* The reason for keeping the logic and drawing loops separate even though they're executing in the same thread asynchronously is because of the nature of timer based updates in an asynchronously updating environment. */
/* You don't want to waste any time when it comes to updating; any "naps" taken by the processor should be at the very end of a cycle after everything has already been processed. */
/* So, say your logic is wrapped in your RAF loop: it's only going to run whenever RAF says it's ready to draw. */
/* If you want your logic to run as consistently as possible on a set interval, it's best to keep it separate, because even if it has to wait for the RAF or input events to be processed, it still might naturally happen before or after those events, and we don't want to force it to occur at an earlier or later time if we don't have to. */
/* Ultimately, keeping these separate will allow them to execute in a more efficient manner rather than waiting when they don't have to. */
/* And since logic is way faster to update than drawing, drawing won't have to wait that long for updates to finish, should they happen before RAF. */
/* time_stamp_ is an argument accepted by the callback function of RAF. It records a high resolution time stamp of when the function was first executed. */
(function render(time_stamp_) {
/* Set up the next callback to RAF to perpetuate the loop! */
handle.animation_frame = window.requestAnimationFrame(render);
/* You don't want to render if your accumulated time is greater than interval_. */
/* This is dropping a frame when your refresh rate is faster than your logic can update. */
/* But it's dropped for a good reason. If interval > accumulated_time, then no new updates have occurred recently, so you'd just be redrawing the same old scene, anyway. */
if (accumulated_time < interval_) {
buffer.clearRect(0, 0, buffer.canvas.width, buffer.canvas.height);
/* accumulated_time/interval_ is the time step. */
/* It should always be less than 1. */
red_square.draw(accumulated_time / interval_);
html.output.innerHTML = "Number of warps: " + red_square.number_of_warps;
/* Always do this last. */
/* This updates the actual display canvas. */
display.clearRect(0, 0, display.canvas.width, display.canvas.height);
display.drawImage(buffer.canvas, 0, 0, buffer.canvas.width, buffer.canvas.height, 0, 0, display.canvas.width, display.canvas.height);
}
})();
},
/* Stops the engine by killing the timeout and the RAF. */
stop : function() {
window.cancelAnimationFrame(this.animation_frame);
window.clearTimeout(this.timeout);
this.animation_frame = this.timeout = undefined;
},
/* VARIABLES. */
animation_frame : undefined,
timeout : undefined
};
这是直接从我的一个项目中提取出来的,因此其中有一些变量是在代码的其他地方定义的。 red_square 是这些变量之一。如果您想查看完整示例,请查看我的 github 页面! userpoth.github.io 另外,附带说明一下,我尝试使用网络工作者来分离逻辑,但这是一个悲惨的失败。当你有很多数学运算而线程之间传递的对象很少时,Web workers 很棒,但他们不能绘图,而且他们在大数据传输方面很慢,至少在游戏逻辑的上下文中是这样。
我的目标是创建一个高效的游戏循环,使用 requestAnimationFrame
更新显示 canvas 和 setTimeout
更新游戏逻辑。我的问题是我应该将所有绘图操作放在 requestAnimationFrame
循环中还是只放在更新 html canvas 的主绘图操作中?
我所说的 "all the drawing operations" 是所有的缓冲。例如,我会将所有精灵绘制到缓冲区,然后将缓冲区绘制到主 canvas。一方面,如果我将所有缓冲都放入 requestAnimationFrame
,我就不会在每次逻辑更新时浪费 cpu 绘图,另一方面,绘图是 cpu 繁重的,可能会导致requestAniomationFrame
等待所有这些操作完成...将逻辑更新与绘图分开的目的是 requestAnimationFrame
不会因非绘图处理而陷入困境。
有没有人对这种创建游戏循环的方法有任何经验?不要说 "just put it all in requestAnimationFrame
," 因为这会减慢渲染速度。我相信将逻辑与绘图分开是可行的方法。这是我正在谈论的示例:
/* The drawing loop. */
function render(time_stamp_){//First parameter of RAF callback is timestamp.
window.requestAnimationFrame(render);
/* Draw all my sprites in the render function? */
/* Or should I move this to the logic loop? */
for (var i=sprites.length-1;i>-1;i--){
sprites[i].drawTo(buffer);
}
/* Update the on screen canvas. */
display.drawImage(buffer.canvas,0,0,100,100,0,0,100,100);
}
/* The logic loop. */
function update(){
window.setTimeout(update,20);
/* Update all my sprites. */
for (var i=sprites.length-1;i>-1;i--){
sprites[i].update();
}
}
谢谢!
编辑:
我决定与网络工作者一起将游戏逻辑与绘图完全分开,据我所知,这必须发生在 DOM 加载的主脚本中。
据我了解你的问题,重点是
- 我想尽可能频繁地刷新屏幕
- 有一些我不想在每次屏幕刷新时都执行的昂贵操作。当然,这意味着还有其他东西要刷新,如果没有,前面的点也没用
- 我没有,也不可能有指示需要前面操作的标志。请注意,这是明智的做法,其他选项只是在不可能的情况下的替代选择
在您的代码中,您已决定每秒执行此操作 20 次。
在这种情况下,我会设置一个时间戳,指示此操作何时完成。
在requestAnimationFrame代码中,测试这个时间戳是否老化超过1/20秒,然后执行代码。
因此,我从未找到分离逻辑和绘图的好方法,因为 JavaScript 使用单线程。无论我做什么,绘制函数的执行都可能妨碍逻辑,反之亦然。我所做的是找到一种方法来尽可能及时地执行它们,同时确保不断更新逻辑并使用 requestAnimation Frame 优化绘图。如果设备太慢而无法以所需的帧速率绘制,则该系统设置为插入动画以弥补跳过的帧。无论如何,这是我的代码。
var engine = {
/* FUNCTIONS. */
/* Starts the engine. */
/* interval_ is the number of milliseconds to wait between updating the logic. */
start : function(interval_) {
/* The accumulated_time is how much time has passed between the last logic update and the most recent call to render. */
var accumulated_time = interval_;
/* The current time is the current time of the most recent call to render. */
var current_time = undefined;
/* The amount of time between the second most recent call to render and the most recent call to render. */
var elapsed_time = undefined;
/* You need a reference to this in order to keep track of timeout and requestAnimationFrame ids inside the loop. */
var handle = this;
/* The last time render was called, as in the time that the second most recent call to render was made. */
var last_time = Date.now();
/* Here are the functions to be looped. */
/* They loop by setting up callbacks to themselves inside their own execution, thus creating a string of endless callbacks unless intentionally stopped. */
/* Each function is defined and called immediately using those fancy parenthesis. This keeps the functions totally private. Any scope above them won't know they exist! */
/* You want to call the logic function first so the drawing function will have something to work with. */
(function logic() {
/* Set up the next callback to logic to perpetuate the loop! */
handle.timeout = window.setTimeout(logic, interval_);
/* This is all pretty much just used to add onto the accumulated time since the last update. */
current_time = Date.now();
/* Really, I don't even need an elapsed time variable. I could just add the computation right onto accumulated time and save some allocation. */
elapsed_time = current_time - last_time;
last_time = current_time;
accumulated_time += elapsed_time;
/* Now you want to update once for every time interval_ can fit into accumulated_time. */
while (accumulated_time >= interval_) {
/* Update the logic!!!!!!!!!!!!!!!! */
red_square.update();
accumulated_time -= interval_;
}
})();
/* The reason for keeping the logic and drawing loops separate even though they're executing in the same thread asynchronously is because of the nature of timer based updates in an asynchronously updating environment. */
/* You don't want to waste any time when it comes to updating; any "naps" taken by the processor should be at the very end of a cycle after everything has already been processed. */
/* So, say your logic is wrapped in your RAF loop: it's only going to run whenever RAF says it's ready to draw. */
/* If you want your logic to run as consistently as possible on a set interval, it's best to keep it separate, because even if it has to wait for the RAF or input events to be processed, it still might naturally happen before or after those events, and we don't want to force it to occur at an earlier or later time if we don't have to. */
/* Ultimately, keeping these separate will allow them to execute in a more efficient manner rather than waiting when they don't have to. */
/* And since logic is way faster to update than drawing, drawing won't have to wait that long for updates to finish, should they happen before RAF. */
/* time_stamp_ is an argument accepted by the callback function of RAF. It records a high resolution time stamp of when the function was first executed. */
(function render(time_stamp_) {
/* Set up the next callback to RAF to perpetuate the loop! */
handle.animation_frame = window.requestAnimationFrame(render);
/* You don't want to render if your accumulated time is greater than interval_. */
/* This is dropping a frame when your refresh rate is faster than your logic can update. */
/* But it's dropped for a good reason. If interval > accumulated_time, then no new updates have occurred recently, so you'd just be redrawing the same old scene, anyway. */
if (accumulated_time < interval_) {
buffer.clearRect(0, 0, buffer.canvas.width, buffer.canvas.height);
/* accumulated_time/interval_ is the time step. */
/* It should always be less than 1. */
red_square.draw(accumulated_time / interval_);
html.output.innerHTML = "Number of warps: " + red_square.number_of_warps;
/* Always do this last. */
/* This updates the actual display canvas. */
display.clearRect(0, 0, display.canvas.width, display.canvas.height);
display.drawImage(buffer.canvas, 0, 0, buffer.canvas.width, buffer.canvas.height, 0, 0, display.canvas.width, display.canvas.height);
}
})();
},
/* Stops the engine by killing the timeout and the RAF. */
stop : function() {
window.cancelAnimationFrame(this.animation_frame);
window.clearTimeout(this.timeout);
this.animation_frame = this.timeout = undefined;
},
/* VARIABLES. */
animation_frame : undefined,
timeout : undefined
};
这是直接从我的一个项目中提取出来的,因此其中有一些变量是在代码的其他地方定义的。 red_square 是这些变量之一。如果您想查看完整示例,请查看我的 github 页面! userpoth.github.io 另外,附带说明一下,我尝试使用网络工作者来分离逻辑,但这是一个悲惨的失败。当你有很多数学运算而线程之间传递的对象很少时,Web workers 很棒,但他们不能绘图,而且他们在大数据传输方面很慢,至少在游戏逻辑的上下文中是这样。