Как отделить игровую логику от отображения?

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

Ответов (8)

Я думаю, что этот вопрос свидетельствует о некотором непонимании того, как должны разрабатываться игровые движки. Что совершенно нормально, потому что это чертовски сложные вещи, которые сложно понять;)

У вас сложилось правильное впечатление, что вам нужно то, что называется независимостью от частоты кадров. Но это относится не только к кадрам рендеринга.

Фрейм в однопоточных игровых движках обычно называют тиком. Каждый тик вы обрабатываете ввод, обрабатываете игровую логику и визуализируете кадр на основе результатов обработки.

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

Это становится проблемой в следующем случае:

Проверить ввод: - Ключ вводится: 'W', что означает, что мы перемещаем персонажа игрока вперед на 10 единиц:

playerPosition + = 10;

Теперь, поскольку вы делаете это каждый кадр, если вы работаете со скоростью 30 кадров в секунду, вы будете перемещать 300 единиц в секунду.

Но если вы вместо этого работаете со скоростью 10 кадров в секунду, вы будете перемещать только 100 единиц в секунду. Таким образом, ваша игровая логика не зависит от частоты кадров.

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

Во-первых, вам нужен таймер, который будет считать время, необходимое для рендеринга каждого кадра. Это число в секундах (т.е. 0,001 секунды для завершения тика) затем умножается на то, что вы хотите, чтобы не зависело от частоты кадров. Итак, в этом случае:

Удерживая "W"

playerPosition + = 10 * frameTimeDelta;

(Дельта - причудливое слово, означающее «что-то изменить»)

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

Однако это будет снижаться, когда дело доходит до свойств, скорость изменения которых также изменяется со временем, например, ускоряющийся автомобиль. Эту проблему можно решить с помощью более продвинутого интегратора, такого как «Верле».

Многопоточный подход

Если вас все еще интересует ответ на свой вопрос (поскольку я не ответил на него, но представил альтернативу), вот он. Разделение игровой логики и рендеринга на разные потоки. Однако у него есть недостатки. Достаточно, чтобы подавляющее большинство игровых движков оставалось однопоточным.

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

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

Подход с фиксированным временным шагом

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

Ссылка здесь для полноты, но другой комментатор также ссылается на нее: Fix Your Time Step

По моему опыту (немного), ответы Джесси и Адама должны направить вас на правильный путь.

Если вам нужна дополнительная информация и понимание того, как это работает, я обнаружил, что образцы приложений для TrueVision 3D были очень полезны.

Вы можете сделать так, чтобы игровой цикл выглядел так:

int lastTime = GetCurrentTime();
while(1) {
    // how long is it since we last updated?
    int currentTime = GetCurrentTime();
    int dt = currentTime - lastTime;
    lastTime = currentTime;

    // now do the game logic
    Update(dt);

    // and you can render
    Draw();
}

Затем вам просто нужно написать свою Update() функцию, чтобы учесть разницу во времени; например, если у вас есть объект, движущийся с некоторой скоростью v, обновляйте его положение в v * dt каждом кадре.

У Коена Виттерса есть очень подробная статья о различных настройках игрового цикла.

Он охватывает:

  • FPS зависит от постоянной скорости игры
  • Скорость игры зависит от переменного FPS
  • Постоянная скорость игры с максимальным FPS
  • Постоянная скорость игры независимо от переменного FPS

(Это заголовки, взятые из статьи в порядке желательности.)

Об этом в свое время была отличная статья о флипкоде. Я бы хотел его откопать и представить вам.

http://www.flipcode.com/archives/Main_Loop_with_Fixed_Time_Steps.shtml

Это хорошо продуманный цикл для запуска игры:

  1. Однопоточный
  2. В фиксированные игровые часы
  3. С графикой как можно быстрее с использованием интерполированных часов

Ну, по крайней мере, я так думаю. :-) Жаль, что обсуждение, которое последовало после этой публикации, найти труднее. Возможно, здесь поможет обратная машина.

time0 = getTickCount();
do
{
  time1 = getTickCount();
  frameTime = 0;
  int numLoops = 0;

  while ((time1 - time0)  TICK_TIME && numLoops < MAX_LOOPS)
  {
    GameTickRun();
    time0 += TICK_TIME;
    frameTime += TICK_TIME;
    numLoops++;
// Could this be a good idea? We're not doing it, anyway.
//    time1 = getTickCount();
  }
  IndependentTickRun(frameTime);

  // If playing solo and game logic takes way too long, discard pending
time.
  if (!bNetworkGame && (time1 - time0)  TICK_TIME)
    time0 = time1 - TICK_TIME;

  if (canRender)
  {
    // Account for numLoops overflow causing percent  1.
    float percentWithinTick = Min(1.f, float(time1 - time0)/TICK_TIME);
    GameDrawWithInterpolation(percentWithinTick);
  }
}
while (!bGameDone);

У Enginuity есть немного другой, но интересный подход: пул задач.

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

Но вы должны правильно синхронизировать потоки;) Реализация займет много времени, поэтому, если ваша игра не слишком велика, однопоточное решение подойдет.

Кроме того, выделение графического интерфейса в отдельный поток кажется отличным подходом. Вы когда-нибудь видели всплывающее сообщение «Миссия завершена» во время передвижения юнитов в играх RTS? Это то, о чем я говорю :)

Это не относится к абстракциям более высоких программ, то есть к конечным автоматам и т. Д.

Можно управлять движением и ускорением, регулируя их в соответствии с интервалом времени кадра. Но как насчет таких вещей, как включение звука через 2,55 секунды после того или иного, или изменение уровня игры на 18,25 секунды позже и т. Д.

Это может быть связано с накопителем истекшего времени кадра (счетчиком), НО эти тайминги могут испортиться, если ваша частота кадров упадет ниже разрешения вашего сценария состояния, то есть если ваша более высокая логика требует детализации 0,05 секунды, а вы упадете ниже 20 кадров в секунду.

Детерминизм может быть сохранен, если игровая логика запускается в отдельном «потоке» (на уровне программного обеспечения, что я бы предпочел для этого, или на уровне ОС) с фиксированным временным интервалом, независимым от fps.

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