вторник, 11 августа 2015 г.

Статья об игровом цикле 

автор Koen Witters 

http://www.koonsolo.com/news/dewitters-gameloop/

перевод Валерий Старощук

    Игровой цикл – это сердце любой игры, ни одна игра не может обойтись без него. Но, к сожалению, для начинающих программистов нет хороших статей, которые бы раскрыли эту тему на должном уровне. Но не переживайте, вы только что натолкнулись на статью, которая описывает игровой цикл так, как он этого заслуживает. Благодаря моей работе, как программиста игр, я просмотрел большое количество кода маленьких мобильных игр и всегда поражался, как реализован в них игровой цикл. Вы можете спросить себя, как такая простая вещь, как игровой цикл, может быть реализована по-разному. Однако такое возможно, и я буду обсуждать все за и против самых популярных реализаций, что даст вам (на мой взгляд) оптимальное решение реализации игрового цикла.

Игровой цикл

   Любая игра состоит из последовательности действий, таких как: получение входных данных от пользователя, обновление состояния игры, обработка логики, проигрывание музыки и звуковых эффектов и отображение игры.
            Эта последовательность обрабатывается с помощью игрового цикла. Как я сказал в начале, игровой цикл, как сердцебиение, он задает ритм в игре. В этой статье я не буду вдаваться в подробности задач решаемых в игровом цикле, мы сосредоточимся на самом цикле. Вот почему я упростил задачи до 2 функций: обновление игры и её отображение. Вот пример кода игрового цикла в наиболее простой форме:


bool game_is_running = true;

    while( game_is_running ) {
        update_game();
        display_game();
    }

   Проблема этого игрового цикла в том,  что не учитывается время, игра просто идет. На медленном оборудовании игра идет медленнее, на быстром – быстрее.  В старые времена, когда скорость аппаратных средств была известна, это не было проблемой, но сегодня очень много аппаратных платформ, где мы должны вводить какую-то обработку времени. Есть много способов сделать это, и я буду говорить об этом в дальнейшем. Но сначала, позвольте объяснить 2 термина, которые используются в статье.

FPS (Frames Per Second) – количество кадров в секунду. В дальнейшей реализации – это количество display_game() за секунду.
Game Speed (Скорость игры)
Скорость игры это количество раз, когда  состояние игры обновляется в секунду, или, другими словами, количество раз update_game () вызывается в секунду.

FPS зависит от постоянно Game Speed

Реализация
Простым решением вопроса будет разрешить игре работать на постоянных 25 кадров в секунду. Код будет выглядеть так:

const int FRAMES_PER_SECOND = 25;
    const int SKIP_TICKS = 1000 / FRAMES_PER_SECOND;

    DWORD next_game_tick = GetTickCount();
// GetTickCount() возвращает текущее значение миллисекунд
    // которое прошло с момента запуска системы

    int sleep_time = 0;

    bool game_is_running = true;

    while( game_is_running ) {
        update_game();
        display_game();

        next_game_tick += SKIP_TICKS;
        sleep_time = next_game_tick - GetTickCount();
        if( sleep_time >= 0 ) {
            Sleep( sleep_time );
        }
        else {
            // Ой, мы остаём!
        }
    }

   Это решение имеет одно огромное преимущество: оно простое! Так как вы знаете, что update_game () вызывается 25 раз в секунду, то записанный игровой код идет строго по плану. Например, реализация функции повтора в игровом цикле очень проста. Если вы не используете случайных значений в игре, вы просто вводите входные изменения пользователя и воспроизводите их позже. На поверочном оборудовании вы можете подобрать  FRAMES_PER_SECOND так, что оно будет идеально. Но что будет происходить на медленных и быстрых устройствах? Давайте выясним.

Медленные устройства
Если устройство может обработать заданный FPS, не проблема. Но проблемы начинаются, когда устройство не успевает обработать кадры. Игра будет идти медленнее. В худшем случае игра будет иметь участки, на которых будет идти очень медленно, на некоторых участках – нормально. В итоге - в вашу игру просто невозможно играть.

Быстрые устройства
Игра не будет иметь проблем на быстрых устройствах, но вы тратите так много драгоценных тактов. Запуск игры на 25 или 30 кадров в секунду, когда она  могла бы легко сделать 300 FPS ... позор на вас! Вы потеряете много визуальной привлекательности с этим, особенно с быстро движущихся объектов. С другой стороны, с мобильными устройствами, это может рассматриваться как выгода. Не давая игре постоянно работать на пределе, вы можете сэкономить время работы от аккумулятора.

