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


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();

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

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


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.
Он немного по-другому подходит к этой проблеме, но я считаю, что это более симпатичное решение, которое делает в основном то же самое (в зависимости от особенностей вашего языка и от того, насколько вы заботитесь о физических вычислениях в вашей игре).

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.