Время и FPS: delta time
Игра, которая на быстром ноутбуке летает, а на стареньком ползёт — это баг. Лечится он одним числом: дельтой времени.
Суть: delta time (dt) — это сколько секунд прошло с прошлого кадра. Умножая скорость на dt, мы делаем движение одинаковым на любом железе.
Представь, что ты двигаешь героя на 5 пикселей каждый кадр. На компьютере, где 60 кадров в секунду, герой за секунду пройдёт 300 пикселей. А на мощном игровом ПК с 240 кадрами — целых 1200! Одна и та же игра на разных машинах играется по-разному. Это классическая ловушка, в которую попадают все новички.
Решение придумали давно: измерять не «сколько кадров», а «сколько времени». Перед тем как двигать героя, мы спрашиваем у часов: сколько секунд прошло с прошлого кадра? Это число и есть delta time, или dt. Обычно оно крошечное — около 0.016 секунды (это 1/60). Дальше мы говорим: герой движется со скоростью 300 пикселей в СЕКУНДУ, а за этот кадр он сдвинется на 300 * dt пикселей.
Магия в том, что на медленной машине dt будет больше (кадры реже), и за один кадр герой шагнёт дальше — итог за секунду совпадёт. Движение становится привязанным к реальному времени, а не к скорости железа.
Как работает под капотом
Метод clock.tick(60) не только ограничивает FPS, но и ВОЗВРАЩАЕТ количество миллисекунд, прошедших с прошлого вызова. Делим на 1000 — получаем секунды. Вот связь скорости, времени и расстояния:
расстояние за кадр = скорость(пикс/сек) x dt(сек) FPS=60 -> dt=0.0166 -> шаг = 300 x 0.0166 = 5.0 пикс FPS=30 -> dt=0.0333 -> шаг = 300 x 0.0333 = 10.0 пикс FPS=240 -> dt=0.0041 -> шаг = 300 x 0.0041 = 1.25 пикс за 1 секунду везде: 300 пикселей. ИТОГ ОДИНАКОВ.
В коде pygame это выглядит так:
clock = pygame.time.Clock()
x = 100.0
SPEED = 300 # пикселей в секунду
while running:
dt = clock.tick(60) / 1000.0 # секунд с прошлого кадра
# ... обработка событий ...
x += SPEED * dt # движение, не зависящее от FPS
screen.fill((0, 0, 0))
pygame.draw.rect(screen, (255, 80, 80), (x, 300, 40, 40))
pygame.display.flip()Докажем, что dt действительно выравнивает движение. Посчитаем, сколько пройдёт герой за 1 секунду при трёх разных FPS — числа должны совпасть. Попробуй сам:
SPEED = 300.0 # пикселей в секунду
def distance_in_one_second(fps):
dt = 1.0 / fps # секунд на один кадр
x = 0.0
for _ in range(fps): # столько кадров за секунду
x += SPEED * dt
return x
for fps in (30, 60, 144, 240):
print(f"FPS={fps}: пройдено {distance_in_one_second(fps):.1f} пикселей")Подводный камень: «спираль смерти»
У delta time есть редкая, но коварная ловушка. Представь, что компьютер на секунду «завис» (свернул окно, переключился на другую программу). Тогда dt окажется огромным — например, целую секунду. Умножив скорость на такой dt, герой телепортируется через полэкрана и может проскочить сквозь стену, не заметив коллизии. В платформерах из-за этого персонаж проваливается под пол. Профессиональное решение — ограничивать dt сверху: dt = min(clock.tick(60) / 1000.0, 0.05). Даже если кадр длился секунду, мир сдвинется максимум как за 0.05 секунды.
Это компромисс: при сильном лаге игра на миг «замедлится» вместо того, чтобы сломаться. Для аркад и платформеров это почти всегда правильный выбор — лучше лёгкое замедление, чем герой, провалившийся сквозь карту. Запомни этот приём с min: он крошечный, а спасает от целого класса странных багов, которые иначе можно искать часами, не понимая, почему «иногда» всё ломается.
Полезно один раз прочувствовать масштаб чисел. При 60 кадрах в секунду dt равен примерно 0.0166 секунды — это всего шестнадцать тысячных. Кажется, такой крошечной величиной можно пренебречь, но именно из сложения тысяч таких микрошагов и складывается плавное движение за секунды и минуты игры. Поэтому точность здесь важна: храни позиции во float, а не в целых числах, иначе дробные приращения вроде 1.25 пикселя будут округляться до нуля или единицы, и герой на медленных скоростях начнёт двигаться рывками или вовсе застывать. Округляй координаты только в самый последний момент — при передаче в Rect для отрисовки, — а всю внутреннюю математику веди в дробных числах. Это маленькое правило избавляет от целого класса трудноуловимых багов с «дрожащим» или «прилипающим» движением.
Частые ошибки
- Двигать на фиксированное число пикселей за кадр — скорость поедет от FPS.
- Забыть поделить на 1000 — dt окажется в миллисекундах, и герой улетит в космос.
- Хранить координаты в int — мелкие шаги вроде 1.25 будут теряться. Держи x, y как float.
Best practices
- Считай
dt = clock.tick(60) / 1000.0один раз в начале каждого кадра. - Задавай скорости в «пикселях в секунду» — это интуитивно и переносимо.
- Координаты объектов держи в float, округляй только при отрисовке.
Итог: dt — это мост между кадрами и реальным временем. Умножай скорость на dt — и твоя игра будет играться одинаково и на калькуляторе, и на игровом ПК.