Как заставить игру работать с частотой 60fps

Автор оригинала: Tyler Glaiel
  • Перевод
Представьте себе задачу: у вас есть игра, и вам нужно, чтобы она работала с частотой 60 fps на 60-герцовом мониторе. Ваш компьютер достаточно быстр для того, чтобы рендеринг и обновление занимали несущественное количество времени, поэтому вы включаете vsync и пишете такой игровой цикл:

while(running) {
    update();
    render();
    display();
}

Очень просто! Теперь игра работает с 60fps и всё идёт как по маслу. Готово. Спасибо, что прочитали этот пост.


Ну ладно, очевидно, что всё не так хорошо. Что если у кого-то слабый компьютер, который не может рендерить игру с достаточной для обеспечения 60fps скоростью? Что если кто-то купил один из тех крутых новых 144-герцовых мониторов? Что если он отключил в настройках драйвера vsync?

Вы можете подумать: мне нужно где-то измерять время и обеспечить обновление с правильной частотой. Это сделать довольно просто — достаточно накапливать время в каждом цикле и выполнять обновление каждый раз, когда он превосходит порог в 1/60 секунды.

while(running) {
    deltaTime = CurrentTime()-OldTime;
    oldTime = CurrentTime();
    accumulator += deltaTime;
    while(accumulator > 1.0/60.0){
        update();
        accumulator -= 1.0/60.0;
    }
    render();
    display();
}

Готово, проще некуда. На самом деле есть куча игр, в которых код по сути выглядит именно так. Но это неправильно. Это подходит для регулировки таймингов, но приводит к проблемам с дёрганьем (stuttering) и прочим рассогласованиям. Очень часто встречается такая проблема: кадры отображаются не ровно 1/60 секунды; даже когда vsync включен, всегда присутствуют небольшой шум во времени их отображения (и в точности таймера ОС). Поэтому будут возникать ситуации, когда вы рендерите кадр, а игра считает, что время повторного обновления ещё не настало (потому что аккумулятор на крошечную долю запаздывает), поэтому она просто снова повторяет тот же кадр, но теперь игра запаздывает на кадр, поэтому выполняет двойное обновление. Вот и дёрганье!

Погуглив, можно найти несколько готовых решений для устранения этого дёрганья. Например, игра может использовать переменный, а не постоянный шаг времени, и просто полностью отказаться от аккумуляторов в коде таймингов. Или можно реализовать постоянный шаг времени с интерполирующим рендерером, описанный в довольно известной статье "Fix Your Timestep" Гленна Филдера. Или можно переделать код таймера так, чтобы он был немного более гибким, как описано в посте "Frame Timing Issues" Slick Entertainment (к сожалению, этого блога уже нет).



Нечёткие тайминги


Метод Slick Entertainment с «нечёткими таймингами» в моём движке было реализовать проще всего, потому что он не требовал изменений в логике игры и рендеринге. Поэтому в The End is Nigh я использовал его. Достаточно было просто вставить его в движок. По сути, он просто позволяет игре обновляться «немного раньше», чтобы избежать проблем с рассогласованием таймингов. Если в игре включен vsync, то он просто позволяет использовать в качестве основного таймера игры vsync, и обеспечивает плавную картинку.

Вот как код обновления выглядит сейчас (игра «может работать» при 62 fps, но всё равно обрабатывает каждый шаг времени так, как будто работает при 60fps. Не совсем понимаю, зачем ограничивать его так, чтобы значения аккумулятора не опускались ниже 0, но без этого код не работает). Можно интерпретировать это так: «игра обновляется с фиксированным шагом, если рендерится в интервале от 60fps до 62fps»:

while(accumulator > 1.0/62.0){
    update();
    accumulator -= 1.0/60.0;
    if(accumulator < 0) accumulator = 0;
}

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

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

