Вы перемещаете круг на один пиксель за кадр. Не должно быть большим сюрпризом, что, если ваш цикл рендеринга работает со скоростью 30 кадров в секунду, ваш круг будет двигаться со скоростью 30 пикселей в секунду.
Просто выберите одну частоту кадров и придерживайтесь ее. Это то, что делали многие игры старой школы - они работали с фиксированной частотой 50 или 60 кадров в секунду, обычно синхронизировались с частотой обновления экрана и просто разрабатывали игровую логику, чтобы делать все необходимое в течение этого фиксированного интервала времени. Если по какой-то причине этого не произошло, игра просто пропустит кадр (или, возможно, потерпит крах), эффективно замедляя как рисование, так и игровую физику до половины скорости.
В частности, игры, используемые функции , такие как обнаружение столкновений аппаратных спрайтов в значительной степени были работа , как это, потому что их игра логика была неразрывно связана с оказанием, что было сделано в аппаратных средств по фиксированной ставке.
Используйте переменный временной шаг для вашей игровой физики. По сути, это означает переписать игровой цикл так, чтобы он выглядел примерно так:
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);
Одна из проблем этого метода заключается в том, что сложно сохранить физику (в основном) независимой от временного шага ; Вы действительно не хотите, чтобы расстояние, на которое игроки могли прыгать, зависело от их частоты кадров. Формула, которую я показал выше, прекрасно работает для постоянного ускорения, например, под действием силы тяжести (а та, что в связанном посте, работает довольно хорошо, даже если ускорение меняется со временем), но даже с самыми совершенными физическими формулами, работа с плавающими производит немного «числового шума», который, в частности, может сделать невозможным точное воспроизведение. Если это то, что вы думаете, вы можете захотеть, вы можете предпочесть другие методы.
Разъедините обновление и нарисуйте шаги. Идея заключается в том, что вы обновляете игровое состояние с использованием фиксированного временного шага, но запускаете различное количество обновлений между каждым кадром. То есть ваш игровой цикл может выглядеть примерно так:
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 за напоминание о необходимости этого .)