Вывод
    Выбор FPS зависящее от постоянной скорости игры является решением, которое может быть быстро реализованы и сохраняет код игры простой. Но есть некоторые проблемы: определение высокого FPS будет создавать проблемы на медленной машине, и определение низкого FPS будет тратить на визуальную привлекательность на быстром оборудовании.
Скорость игры зависит от переменной FPS
Реализация
Другая реализация игровой цикла, чтобы он работал как можно быстрее, чтобы  FPS определял скорость игры. Игра обновляется с разницей во времени предыдущего кадра.

DWORD prev_frame_tick;
    DWORD curr_frame_tick = GetTickCount();

    bool game_is_running = true;
    while( game_is_running ) {
        prev_frame_tick = curr_frame_tick;
        curr_frame_tick = GetTickCount();

        update_game( curr_frame_tick - prev_frame_tick );
        display_game();
    }

Код игры становится немного сложнее, потому что мы теперь должны рассмотреть разницу во времени в функции update_game (). Но все-таки, это не так сложно. На первый взгляд это выглядит как идеальное решение нашей проблемы. Я видел много умных программистов реализующих этот вид игрового цикла. Некоторые из них, вероятно, хотели бы почитать эту статью, прежде чем они реализовали свой цикл. Я покажу вам за минуту, что этот цикл может иметь серьезные проблемы на медленном  так и на быстром (да, быстром!) оборудовании.

Медленное оборудование
    Медленное оборудование может иногда вызвать определенные задержки в некоторых точках, где игра становится "тяжелой". Это может определенно происходить в 3D-играх, где за определенное время слишком много полигонов нужно показать на картинке. Падение частоты кадров влияет на время отклика ввода, и, следовательно, время реакции игрока. Обновление игры также будет чувствовать задержку, и состояние игры будет обновляться за большие временные интервалы. В результате время реакции игрока, как и обработка логики, будет замедляться, в итоге сделать простой маневр может и удастся, а может и нет. Например, помеха, которую можно было бы избежать при нормальном FPS, может оказаться непреодолимой с низкой частотой кадров. Более серьезная проблема с медленным аппаратным средством в том, что при использовании физики, ваша симуляция может даже взорваться!

Быстрое оборудование
     Вам, наверное, интересно, как вышеприведенный цикл может пойти не так на быстром аппаратном обеспечении. К сожалению, это так. Чтобы продемонстрировать вам, позвольте мне объяснить кое-что о математике на компьютере. Пространство памяти с плавающей запятой или двойное значение ограничено, так что некоторые значения не могут быть представлены. Например, 0,1 не могут быть представлены двоичном коде, и, следовательно, округляется при хранении в двоичном. Позвольте мне показать вам, с помощью Python:
>>> 0.1
0.10000000000000001
   Это само по себе не является существенным, но последствия. Скажем, у вас есть гоночный автомобиль, который имеет скорость 0,001 единиц за миллисекунду. Через 10 секунд ваш автомобиль проехал на расстояние 10,0. Если разделить этот результат, как игра будет делать, у вас есть следующая функция, использующая  кадры за секунду как ввод:
>>> def get_distance( fps ):
...     skip_ticks = 1000 / fps
...     total_ticks = 0
...     distance = 0.0
...     speed_per_tick = 0.001
...     while total_ticks < 10000:
...             distance += speed_per_tick * skip_ticks
...             total_ticks += skip_ticks
...     return distance
Теперь мы можем вычислить расстояние, за 40 кадров в секунду:
>>> Get_distance (40)
+10,000000000000075
Минутку ... это не 10,0 ??? Что случилось? Ну, потому что мы делим расчет в 400 прибавлений, ошибку округления получили большой. Интересно, что будет происходить на 100 кадров в секунду ...
>>> get_distance( 100 )
9.9999999999998312
Что ??? Ошибка даже больше !! Ну, потому что у нас больше прибавлений в 100 кадров в секунду, ошибка округления имеет больше шансов стать большой. Так игра будет отличаться при работе на 40 или 100 кадров в секунду:
>>> get_distance( 40 ) - get_distance( 100 )
2.4336088699783431e-13
Вы думаете, что эта разница слишком мала, чтобы увидеть в самой игре. Но реальная проблема начнется, когда вы используете это неверное значение, чтобы сделать еще несколько расчетов. Таким образом, небольшая ошибка может стать большой, и испортить вашу игру на высоких частотах кадров. Шансы что это произойдет? Достаточно большие, чтобы рассмотреть их! Я видел игру, где использовали этот вид игрового цикла, и действительно были проблемы на высокой частоте кадров. После программист обнаружил, что проблема скрывается в ядре игры, и только много кода перезаписи могло это исправить.