Ой, но ведь мы никак не может проверить, отключен ли vsync. В ОС для этого нет вызовов, и хотя мы можем запрашивать из приложения включение или отключение vsync, на самом деле это полностью зависит от ОС и графического драйвера. Единственное, что можно сделать — отрендерить кучу кадров, попробовать измерить время выполнения этой задачи, а затем сравнить, занимают ли они примерно одинаковое время. Именно так я и сделал для The End is Nigh. Если в игре не включен vsync с частотой 60 Гц, то она откатывается к исходному таймеру кадров со «строгими 60 fps». Кроме того, я добавил в файл конфигурации параметр, принуждающий игру не использовать нечёткость (с основном для спидраннеров, которым нужно точное время) и добавил для них точный обработчик внутриигрового таймера, позволяющий использовать autosplitter (это скрипт, работающий с таймером атомного времени).

Некоторые пользователи по-прежнему жаловались на возникающие иногда дёрганья отдельных кадров, но они казались настолько редкими, что их можно было объяснить событиями ОС или другими внешними причинами. Ничего особо страшного. Верно?

Просматривая недавно свой код таймера, я заметил нечто странное. Аккумулятор смещался, каждый кадр занимал чуть больше времени, чем 1/60 секунды, поэтому периодически игра думала, что запаздывает на кадр, и выполняла двойное обновление. Оказалось, что мой монитор работает с частотой 59,94 Гц, а не 60 Гц. Это означало, что каждые 1000 кадров ему приходится выполнять двойное обновление, чтобы «догнать». Однако это очень просто исправить — достаточно изменить интервал допустимых частот кадров (не с 60 до 62, а с 59 до 61).

while(accumulator > 1.0/61.0){
    update();
    accumulator -= 1.0/59.0;
    if(accumulator < 0) accumulator = 0;
}

Описанная выше проблема с отключенным vsync и мониторами высокой частоты по-прежнему сохраняется, и к нему применимо то же решение (откат к строгому таймеру, если монитор не синхронизирован vsync на 60).

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

Симулятор монитора


Пытаясь придумать надёжное решение «проблемы 59,94-герцового монитора», я осознал, что не могу просто выполнять проверки методом проб и ошибок, надеясь найти надёжное решение. Мне нужен был удобный способ тестирования разных попыток написания качественного таймера и простой способ проверки, вызывает ли он дёрганье или сдвиг времени в разных конфигурациях мониторов.

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

Например, для простейшего таймера из начала статьи выводятся такие значения:

20211012021011202111020211102012012102012[...]
TOTAL UPDATES: 10001
TOTAL VSYNCS: 10002
TOTAL DOUBLE UPDATES: 2535
TOTAL SKIPPED RENDERS: 0
GAME TIME: 166.683
SYSTEM TIME: 166.7


Сначала код выводит для каждого эмулируемого vsync число количества «обновлений» игрового цикла после предыдущего vsync. Любые значения, отличающиеся от сплошных 1, приводят к дёрганой картинке. В конце код выводит накопившуюся статистику.

При использовании «нечёткого таймера» (с интервалом 60–62fps) на 59,94-герцовом мониторе код выводит следующее:

111111111111111111111111111111111111111111111[...]
TOTAL UPDATES: 10000
TOTAL VSYNCS: 9991
TOTAL DOUBLE UPDATES: 10
TOTAL SKIPPED RENDERS: 0
GAME TIME: 166.667
SYSTEM TIME: 166.683


Дёрганье кадра возникает очень редко, поэтому его может быть сложно заметить при таком количестве 1. Но выводимая статистика чётко показывает, что игра выполнила здесь несколько двойных обновлений, что приводит к дёрганью. В исправленной версии (с интервалом 59–61 fps) присутствует 0 пропущенных или двойных обновлений.

Также можно отключить vsync. Остальные данные статистики становятся неважными, но это чётко показывает мне величину «сдвига времени» (сдвига системного времени относительно того, где должно находиться игровое время).

GAME TIME: 166.667
SYSTEM TIME: 169.102


Именно поэтому при отключенном vsync нужно переключаться на строгий таймер, иначе эти расхождения со временем накапливаются.

