Частота кадров влияет на скорость объекта

9

Я экспериментирую с созданием игрового движка с нуля на Java, и у меня есть пара вопросов. Мой основной игровой цикл выглядит так:

        int FPS = 60;
        while(isRunning){
            /* Current time, before frame update */
            long time = System.currentTimeMillis();
            update();
            draw();
            /* How long each frame should last - time it took for one frame */
            long delay = (1000 / FPS) - (System.currentTimeMillis() - time);
            if(delay > 0){
                try{
                    Thread.sleep(delay);
                }catch(Exception e){};
            }
        }

Как вы можете видеть, я установил частоту кадров 60FPS, которая используется в delayрасчете. Задержка гарантирует, что каждый кадр занимает одинаковое количество времени перед рендерингом следующего. В своей update()функции я делаю, x++что увеличивает горизонтальное значение графического объекта, который я рисую, с помощью следующего:

bbg.drawOval(x,40,20,20);

Что меня смущает, так это скорость. Когда я установил FPSзначение 150, рендеринг круга действительно быстро пересекает скорость, в то время как установка FPS30 перемещается по экрану на половине скорости. Разве частота кадров не влияет только на «плавность» рендеринга, а не на скорость рендеринга объектов? Я думаю, что я пропускаю большую часть, я хотел бы получить некоторые разъяснения.

Carpetfizz
источник
4
Вот хорошая статья об игровом цикле: исправьте свой временной шаг
Костя Регент
2
Как примечание, мы обычно стараемся помещать вещи, которые не должны выполняться, каждый цикл вне цикла. В вашем коде ваше 1000 / FPSделение может быть выполнено, а результат назначен переменной перед вашим while(isRunning)циклом. Это помогает сохранить пару инструкций процессора для выполнения чего-то более одного раза без необходимости.
Vaillancourt

Ответы:

21

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

У вас есть три возможных способа решения этой проблемы:

  1. Просто выберите одну частоту кадров и придерживайтесь ее. Это то, что делали многие игры старой школы - они работали с фиксированной частотой 50 или 60 кадров в секунду, обычно синхронизировались с частотой обновления экрана и просто разрабатывали игровую логику, чтобы делать все необходимое в течение этого фиксированного интервала времени. Если по какой-то причине этого не произошло, игра просто пропустит кадр (или, возможно, потерпит крах), эффективно замедляя как рисование, так и игровую физику до половины скорости.

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

  2. Используйте переменный временной шаг для вашей игровой физики. По сути, это означает переписать игровой цикл так, чтобы он выглядел примерно так:

    long lastTime = System.currentTimeMillis();
    while (isRunning) {
        long time = System.currentTimeMillis();
        float timestep = 0.001 * (time - lastTime);  // in seconds
        if (timestep <= 0 || timestep > 1.0) {
            timestep = 0.001;  // avoid absurd time steps
        }
        update(timestep);
        draw();
        // ... sleep until next frame ...
        lastTime = time;
    }
    

    и, внутри update(), корректируя физические формулы для учета переменного временного шага, например, так:

    speed += timestep * acceleration;
    position += timestep * (speed - 0.5 * timestep * acceleration);
    

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

  3. Разъедините обновление и нарисуйте шаги. Идея заключается в том, что вы обновляете игровое состояние с использованием фиксированного временного шага, но запускаете различное количество обновлений между каждым кадром. То есть ваш игровой цикл может выглядеть примерно так:

    long lastTime = System.currentTimeMillis();
    while (isRunning) {
        long time = System.currentTimeMillis();
        if (time - lastTime > 1000) {
            lastTime = time;  // we're too far behind, catch up
        }
        int updatesNeeded = (time - lastTime) / updateInterval;
        for (int i = 0; i < updatesNeeded; i++) {
            update();
            lastTime += updateInterval;
        }
        draw();
        // ... sleep until next frame ...
    }
    

    Чтобы сделать воспринимаемое движение более плавным, вы, возможно, захотите, чтобы ваш draw()метод плавно интерполировал такие вещи, как положение объектов между предыдущим и следующим игровыми состояниями. Это означает, что вам нужно передать корректное интерполяционное смещение в draw()метод, например, так:

        int remainder = (time - lastTime) % updateInterval;
        draw( (float)remainder / updateInterval );  // scale to 0.0 - 1.0
    

    Вам также необходимо, чтобы ваш update()метод фактически вычислял игровое состояние на один шаг вперед (или, возможно, несколько, если вы хотите выполнить сплайн-интерполяцию более высокого порядка), и сохранял предыдущие позиции объектов перед их обновлением, чтобы draw()метод мог интерполировать между ними. (Также возможно просто экстраполировать прогнозируемые позиции на основе скоростей и ускорений объектов, но это может выглядеть отрывисто, особенно если объекты движутся сложными способами, что часто приводит к сбоям прогнозов.)

    Одним из преимуществ интерполяции является то, что для некоторых типов игр она позволяет значительно снизить частоту обновления игровой логики, сохраняя при этом иллюзию плавного движения. Например, вы можете обновлять игровое состояние только, скажем, 5 раз в секунду, в то же время рисуя от 30 до 60 интерполированных кадров в секунду. При этом вы можете также рассмотреть возможность чередования игровой логики с чертежом (т. Е. Иметь параметр для вашего update()метода, который указывает ему запускать только x % от полного обновления перед возвратом), и / или запускать физику игры / логика и код рендеринга в отдельных потоках (остерегайтесь глюков синхронизации!).

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

