ЧРЕЗВЫЧАЙНО запутался из-за игрового цикла «Максимальная скорость в секунду»

12

Я недавно прочитал эту статью об игровых циклах: http://www.koonsolo.com/news/dewitters-gameloop/

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

Я понимаю принцип: обновляйте игру с постоянной скоростью, с тем, что осталось, визуализируйте игру столько раз, сколько возможно.

Я так понимаю, вы не можете использовать:

  • Получите вход для 25 тиков
  • Рендеринг игры за 975 тиков

Подход, поскольку вы будете получать информацию для первой части второй, и это будет странно? Или это то, что происходит в статье?


По существу:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

Как это вообще действует?

Давайте предположим его ценности.

MAX_FRAMESKIP = 5

Давайте предположим, что next_game_tick был назначен через несколько секунд после инициализации, прежде чем основной цикл игры скажет ... 500.

Наконец, поскольку я использую SDL и OpenGL для своей игры, а OpenGL используется только для рендеринга, давайте предположим, что он GetTickCount()возвращает время с момента вызова SDL_Init, что и происходит.

SDL_GetTicks -- Get the number of milliseconds since the SDL library initialization.

Источник: http://www.libsdl.org/docs/html/sdlgetticks.html

Автор также предполагает это:

DWORD next_game_tick = GetTickCount();
// GetTickCount() returns the current number of milliseconds
// that have elapsed since the system was started

Если мы расширим whileутверждение, мы получим:

while( ( 750 > 500 ) && ( 0 < 5 ) )

750, потому что время прошло с тех пор, как next_game_tickбыло назначено. loopsэто ноль, как вы можете видеть в статье.

Итак, мы вошли в цикл while, давайте сделаем некоторую логику и примем ввод.

Yadayadayada.

В конце цикла while, который, напомню, находится внутри нашего основного цикла игры:

next_game_tick += SKIP_TICKS;
loops++;

Давайте обновим, как выглядит следующая итерация кода while

while( ( 1000 > 540 ) && ( 1 < 5 ) )

1000, потому что прошло время получения входных данных и выполнения каких-либо действий, прежде чем мы достигли следующего изменения цикла, где вызывается GetTickCount ().

540 потому что в коде 1000/25 = 40, следовательно, 500 + 40 = 540

1, потому что наш цикл повторялся один раз

5 , ты знаешь почему.


Итак, поскольку этот цикл «ЯДЕР» явно зависит, MAX_FRAMESKIPа не предназначен, TICKS_PER_SECOND = 25;как игра должна работать правильно?

Меня не удивило, что когда я реализовал это в своем коде, я мог бы правильно добавить это, просто переименовав свои функции для обработки пользовательского ввода и нарисовав игру так, как это сделал автор статьи в своем примере кода, игра ничего не сделала ,

Я поместил fprintf( stderr, "Test\n" );внутри цикл while, который не печатается, пока игра не закончится.

Как гарантированно работает этот игровой цикл 25 раз в секунду, и рендерится так быстро, как может?

Для меня, если я не пропустил что-то ОГРОМНОЕ, это выглядит ... ничего.

И разве эта структура, этого цикла while, якобы запускается 25 раз в секунду, а затем обновляет игру именно так, как я упоминал ранее в начале статьи?

Если это так, почему мы не можем сделать что-то простое, как:

while( loops < 25 )
{
    getInput();
    performLogic();

    loops++;
}

drawGame();

И рассчитывать на интерполяцию другим способом.

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

tsujp
источник
1
Рэнты лучше направлены на автора статьи. Какая часть является вашим объективным вопросом ?
Анко
3
Является ли этот игровой цикл даже действительным, кто-то объяснит. Из моих тестов он не имеет правильной структуры для запуска 25 раз в секунду. Объясните, почему это так. Также это не напыщенная речь, это серия вопросов. Должен ли я использовать смайлики, я выгляжу злым?
tsujp
2
Поскольку ваш вопрос сводится к «Что я не понимаю об этом игровом цикле», и у вас много смелых слов, это звучит как минимум раздраженно.
Кирбинатор
@Kirbinator Я могу это оценить, но я пытался обосновать все, что я нахожу необычным в этой статье, так что это не мелкий и пустой вопрос. Я не думаю, что это так важно, что в любом случае, я хотел бы думать, что у меня есть веские аргументы - в конце концов, это статья, которая пытается преподавать, но не делает такую ​​хорошую работу.
tsujp
7
Не поймите меня неправильно, это хороший вопрос, он может быть на 80% короче.
Кирбинатор

Ответы:

8

Я думаю, что автор допустил небольшую ошибку:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

должно быть

while( GetTickCount() < next_game_tick && loops < MAX_FRAMESKIP)

То есть: пока еще не пришло время рисовать наш следующий кадр, и пока мы не пропустили столько кадров, сколько MAX_FRAMESKIP, нам следует подождать.

Я также не понимаю, почему он обновляется next_game_tickв цикле, и я предполагаю, что это еще одна ошибка. Поскольку в начале кадра вы можете определить, когда должен быть следующий кадр (при использовании фиксированной частоты кадров). Значение next game tickне зависит от того, сколько времени у нас осталось после обновления и рендеринга.

Автор также делает еще одну распространенную ошибку

с тем, что осталось, сделать игру как можно больше раз.

Это означает рендеринг одного и того же кадра несколько раз. Автор даже знает об этом:

Игра будет обновляться с постоянной скоростью 50 раз в секунду, а рендеринг будет выполняться максимально быстро. Обратите внимание, что когда рендеринг выполняется более 50 раз в секунду, некоторые последующие кадры будут одинаковыми, поэтому фактические визуальные кадры будут отображаться с максимальной скоростью 50 кадров в секунду.

Это только приводит к тому, что графический процессор выполняет ненужную работу, и если рендеринг занимает больше времени, чем ожидалось, это может привести к тому, что вы начнете работать над следующим кадром позже, чем предполагалось, поэтому лучше уступить ОС и ждать.

Рой Т.
источник
+ Vote. Мммм. Это действительно оставляет меня озадаченным, что делать тогда. Я благодарю вас за понимание. Я, вероятно, поиграю, проблема на самом деле в знаниях об ограничении FPS или о динамической установке FPS и о том, как это сделать. На мой взгляд, требуется фиксированная обработка ввода, чтобы игра шла одинаково для всех. Это всего лишь простая 2D-платформенная MMO (в очень долгой перспективе)
tsujp
Хотя цикл кажется правильным, и для этого есть приращение next_game_tick. Он поддерживает симуляцию на постоянной скорости на медленном и быстром оборудовании. Выполнение кода рендеринга «как можно быстрее» (или быстрее, чем физика в любом случае) имеет смысл только тогда, когда в коде рендеринга есть интерполяция, чтобы сделать его более плавным для быстрых объектов и т. Д. (Конец статьи), но это просто пустая трата энергии, если это рендеринг больше, чем то, что может показать любое устройство вывода (экран).
Дотти
Таким образом, его код является допустимой реализацией, теперь это так. И мы просто должны жить с этими отходами? Или есть методы, которые я могу найти для этого.
tsujp
Уступить ОС и ждать - это только хорошая идея, если у вас есть четкое представление о том, когда ОС вернет вам контроль, а это может произойти не так быстро, как хотелось бы.
Kylotan
Я не могу проголосовать за этот ответ. Он полностью пропускает интерполяцию, что делает всю вторую половину ответа недействительной.
AlbeyAmakiir
4

Может быть, лучше, если я немного упросту это:

while( game_is_running ) {

    current = GetTickCount();
    while(current > next_game_tick) {
        update_game();

        next_game_tick += SKIP_TICKS;
    }
    display_game();
}

whileЦикл внутри mainloop используется для запуска шагов моделирования от того, где он был, до того места, где он должен быть сейчас. update_game()Функция всегда должна предполагать, что SKIP_TICKSс момента последнего вызова прошло только время. Это позволит поддерживать физику игры на постоянной скорости на медленном и быстром оборудовании.

Увеличение next_game_tickна количество SKIP_TICKSперемещений приближает его к текущему времени. Когда это становится больше, чем текущее время, оно ломается ( current > next_game_tick) и mainloop продолжает отображать текущий кадр.

После рендеринга следующий вызов GetTickCount()возвратит новое текущее время. Если это время больше, чем next_game_tickэто означает, что мы уже отстаем от 1-N шагов в симуляции, и мы должны наверстать упущенное, выполняя каждый шаг в симуляции с той же постоянной скоростью. В этом случае, если он ниже, он просто снова визуализирует тот же кадр (если нет интерполяции).

Оригинальный код ограничил количество циклов, если мы остались слишком далеко ( MAX_FRAMESKIP). Это только заставляет его фактически показывать что-то и не похоже на то, что оно заблокировано, если, например, возобновить приостановку или приостановку игры в отладчике на долгое время (при условии, что GetTickCount()он не останавливается в течение этого времени), пока это не застало время.

Чтобы избавиться от бесполезного рендеринга того же кадра, если вы не используете интерполяцию внутри display_game(), вы можете обернуть его внутри, если выражение вроде

while (game_is_running) {
    current = GetTickCount();
    if (current > next_game_tick) {
        while(current > next_game_tick) {
            update_game();

            next_game_tick += SKIP_TICKS;
        }
    display_game();
    }
    else {
    // could even sleep here
    }
}

Это также хорошая статья об этом: http://gafferongames.com/game-physics/fix-your-timestep/

Также, возможно, причина, по которой ваши fprintfрезультаты, когда ваша игра заканчивается, может быть просто в том, что она не сбрасывается.

Извините за мой английский.

Дотти
источник
4

Его код выглядит полностью действительным.

Рассмотрим whileцикл последнего набора:

// JS / pseudocode
var current_time = function () { return Date.now(); }, // in ms
    framerate = 1000/30, // 30fps
    next_frame = current_time(),

    max_updates_per_draw = 5,

    iterations;

while (game_running) {

    iterations = 0;

    while (current_time() > next_frame && iterations < max_updates_per_draw) {
        update_game(); // input, physics, audio, etc

        next_frame += framerate;
        iterations += 1;
    }

    draw();
}

У меня есть система, которая говорит: «пока игра запущена, проверьте текущее время - если оно больше, чем наша подсчет частоты кадров, и мы пропустили рисование менее 5 кадров, затем пропустите рисование и просто обновите ввод и физика: еще нарисовать сцену и начать следующую итерацию обновления "

Когда происходит каждое обновление, вы увеличиваете время «next_frame» на свою идеальную частоту кадров. Затем вы снова проверяете свое время. Если ваше текущее время сейчас меньше, чем когда должен быть обновлен next_frame, тогда вы пропустите обновление и нарисуете то, что у вас есть.

Если ваш current_time больше (представьте, что последний процесс рисования занял очень много времени, потому что где-то был какой-то сбой, или сборка мусора в управляемом языке, или реализация управляемой памяти в C ++ или что-то еще), тогда рисовать получает пропущено , и next_frameобновляются с другой дополнительной рамкой стоит времени, пока либо обновления не догонять , где мы должны быть на часах, или мы пропустили рисунок достаточно кадров , что мы обязательно должны сделать один, так что игрок может видеть что они делают

Если ваша машина очень быстрая или ваша игра очень простая, то это current_timeможет быть не так next_frameчасто, что означает, что вы не обновляетесь в течение этих пунктов.

Вот где начинается интерполяция. Аналогично, вы можете иметь отдельный тип bool, объявленный вне циклов, чтобы объявить «грязное» пространство.

Внутри цикла обновления вы бы установили dirty = true, что означает, что вы действительно выполнили обновление.

Тогда вместо того, чтобы просто позвонить draw(), вы скажете:

if (is_dirty) {
    draw(); 
    is_dirty = false;
}

Тогда вы упускаете любую интерполяцию для плавного движения, но вы гарантируете, что обновляетесь только тогда, когда у вас действительно происходят обновления (а не интерполяции между состояниями).

Если вы смелы, есть статья под названием «Исправьте свой временной шаг!» GafferOnGames.
Он немного по-другому подходит к этой проблеме, но я считаю, что это более симпатичное решение, которое делает в основном то же самое (в зависимости от особенностей вашего языка и от того, насколько вы заботитесь о физических вычислениях в вашей игре).

Norguard
источник