Если я присвою времени рендеринга значение .02 (то есть для рендеринга нужно «больше, чем кадр»), то снова получу дёрганье. В идеале паттерн игры должен выглядеть как 202020202020, но он немного неравномерен.

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

while(accumulator >= 1.0/61.0){
    simulate_update();
    accumulator -= 1.0/60.0;
    if(accumulator < 1.0/59.0–1.0/60.0) accumulator = 0;
}

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

Я доволен своим решением не на 100% (в нём всё равно требуется хак с «распознаванием vsync» и могут возникать единичные дёрганья при рассинхронизации), но я считаю, что он почти так же хорош, как и попытка реализации игрового цикла с фиксированным шагом. Частично эта проблема возникает потому, что очень сложно определить параметры того, что считается здесь «приемлемым». Главная сложность заключается в компромиссе между сдвигом времени и двойными/пропущенными кадрами. Если запустить 60-герцовую игру на 50-герцовом PAL-мониторе… то каким будет верное решение? Хотите ли вы дикого дёрганья, или заметно более медленной работы игры? Оба варианта кажутся плохими.

Отделённый рендеринг


В предыдущих методах я описывал то, что называю «рендерингом с фиксированным шагом» (lockstep rendering). Игра обновляет своё состояние, затем выполняет рендеринг, а при рендеринге она всегда отображает наиболее свежее состояние игры. Рендеринг и обновление соединены вместе.

Но можно их разделить. Именно это делает метод, описанный в посте "Fix Your Timestep". Я не буду повторяться, вам определённо стоит прочитать этот пост. Это (насколько я понимаю) «отраслевой стандарт», используемый в AAA-играх и таких движках, как Unity и Unreal (однако в напряжённых активных 2D-играх обычно предпочитают использовать фиксированный шаг (lockstep), потому что иногда просто необходима точность, которую даёт этот метод).

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

while(running){
    computeDeltaTimeSomehow();
    accumulator += deltaTime;
    while(accumulator >= 1.0/60.0){
        previous_state = current_state;
        current_state = update();
        accumulator -= 1.0/60.0;
    }
    render_interpolated_somehow(previous_state, current_state, accumulator/(1.0/60.0));
    display();
}

Вот так, элементарно. Проблема решена.

Теперь нужно просто сделать так, чтобы игра могла рендерить интерполированные состояния… но постойте, на самом деле это совсем непросто. В посте Гленна просто допускается, что это можно сделать. Достаточно легко кэшировать предыдущее положение игрового объекта и интерполировать его перемещения, но в состояние игры входит гораздо больше всего. Нужно учитывать в нём состояния анимации, создание и уничтожение объектов, и ещё кучу всякого.

Плюс в логике игры нужно учитывать, телепортируется ли объект или его нужно плавно двигать, чтобы интерполятор не делал ложных предположений о пути, проделанным игровым объектом в его текущее положение. Настоящий хаос может возникнуть с поворотами, особенно если за один кадр поворот объекта может смениться больше, чем на 180 градусов. А как правильно обрабатывать создаваемые и уничтожаемые объекты?

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

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

В своём движке я просто даю возможность выбора. Если у вас 60-герцовый монитор и быстрый компьютер, то лучше всего использовать lockstep со включенным vsync. Если у монитора нестандартная частота обновления, или ваш слабый компьютер не может постоянно рендерить 60 кадров в секунду, то включайте интерполяцию кадров. Я хочу назвать эту опцию «unlock framerate» («разблокировать частоту кадров»), но люди могут подумать, что это просто означает «включите эту опцию, если у вас хороший компьютер». Однако эту проблему можно решить позже.

Вообще-то есть метод, позволяющий обойти эту проблему.

Обновления с переменным шагом времени


Меня многие спрашивали, почему бы просто не обновлять игру с переменным шагом времени, а программисты-теоретики часто говорят: «если игра написана ПРАВИЛЬНО, то можно просто обновлять её с произвольным шагом времени».