Вывод
Этот вид игрового цикла может показаться очень хорошим на первый взгляд, но не обманывайте себя. Медленные и быстрые аппаратные средства могут вызвать серьезные проблемы для вашей игры. И, кроме того, реализация функции обновления игры сложнее, чем при использовании фиксированной частоты кадров, так зачем его использовать?



Постоянная Game Speed с максимальным FPS

Реализация
    Наше первое решение, FPS зависит от Constant Game Speed, имеет проблемы при работе на медленном оборудовании. И скорость игры и кадров снизится в этом случае. Возможным решением этого может быть постоянно обновлять игру на этой скорости, но сократить рендеринг кадров. Это может быть сделано с помощью следующей игрового цикла:
const int TICKS_PER_SECOND = 50;
    const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
    const int MAX_FRAMESKIP = 10;

    DWORD next_game_tick = GetTickCount();
    int loops;

    bool game_is_running = true;
    while( game_is_running ) {

        loops = 0;
        while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) {
            update_game();

            next_game_tick += SKIP_TICKS;
            loops++;
        }

        display_game();
    }
    Игра будет обновляться все время 50 раз в секунду, и предоставление (рендеринг) делается так быстро, как это возможно. Заметим, что при рендеринге более 50 раз в секунду, некоторые последующие кадры будут те же самые, таким образом,  фактические отображаемые кадры будут максимум 50 кадров в секунду. При работе на медленном оборудовании, частота кадров может упасть до цикла обновление игры и будет достигать MAX_FRAMESKIP. На практике это означает, что, когда наша оказать FPS падает ниже 5 (= FRAMES_PER_SECOND / MAX_FRAMESKIP), текущая игра будет замедляться.

Медленное оборудование
При медленном оборудовании количество кадров в секунду будет падать, но сама игра, мы надеемся, будет работать на нормальной скорости. Если оборудование не сможет справиться, сама игра будет работать медленнее и частота кадров не будет плавной вообще.

Быстрое оборудование
Игра не будет иметь никаких проблем на быстрых аппаратных средствах, но, как и первое решение, вы потратите так много драгоценных тактов, которые могут быть использованы для более высокой частотой кадров. Нахождение баланса между высокой скоростью обновления, и возможность работать на медленном оборудовании имеет решающее значение.

Вывод
Использование постоянной скорости игры с максимальным FPS является решением, которое легко осуществлять и сохраняет код игры простой. Но есть еще некоторые проблемы: Определение высокого FPS еще может создать проблемы на медленном оборудовании (но не столь сильное, как в первом решении), и определение низкого FPS будет тратить на визуальную привлекательность быстрого оборудования.


Constant Game Speed зависит от переменной  FPS

Реализация
Можно ли улучшить решение, о котором говорилось выше,  и  работать быстрее на медленном оборудовании и визуально более красиво на быстрой оборудовании? Ну, к счастью для нас, это возможно. Состояние игры не нужно обновлять 60 раз в секунду. Входные от игрока, логика и обновление состояния игры достаточно 25 кадров в секунду. Так что будем вызвать update_game () 25 раз в секунду, не больше и не меньше. Рендеринга, с другой стороны, должна быть такой же скоростью, как аппаратное обеспечение может обработать. Но низкая скорость кадров не должна мешать обновлению игры. Способ достижения этой цели является использование следующей игровой цикл:

const int TICKS_PER_SECOND = 25;
    const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
    const int MAX_FRAMESKIP = 5;

    DWORD next_game_tick = GetTickCount();
    int loops;
    float interpolation;

    bool game_is_running = true;
    while( game_is_running ) {

        loops = 0;
        while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) {
            update_game();

            next_game_tick += SKIP_TICKS;
            loops++;
        }

        interpolation = float( GetTickCount() + SKIP_TICKS - next_game_tick )
                        / float( SKIP_TICKS );
        display_game( interpolation );
    }
В таком игровом цикле, осуществление update_game () остается легким. Но, к сожалению, функция display_game () становится все более сложной. Вам придется реализовать функцию предсказания, которая принимает в качестве аргумента интерполяцию. Но не волнуйтесь, это не трудно, просто немного больше работы. Я объясню ниже, как делать интерполяцию и прогнозирование работы, но сначала позвольте мне показать вам, почему это необходимо.