(Редактировать: добавлен код, чтобы избежать абсурдных интервалов обновления / подсчета, если, скажем, компьютер временно приостановлен или иным образом заморожен более чем на секунду, пока запущен игровой цикл. Спасибо Mooing Duck за напоминание о необходимости этого .)

Илмари Каронен
источник
1
Большое спасибо, что нашли время ответить на мой вопрос, я действительно ценю это. Мне действительно нравится подход № 3, он имеет смысл для меня. Два вопроса, чем определяется updateInterval и почему вы делите его?
Carpetfizz
1
@Carpetfizz: updateIntervalэто просто количество миллисекунд, которое вы хотите между обновлениями состояния игры. Например, 10 обновлений в секунду updateInterval = (1000 / 10) = 100.
Илмари Каронен
1
currentTimeMillisэто не монотонные часы. Используйте nanoTimeвместо этого, если только вы не хотите, чтобы синхронизация времени в сети мешала скорости вашей игры.
user253751
@MooingDuck: хорошо заметили. Я исправил это сейчас, я думаю. Спасибо!
Илмари Каронен
@IlmariKaronen: На самом деле, глядя на код, может быть проще просто while(lastTime+=updateInterval <= time). Это просто мысль, а не исправление.
Mooing Duck
7

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

Чтобы решить эту проблему, вы должны обратиться к Delta Timing .

Целью Delta Timing является устранение влияния задержки на компьютеры, которые пытаются обрабатывать сложную графику или большой объем кода, путем добавления скорости объектов, чтобы они в конечном итоге перемещались с одинаковой скоростью, независимо от задержки.

Сделать это:

Это делается путем вызова таймера каждый кадр в секунду, который содержит время между текущим и последним вызовом в миллисекундах.

Затем вам нужно будет умножить дельта-время на значение, которое вы хотите изменить по времени. Например:

distanceTravelledSinceLastFrame = Speed * DeltaTime
статический
источник
3
Кроме того, установите максимальные и минимальные значения дельта-времени. Если компьютер переходит в спящий режим, а затем возобновляет работу, вы не хотите, чтобы что-то запускалось за пределами экрана. Если чудо появляется и time()возвращает одно и то же дважды, вам не нужны ошибки div / 0 и бесполезная обработка.
Утка
@MooingDuck: Это очень хороший момент. Я отредактировал свой собственный ответ, чтобы отразить его. (Как правило, вы не должны делить что-либо по временному шагу в типичном обновлении состояния игры, поэтому нулевой временной шаг должен быть безопасным, но его разрешение действительно добавляет дополнительный источник потенциальных ошибок для небольшого или нулевого выигрыша, и поэтому должно быть избегать.)
Илмари Каронен
5

Это потому, что вы ограничиваете частоту кадров, но вы делаете только одно обновление на кадр. Итак, давайте предположим, что игра работает на скорости 60 кадров в секунду, вы получаете 60 обновлений логики в секунду. Если частота кадров упадет до 15 кадров в секунду, у вас будет только 15 обновлений логики в секунду.

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

Добавьте альтернативное (лучше для визуального) обновление вашей логики на основе прошедшего времени.

Марио
источник
1
то есть обновление (elapsedSeconds);
Джон
2
А внутри позиция + = скорость * истекшие секунды;
Джон