while(running) {
    deltaTime = CurrentTime()-OldTime;
    oldTime = CurrentTime();
    update(deltaTime);
    render();
    display();
}

Никаких странностей с таймингами. Никакого странного интерполяционного рендеринга. Всё просто, всё работает.

Вот так, элементарно. Проблема решена. И теперь навсегда! Лучшего результата добиться невозможно!

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

position += speed;

на такой:

position += speed * deltaTime;

и заменить вот такой код:

speed += acceleration;
position += speed;

на такой:

speed += acceleration * deltaTime;
position += speed * deltaTime;

и заменить вот такой код:

speed += acceleration;
speed *= friction;
position += speed;

на такой:

Vec3D p0 = position;
Vec3D v0 = velocity;
Vec3D a = acceleration*(1.0/60.0);
double f = friction;
double n = dt*60;
double fN = pow(friction, n);
position = p0 + ((f*(a*(f*fN-f*(n+1)+n)+(f-1)*v0*(fN-1)))/((f-1)*(f-1)))*(1.0/60.0);
velocity = v0*fN+a*(f*(fN-1)/(f-1));

… так, постойте-ка

Откуда это всё взялось?

Последняя часть в буквальном смысле скопирована из вспомогательного кода моего движка, выполняющего «действительно корректное, не зависящее от частоты кадров движение с ограничивающим скорость трением». В нём есть немного мусора (эти умножения и деления на 60). Но это «правильная» версия кода с переменным шагом времени для предыдущего фрагмента. Я вычислял её больше часа при помощи Wolfram Alpha.

Теперь меня могут спросить, почему бы не сделать вот так:

speed += acceleration * deltaTime;
speed *= pow(friction, deltaTime);
position += speed * deltaTime;

И хотя это как будто сработает, на самом деле так делать неправильно. Можете проверить сами. Выполните два обновления со значением deltaTime = 1, а затем выполните одно обновление с deltaTime = 2, и результаты будут отличаться. Обычно мы стремимся, чтобы игра работала согласованно, поэтому такие расхождения не приветствуются. Вероятно, это достаточно хорошее решение, если точно знать, что deltaTime всегда примерно равно одному значению, но тогда нужно написать код, обеспечивающий выполнение обновлений с какой-то постоянной частотой и… да. Верно, теперь мы пытаемся сделать всё «ПРАВИЛЬНО».

Если такой крошечный фрагмент кода разворачивается в чудовищные математические вычисления, то представьте более сложные паттерны движения, в которых участвует множество взаимодействующих объектов, и тому подобное. Теперь можно чётко увидеть, что «правильное» решение нереализуемо. Максимум, чего мы можем добиться — это «грубое приближение». Давайте пока об этом забудем, и допустим, что у нас и в самом деле есть «действительно корректная» версия функций движения. Здорово, правда?

Вообще-то нет. Вот реальный пример проблемы, которая у меня возникла с этим в Bombernauts. Игрок может подпрыгивать примерно на 1 тайл, а игра разворачивается в сетке из блоков в 1 тайл. Чтобы приземлиться на блок, ноги персонажа должны подняться над верхней поверхностью блока.


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


Очевидно, что эта проблема решаема. Но она иллюстрирует виды проблем, с которыми мы сталкиваемся при попытке правильной реализации работы игрового цикла с переменным шагом времени. Мы теряем согласованность и детерминированность, поэтому придётся избавиться от функций реплея игры по записи ввода игрока, детерминированного мультиплеера и того подобного. Для основанной на рефлексах быстрой 2D-игры согласованность чрезвычайно важна (и снова привет спидраннерам).

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



Заключение


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

