简体   繁体   English

我应该如何并行使用requestAnimationFrame和setTimeout来制作更好的游戏循环?

[英]How should I use requestAnimationFrame and setTimeout in parallel to make a better game loop?

My goal is to create an efficient game loop that uses requestAnimationFrame for updating the display canvas and setTimeout for updating the game logic. 我的目标是创建一个有效的游戏循环,使用requestAnimationFrame更新显示画布,使用setTimeout更新游戏逻辑。 My question is should I put all the drawing operations inside the requestAnimationFrame loop or only the main drawing operation that updates the html canvas? 我的问题是我应该将所有绘图操作放在requestAnimationFrame循环中还是只放置更新html画布的主绘图操作?

What I mean by "all the drawing operations" is all of the buffering. 我所说的“所有绘图操作”都是缓冲。 For instance, I'd draw all my sprites to the buffer and then draw the buffer to the main canvas. 例如,我将所有精灵绘制到缓冲区,然后将缓冲区绘制到主画布。 On the one hand, if I put all the buffering into requestAnimationFrame I won't be wasting cpu drawing on each logic update, on the other hand, drawing is cpu heavy and could cause requestAniomationFrame to wait until all those operations are finished... The point of separating logic updates from drawing is so that requestAnimationFrame doesn't get bogged down by non-drawing processing. 一方面,如果我将所有缓冲放入requestAnimationFrame我将不会在每次逻辑更新上浪费cpu绘图,另一方面,绘图是cpu很重并且可能导致requestAniomationFrame等待所有这些操作完成...将逻辑更新与绘图分离的要点是, requestAnimationFrame不会因非绘图处理而陷入困境。

Does anyone have any experience with this approach to creating a game loop? 有没有人有这种方法来创建游戏循环的经验? And don't say "just put it all in requestAnimationFrame ," because this does slow down rendering. 并且不要说“只是将它全部放在requestAnimationFrame中”,因为这确实会降低渲染速度。 I'm convinced that separating logic from drawing is the way to go. 我确信将逻辑与绘图分离是可行的方法。 Here's an example of what I'm talking about: 这是我正在谈论的一个例子:

/* 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();
    }
}

Thanks! 谢谢!

Edit: 编辑:

I've decided to go with web workers to completely separate the game logic from the drawing, which from what I understand, must take place in the main script loaded by the DOM. 我决定与网络工作者一起将游戏逻辑与绘图完全分开,根据我的理解,这必须在DOM加载的主脚本中进行。

So, I never found a great way to separate logic and drawing because JavaScript uses a single thread. 因此,我从未找到一种分离逻辑和绘图的好方法,因为JavaScript使用单个线程。 No matter what I do the execution of the draw function may get in the way of the logic or vis versa. 无论我做什么,绘制函数的执行都可能妨碍逻辑,反之亦然。 What I did do was find a way to execute them in the most timely manner possible while also ensuring constant time updates to the logic and optimized drawing using requestAnimation Frame. 我所做的是找到一种方法以尽可能最及时的方式执行它们,同时确保使用requestAnimation Frame对逻辑和优化绘图进行持续的时间更新。 This system is set up to interpolate animations to make up for skipped frames should the device be too slow to draw at the desired frame rate. 如果设备太慢而无法以所需的帧速率绘制,则该系统被设置为插入动画以弥补跳过的帧。 Anyway, here's my code. 无论如何,这是我的代码。

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
    };

This is ripped straight out of one of my projects so there are a few variables in there that are defined elsewhere in code. 这是从我的一个项目中直接撕掉的,所以在那里有一些变量在代码的其他地方定义。 red_square is one of those variables. red_square是其中一个变量。 If you want to check out the full example, take a look at my github page! 如果您想查看完整示例,请查看我的github页面! userpoth.github.io Also, a side note, I tried using web workers to separate out logic and it was a miserable failure. userpoth.github.io另外,旁注,我尝试使用网络工作者来分离逻辑,这是一个悲惨的失败。 Web workers are great when you have a lot of math to do and very few objects to pass between threads, but they can't do drawing and they are slow with big data transfers, at least in the context of game logic. 当你有很多数学要做的事情并且很少有对象在线程之间传递时,网络工作者是很棒的,但是他们无法进行绘图,并且在大数据传输时它们很慢,至少在游戏逻辑的上下文中。

As I understand your question, the key points are 据我了解你的问题,关键点是

  1. I want to refresh the screen as often as possible 我想尽可能经常刷新屏幕
  2. There is some expensive operations that I don't want to do on every screen refresh. 在每次屏幕刷新时我都不想做一些昂贵的操作。 Of course, that means that there are other things to refresh, if not, the prior point is useless 当然,这意味着还有其他事情要刷新,如果没有,先前的观点是没用的
  3. I don't have, and can't have, a flag indicating that the previous operations are needed. 我没有,也没有,有一个标志,表明需要以前的操作。 Note that this is the sensible way to do it, and the other options are only a alternate choice in case this is not possible 请注意,这是一种明智的方法,其他选项只是在不可能的情况下的替代选择

In your code, you have decided to do this operations 20 times per second. 在您的代码中,您决定每秒执行此操作20次。

In this case, I would set a timestamp indicating when this operations have been done. 在这种情况下,我会设置一个时间戳,指示此操作何时完成。

In the requestAnimationFrame code, test if this timestamp has aged more than 1/20 s, and then execute the code. 在requestAnimationFrame代码中,测试此时间戳是否已超过1/20秒,然后执行代码。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM