Каков наилучший шаблон для создания системы, в которой все объекты должны быть интерполированы между двумя состояниями обновления?
Обновление всегда будет запускаться с одинаковой частотой, но я хочу иметь возможность рендеринга на любом FPS. Таким образом, рендеринг будет максимально плавным, независимо от количества кадров в секунду, будь то ниже или выше частоты обновления.
Я хотел бы обновить 1 кадр в будущем интерполировать из текущего кадра в будущий кадр. Этот ответ имеет ссылку, которая говорит о том, как это сделать:
Полуфиксируемый или полностью фиксированный временной шаг?
Изменить: Как я мог также использовать последнюю и текущую скорость в интерполяции? Например, при простой линейной интерполяции он будет перемещаться с одинаковой скоростью между позициями. Мне нужен способ, чтобы он интерполировал положение между двумя точками, но принимал во внимание скорость в каждой точке для интерполяции. Это было бы полезно для низкоскоростных симуляций, таких как эффекты частиц.
источник
Ответы:
Вы хотите разделить ставки обновления (логический тик) и отрисовки (рендеринг тиков).
Ваши обновления будут отображать положение всех объектов в мире, которые будут нарисованы.
Здесь я расскажу о двух разных возможностях: запрошенная вами, экстраполяция, а также другой метод, интерполяция.
1.
Экстраполяция - это то, где мы вычисляем (прогнозируемую) позицию объекта в следующем кадре, а затем интерполируем между текущей позицией объектов и позицией, в которой объект будет находиться в следующем кадре.
Для этого каждый объект должен быть связан с
velocity
иposition
. Чтобы найти позицию, в которой будет находиться объект в следующем кадре, мы просто добавляемvelocity * draw_timestep
текущую позицию объекта, чтобы найти прогнозируемую позицию следующего кадра.draw_timestep
количество времени, прошедшее с предыдущего тика рендеринга (он же предыдущий вызов отрисовки).Если вы оставите это здесь, вы обнаружите, что объекты «мерцают», когда их прогнозируемое положение не соответствует фактическому положению в следующем кадре. Чтобы убрать мерцание, вы можете сохранить прогнозируемую позицию и сделать рывок между ранее прогнозируемой позицией и новой прогнозируемой позицией на каждом шаге отрисовки, используя время, прошедшее с момента предыдущего обновления, в качестве фактора скачка. Это по-прежнему будет приводить к плохому поведению, когда быстро движущиеся объекты внезапно меняют местоположение, и вы можете решить этот особый случай. Все сказанное в этом параграфе является причиной, по которой вы не хотите использовать экстраполяцию.
2.
Интерполяция - это место, где мы храним состояние двух последних обновлений и интерполируем их между собой на основе текущего количества времени, прошедшего с момента последнего обновления. В этой настройке каждый объект должен иметь связанный
position
иprevious_position
. В этом случае наш рисунок будет представлять в худшем случае один тик обновления за текущим игровым состоянием, а в лучшем случае в том же состоянии, что и текущий тик обновления.На мой взгляд, вы, вероятно, захотите интерполяцию, как я ее описал, так как это легче реализовать из двух, и можно нарисовать крошечную долю секунды (например, 1/60 секунды) за текущим обновленным состоянием.
Редактировать:
В случае, если вышеприведенного недостаточно для выполнения реализации, вот пример того, как выполнить метод интерполяции, который я описал. Я не буду освещать экстраполяцию, потому что не могу придумать ни одного реального сценария, в котором вы бы предпочли его.
Когда вы создаете нарисованный объект, он будет хранить свойства, необходимые для рисования (т. Е. Информацию о состоянии, необходимую для его рисования).
Для этого примера мы будем хранить положение и вращение. Вы также можете сохранить другие свойства, такие как координаты цвета или текстуры (например, если прокручивается текстура).
Чтобы предотвратить изменение данных во время их отрисовки потоком рендеринга (т. Е. Расположение одного объекта во время отрисовки потока рендеринга, но все остальные еще не обновлены), нам необходимо реализовать некоторый тип двойной буферизации.
Объект хранит две его копии
previous_state
. Я положу их в массив и буду ссылаться на них какprevious_state[0]
иprevious_state[1]
. Точно так же нужно две копии этогоcurrent_state
.Чтобы отслеживать, какая копия двойного буфера используется, мы храним переменную
state_index
, которая доступна как для обновления, так и для потока отрисовки.Поток обновления сначала вычисляет все свойства объекта, используя его собственные данные (любые структуры данных, которые вы хотите). Затем он копирует
current_state[state_index]
вprevious_state[state_index]
и копирует новые данные , относящиеся к рисованию,position
иrotation
вcurrent_state[state_index]
. Затем это делаетstate_index = 1 - state_index
, чтобы перевернуть текущую используемую копию двойного буфера.Все в вышеприведенном абзаце должно быть сделано с снятой блокировкой
current_state
. Обновление и отрисовка потоков снимают эту блокировку. Блокировка снимается только на время копирования информации о состоянии, что быстро.В потоке рендеринга вы затем выполняете линейную интерполяцию по положению и повороту следующим образом:
current_position = Lerp(previous_state[state_index].position, current_state[state_index].position, elapsed/update_tick_length)
Где
elapsed
- количество времени, прошедшее в потоке рендеринга с момента последнего обновления, иupdate_tick_length
количество времени, которое ваша фиксированная частота обновления занимает на такт (например, при 20FPS обновленияхupdate_tick_length = 0.05
).Если вы не знаете, что это за
Lerp
функция, то посмотрите статью в Википедии на эту тему: Линейная интерполяция . Однако, если вы не знаете, что такое lerping, то вы, вероятно, не готовы реализовать разделенное обновление / рисование с интерполированным рисунком.источник
Lerp(previous_speed, current_speed, elapsed/update_tick_length)
). Вы можете сделать это с любым номером, который хотите сохранить в штате. Lerping просто дает вам значение между двумя значениями, учитывая коэффициент lerp.Эта проблема требует от вас думать о ваших определениях начала и конца немного по-другому. Начинающие программисты часто думают об изменении позиции в кадре, и это хороший способ начать с самого начала. Ради моего ответа, давайте рассмотрим одномерный ответ.
Допустим, у вас есть обезьяна в положении x. Теперь у вас также есть «addX», к которому вы добавляете позицию обезьяны для каждого кадра на основе клавиатуры или какого-либо другого элемента управления. Это будет работать до тех пор, пока у вас есть гарантированная частота кадров. Допустим, ваш x равен 100, а addX равен 10. После 10 кадров ваш x + = addX должен накапливаться до 200.
Теперь вместо addX, когда у вас есть переменная частота кадров, вы должны думать о скорости и ускорении. Я проведу вас через всю эту арифметику, но она очень проста. Что мы хотим знать, так это то, как далеко вы хотите путешествовать за миллисекунду (1/1000 секунды)
Если вы снимаете со скоростью 30 кадров в секунду, то ваш velX должен составлять 1/3 секунды (10 кадров из последнего примера со скоростью 30 кадров в секунду), и вы знаете, что хотите пройти 100 x за это время, поэтому установите для velX значение 100 дистанций / 10 кадров в секунду или 10 дистанций на кадр. В миллисекундах это работает до 1 расстояния x за 3,3 миллисекунды или до 0,3 'x' за миллисекунду.
Теперь, каждый раз, когда вы обновляете, все, что вам нужно сделать, это выяснить, сколько времени прошло. Независимо от того, прошло ли 33 мс (1/30 секунды) или что-то еще, вы просто умножаете расстояние 0,3 на количество пройденных миллисекунд. Это означает, что вам нужен таймер, который дает вам мс (миллисекунду) точность, но большинство таймеров дают вам это. Просто сделайте что-то вроде этого:
var beginTime = getTimeInMillisecond ()
... позже ...
var time = getTimeInMillisecond ()
var elapsedTime = time-beginTime
beginTime = время
... теперь используйте этот elapsedTime для расчета всех ваших расстояний.
источник