Необходимость интерполяции

    GameState обновляется 25 раз в секунду, так что если вы не используете интерполяции в рендеринге, то кадры также будут отображаться на этой скорости. Заметим, что 25 кадров в секунду не так медленно, как некоторые люди думают, фильмы, например, работают на 24 кадров в секунду. Так 25 кадров в секунду должно быть достаточно для визуального восприятия, но и для быстро движущихся объектов, мы все еще можем видеть улучшение, когда сделать больше FPS. Так что мы можем сделать, это быстрые движения более плавное между кадрами. И это случай, когда интерполяция и предсказание функция может обеспечить решение.


Интерполяция и прогнозирование
    Как я уже сказал, код игры работает на его собственной частоте кадров в секунду, поэтому, когда вы рисуете ваши кадры, вполне возможно, что это между 2 gameticks. Допустим, вы только что обновили GameState на время 10-го, и теперь вы собираетесь сделать сцену. Это будет сделано между 10 и 11 обновлениями игры. Так что вполне возможно, что сделать это примерно 10,3. Значение 'интерполяции " имеет 0,3.
Возьмем такой пример: у меня есть автомобиль, который движется каждый игровой цикл, как:
position = position + speed;
Если в 10-е gametick положение 500, и скорость 100, затем в 11-м gametick позиция будет 600. Так, где вы поместите свой автомобиль, когда вы создадите его? Вы могли бы просто взять позицию последнего gametick (в данном случае 500). Но лучше, чтобы предсказать, где автомобиль будет в точном 10.3, и это происходит так:
    view_position = position + (speed * interpolation)
Автомобиль будет оказываться в положении 530. Таким образом, в основном интерполяция переменных содержит значение, которое находится между предыдущей gametick и следующей (предыдущей = 0,0, следующая = 1,0). То, что вы должны сделать, так это создать функцию "предсказания", где автомобиль / камеры / ... будет сделан на время рендеринга. Вы можете основывать эту функцию предсказания на скорости объекта, рулевого управления или скорости вращения. Это не должны быть сложными, потому что мы только использовать его для сглаживания положения объекта между кадрами. Это действительно возможно, что объект вынесен в другой объект прямо перед столкновением и это случается. Но, как мы видели раньше, игра обновляется 25 кадров в секунду, и поэтому, когда это произойдет, ошибка отображается только для доли секунды, едва заметным для человеческого глаза.

Медленное Оборудование
    В большинстве случаев, update_game () будет принимать намного меньше времени, чем display_game (). На самом деле, мы можем предположить, что даже на медленном оборудовании функция update_game () может работать в 25 раз в секунду. Таким образом, наша игра будет обрабатывать ввод-плеер и обновлять состояние игры без особых проблем, даже если игра будет отображаться только 15 кадров в секунду.

Быстрое Оборудование
    На быстром оборудовании, игра будет по-прежнему работать в постоянном темпе 25 раз в секунду, но обновление экрана будет намного быстрее, чем это. Метод интерполяции (прогноз положения) создаст визуальную привлекательность, когда игра на самом деле работает на высокой частоте кадров. Хорошо, что мы вроде обманули с FPS. Потому что вы не обновляете состояние игры каждый кадр, а только показываете, игра будет быстрее, чем FPS при использовании второго метода .
Вывод
Создание игрового состояния независимо от FPS, кажется, лучшая реализация для игрового цикла. Тем не менее, вам придется реализовать функцию предсказания в display_game (), но это не трудно сделать.

Общий вывод
    Игровой цикл это больше, чем вы думаете. Мы рассмотрели 4 возможных реализаций, и кажется, что есть один из них, которые вы обязательно должны избежать, и это тот, где переменная FPS диктует скорость игры. Постоянная частота кадров может быть хорошим и простым решением для мобильных устройств, но если вы хотите задействовать все типы устройств, лучше использовать игровой цикл, в котором FPS совершенно не зависит от скорости игры, используя функцию прогнозирования для высоких частоты кадров. Если вы не хотите возиться с функцией прогнозирования, вы можете работать с максимальной частотой кадров, но найти нужную  частоту обновления игры, как для медленных так  и для быстрых устройств, что может быть сложнее.
Теперь идите и начните кодирование фантастической игры, которую задумали!


Koen Witters

Комментариев нет:

Отправить комментарий