如何同时使用requestAnimationFrame和setTimeout来制作更好的游戏循环?

5
我的目标是创建一个高效的游戏循环,使用requestAnimationFrame更新显示画布和setTimeout更新游戏逻辑。我的问题是,我应该把所有绘图操作都放在requestAnimationFrame循环内,还是只放主要的绘图操作来更新html画布?
所谓“所有绘图操作”是指所有缓冲区操作。例如,我会把所有精灵绘制到缓冲区,然后将缓冲区绘制到主画布上。一方面,如果我把所有缓冲区操作都放在requestAnimationFrame中,就不会浪费CPU在每个逻辑更新上进行绘制,另一方面,绘制是CPU密集型的,可能会导致requestAnimationFrame等待直到所有这些操作完成... 将逻辑更新与绘制分开的重点是,让requestAnimationFrame不会被非绘制处理拖慢。
有没有人有使用这种方法创建游戏循环的经验?不要说“把它全部放在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();
    }
}

谢谢!

编辑:

我决定使用Web Workers来完全将游戏逻辑与绘图分离,从我的理解来看,这必须在由DOM加载的主脚本中进行。


1
如果 update 运行缓慢,它仍会阻塞 RAF。异步代码并不意味着它是多线程的。如果您遇到性能问题,请考虑使用 workers。 - zzzzBov
我听说过工作线程,你知道有什么好的文档吗?它们是否是JavaScript实现多线程的一种方式? - Frank
一个好的WebWorker教程是这个:http://www.html5rocks.com/en/tutorials/workers/basics/ 确保在不支持它们(或不是多核)的系统中有备选方案! - Rias
嗨@Frank,你能找到解决方案吗?可以分享一下你的发现吗? - Tahir Ahmed
2个回答

3
所以,我从未找到一种很好的方法来分离逻辑和绘图,因为JavaScript使用单个线程。无论我做什么,绘制函数的执行可能会妨碍逻辑或相反。我所做的是找到一种在最及时的情况下执行它们的方法,同时确保对逻辑进行恒定时间更新并使用requestAnimationFrame进行优化绘图。该系统设置了插值动画,以弥补如果设备过慢而无法按所需帧速率绘制的跳帧。无论如何,以下是我的代码。
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来分离逻辑,但失败了。当你需要进行大量数学计算且需要在线程之间传递很少的对象时,Web Workers非常好用,但是它们无法绘制图形,并且在处理大数据传输时速度较慢,至少在游戏逻辑的上下文中是这样的。

1
我理解你的问题,主要有以下几点:
  1. 我希望屏幕尽可能频繁地刷新
  2. 有一些昂贵的操作,我不想在每次屏幕刷新时都执行。当然,这意味着如果没有其他需要刷新的内容,第一点就是无用的
  3. 我没有,并且不能有一个标志来指示之前的操作是必需的。请注意,这是明智的做法,其他选择只是在无法实现此方法时的备选方案

在你的代码中,你决定每秒执行这些操作20次。

在这种情况下,我会设置一个时间戳,指示何时执行这些操作。

在requestAnimationFrame代码中,测试该时间戳是否已经超过1/20秒,然后执行代码。


不幸的是,我无法使用单个线程避免我的问题。无论我把CPU密集型逻辑放在哪里,它仍然会在异步执行requestAnimationFrame之前或之后执行。最好的情况是我将能够更频繁地使用requestAnimationFrame,但我没有任何新的逻辑更新可供渲染,因此即使没有第二个线程也是无用的。我认为我必须更深入地研究Web Workers和Transferable Objects,如果我要真正将逻辑与绘图分离。 - Frank

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接