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