Комментарии 36

    +11
    speed += acceleration * deltaTime;
    speed *= pow(friction, deltaTime);
    position += speed * deltaTime;

    И хотя это как будто сработает, на самом деле так делать неправильно. Можете проверить сами.


    Потому что физику не обманешь — расстояние это интеграл от изменения скорости по времени, а не просто произведение моментальной скорости на время. По-сути, он считал раньше интеграл дискретным суммированием и делать это можно лишь с минимальным возможным шагом времени, но попытка увеличивать этот шаг сразу же дает ошибку. В правильном варианте надо вывести функцию изменения скорости (а вывести ее с таким трением будем не просто) и затем интегрировать. В крайнем случае можно хотя бы приближенно брать (speed — prevSpeed) * deltaTime/ 2, но это тоже не очень точно.

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


      0
      Это в Maple формулка такая вышла?
        0
        Это в Mathematica. Вверху видно условие, а ниже решение, которое получилось )
        Решалась обратная задача — имея формулу движения найти сколько времени лететь до остановки.
          +2
          Тут, похоже, нужны численные методы с адаптивным шагом. Ну или предрасчёт, зависит от игровой механики.
            0
            Я думаю что тут проблема в том, что обычно численные/разностные схемы — многопроходные, а это сильно увеличит время обработки
        +2
        (посыпает голову пеплом)
        (speed — prevSpeed) * deltaTime/ 2

        + конечно же
          +1
          Да, было бы очень интересно прочитать.
            +2
            Вот так и мечтай о геймдеве…
              0
              А что там мечтать, идти надо и делать. Начать можно просто, технологий масса.
                0
                Слишком сложно?
                Геймдев — это ведь не только разработка движков.
                0

                Жги!

                  +4
                  Функцию изменения сокрости не обязательно находить, см. интегрирование Верле.
                  Глядя на вашу формулу, понимаешь, что это тот самый момент, когда проще численно интегрировать. У вашей системы решения гладкие, поэтому при выборе корректного метода, численное решение будет сравнимо по точности с аналитическим. Не прямоугольниками или трапециями, а, например методом Рунге — Кутты, или как выше сказал — Верле, через разложение в ряд Тейлора.
                    +1
                    Большое спасибо за ссылку на Верле, не знал о таком
                    –1
                    Зачем вычислять моментальную скорость (которая нам не нужна) и неправильно считать от неё позицию, когда можно просто составить формулу позиции от времени, которая совершенно несложная?
                      0

                      Вы не поверите, именно это я и написал, теми же словами :)

                    0
                    Ха, при чтении поста не покидало желание скинуть в комменты линк на другой Ваш перевод. И тут на тебе, заключение.
                      0
                      Спидраннеры. Спидраннеры заметят. Вскоре после выхода игры они заметили, что некоторые люди в списках рекордов спидрана имели более плохое время прохождения, но по подсчёту оказавшееся более хорошим, чем у других. И непосредственной причиной этого была нечёткость таймингов и отключение vsync в игре (или 144-герцовые мониторы). Поэтому стало очевидно, что нужно выключать эту нечёткость при отключении vsync.


                      Лично мне стало очевидно, что необходимо вводить две отдельные категории для спидранов. Все баги в программе не поправить, найти новые баги в программе упорные спидранеры всегда смогут. Какой смысл ориентироваться на них?
                        +1
                        дело не только в спидранерах, запустите старую игру, вроде half-life и попробуйте «двинуть ящик» он улетит очень далеко, а все дело в том, что было рассчитано на 20-30 фпс, а при фпс 100 (комфортный по плавности) все ускоряется.
                          +1
                          Насколько я знаю, в спидранах уже давно есть категории по использованию багов, причем больше двух и для разных игр разные (в зависимости от того, насколько эти баги влияют на геймплей).
                          А неправильный подсчет времени, который зависит от случайных факторов — это довольно неприятно, и не только для спидраннеров. Если его можно исправить без критических проблем, то очевидно, что лучше это сделать.
                            0
                            Насколько я знаю, в спидранах уже давно есть категории по использованию багов, причем больше двух и для разных игр разные (в зависимости от того, насколько эти баги влияют на геймплей).

                            И я знаю. К чему это?

                            А неправильный подсчет времени, который зависит от случайных факторов — это довольно неприятно, и не только для спидраннеров.

                            Для кого ещё кроме спидранеров и разработчика (по вполне очевидным причинам)?
                          0
                          Update c произвольным deltaTime всё равно будет источником накопления ошибок округления.
                          .1+.2=.30000000000000004
                          Можно считать симуляцию мира с фиксированным шагом, а само отображение делать за счёт интерполяции сколько угодно FPS.
                          Т.е. непосредственно для отрисовки переменный шаг времени — это отлично, но для самой игры, проверки столкновений, ИИ — это не нужно. Непонятно почему автор не предлагает гибридный подход.
                            0
                            А можно ли в отдельном потоке в бесконечном цикле запустить функцию общёта симуляции мира, а в потоке отрисовки его отображать с фиксированным интервалом?
                              0
                              Отдельные потоки для симуляции и отрисовки уже давно используются.
                              V-Sync по факту фиксированный интервал, привязанный к монитору.
                                0
                                Nvidia и AMD топят за FreeSync.
                              0
                              А что делать, если фпс ниже чем шаг времени для изменения мира?
                                0
                                А это так и должно быть. Вполне нормально считать физику за 20-30 а то и больше итераций на кадр.
                                  0
                                  Ну это зависит от необходимой точности и метода интегрирования. Для метода Эйлера, как в статье, это так.
                                  Но для фиксированного шага можно использовать, например, интегрирование Верле — там число итераций можно сделать меньше. Не обязательно каждый кадр считать физику, особенно если там 144 Гц монитор и быстрая видеокарта.
                                    0
                                    Как раз наоборот, игра считает физику не 20/30 за кадр, а каждый кадр. Просто изначально железо позволяло дать 20/30 кадров и все работало с нужной скоростью, а на современном железе ситуация другая.
                                      +1
                                      Стоит уточнить что есть «считает физику». Новое положение объектов (анимация) действительно вычисляется каждый кадр, однако это достаточно просто, можно целиком запихнуть в вершинный шейдер чтобы видеокарта сама считала поворот и смещение.
                                      Рассчёт геометрии для коллизий, приложенных сил и импульсов, ограничений, ИИ — более тяжёлые в вычислительном плане операции, которые довольно накладно делать каждый кадр.
                                    0
                                    Можно замедлить игру (если это для одного игрока) или несколько раз за кадр пересчитывать мир (что не такая уж и редкость на самом деле).
                                    0

                                    Автор как раз ссылается на такой подход из статьи Гленна Филдера "Fix Your Timestep", сложности и недостатки подхода тоже перечислены в статье автора.

                                      0
                                      Это хорошее замечание.
                                      Да, более того — этот подход уже реализован.
                                    0
                                    Когда я делал игры с переменным шагом обновления, я просто не допускал ситуаций с deltaTime > 1. Если оно реально становилось больше, я просто повторял цикл обновления нужное количество раз, сначала с deltaTime = 1, и потом с дробным остатком. Да, это заметно медленнее, но и код значительно проще, а ситуация с регулярным deltaTime > 1 в принципе неадекватна (компьютер не тянет игру), чтобы считать её нормой, а не исключением.
                                      +1

                                      Делают расчет физики на 60 герц, а потом у людей с нормальными мониторами (144 Hz) все работает непонятно как.


                                      Апофиозом из того что я наблюдал было слизерио. На 144-герцевой развёртке невозможно было играть — жуткие дёргания 2-3 раза в секунду. Пришлось переключаться на 60 герц.


                                      Считайте ключевые точки на 60 герцах и интерполируйте между ними если сложно сделать переменный шаг.

                                        0
                                        Просто оставлю это здесь habr.com/ru/post/341198
                                          0
                                          А сбрасывание дельты при достаточном накоплении погрешности к чему приведёт?
                                          Мне кажется что это тоже может решить часть проблемы, но я в этом не уверен.
                                          Сам с рендером работал только на начальном уровне. Но в будущем планирую развернуться немного.
                                          Потому подобная статья и советы от знающих ценю :)

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

                                          Самое читаемое