Как мне использовать requestAnimationFrame и setTimeout параллельно, чтобы улучшить игровой цикл?

Моя цель — создать эффективный игровой цикл, который использует requestAnimationFrame для обновления холста отображения и setTimeout для обновления игровой логики. Мой вопрос: следует ли помещать все операции рисования в цикл requestAnimationFrame или только основную операцию рисования, которая обновляет холст html?

Под «всеми операциями рисования» я подразумеваю всю буферизацию. Например, я рисовал все свои спрайты в буфере, а затем рисовал буфер на основном холсте. С одной стороны, если я перенесу всю буферизацию в requestAnimationFrame, я не буду тратить отрисовку ЦП на каждое обновление логики, с другой стороны, отрисовка сильно загружает ЦП и может привести к тому, что requestAniomationFrame будет ждать завершения всех этих операций... Смысл отделения логических обновлений от рисования заключается в том, чтобы 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();
    }
}

Спасибо!

Редактировать:

Я решил пойти с веб-воркерами, чтобы полностью отделить игровую логику от рисования, которое, насколько я понимаю, должно происходить в основном скрипте, загружаемом DOM.


person Frank    schedule 24.04.2015    source источник
comment
если update работает медленно, он все равно будет блокировать RAF. Асинхронный код не означает, что он многопоточный. Подумайте об использовании рабочих, если у вас есть проблемы с производительностью.   -  person zzzzBov    schedule 24.04.2015
comment
Я слышал о рабочих, знаете ли вы какую-нибудь хорошую документацию? И являются ли они способом многопоточного JavaScript?   -  person Frank    schedule 24.04.2015
comment
Хороший учебник по вебворкеру: html5rocks.com/en/tutorials/workers/basics убедитесь, что в системах, которые их не поддерживают (или не являются многоядерными), у вас есть запасной вариант!   -  person Rias    schedule 27.04.2015
comment
Привет @Frank, ты смог найти решение этой проблемы? Можете поделиться своими выводами, пожалуйста?   -  person Tahir Ahmed    schedule 03.06.2015


Ответы (2)


Итак, я так и не нашел отличного способа разделить логику и рисование, потому что 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 Кроме того, примечание: я пытался использовать веб-воркеры для разделения логики, и это было жалко. отказ. Веб-воркеры великолепны, когда у вас много математических операций и очень мало объектов для передачи между потоками, но они не могут рисовать и медленны при передаче больших объемов данных, по крайней мере, в контексте игровой логики.

person Frank    schedule 07.06.2015

Насколько я понимаю ваш вопрос, ключевыми моментами являются

  1. Я хочу обновлять экран как можно чаще
  2. Есть некоторые дорогостоящие операции, которые я не хочу выполнять при каждом обновлении экрана. Конечно, это означает, что есть что обновить, если нет, то предыдущий пункт бесполезен.
  3. У меня нет и не может быть флага, указывающего на необходимость выполнения предыдущих операций. Обратите внимание, что это разумный способ сделать это, а другие варианты являются лишь альтернативным выбором в случае, если это невозможно.

В вашем коде вы решили выполнять эти операции 20 раз в секунду.

В этом случае я бы установил метку времени, указывающую, когда эта операция была выполнена.

В коде requestAnimationFrame проверьте, не устарела ли эта метка времени более чем на 1/20 с, а затем выполните код.

person vals    schedule 25.04.2015
comment
К сожалению, я не могу избежать своей проблемы, используя один поток. Независимо от того, где я поместил логику интенсивного использования ЦП, она все равно будет выполняться до или после requestAnimationFrame асинхронно. В лучшем случае я смогу использовать requestAnimationFrame чаще, но у меня не будет новых обновлений логики для рендеринга, поэтому бессмысленно даже пытаться без второго потока. Я думаю, что мне придется больше изучать Web Workers и Transferable Objects, если я действительно хочу отделить логику от рисования. - person Frank; 26.04.2015