Delta time: движение независимо от FPS
Скорость в «пикселях за кадр» зависит от чужого железа. Скорость в «пикселях за секунду», умноженная на delta time, — нет.
В мини-проекте раздела 20.5 мяч двигался так: x += dx на
каждом кадре. Это работает — но только пока FPS постоянен. У этого подхода есть скрытая
проблема, которая рано или поздно проявляется на реальных устройствах.
Проблема: движение, привязанное к кадрам
Если dx — это «пикселей за кадр», то итоговая скорость мяча
в пикселях за секунду напрямую зависит от того, сколько кадров реально успевает
пройти за секунду, а не от того, что вы попросили у clock.tick().
clock.tick(60) из раздела 20.5 задаёт только верхний
предел — 60 кадров в секунду, не больше — но ничего не может сделать, если устройство
не справляется и с этим. На игровом ноутбуке разработчика игра стабильно держит все 60 FPS;
на слабом или перегретом (раздел 20.13) устройстве игрока код одного кадра может не
укладываться в бюджет, и реальная частота проседает, скажем, до 30 FPS. С кодом «пикселей за
кадр» это означает, что мяч у такого игрока движется вдвое медленнее, чем у разработчика,
при абсолютно одинаковом коде — игра, которая ощущается правильно на одном устройстве, может
оказаться заметно медленнее или быстрее на другом.
Решение: delta time
Delta time (сокращённо dt) — время, реально
прошедшее с предыдущего кадра, в секундах. clock.tick(FPS) не
только ограничивает частоту кадров, но и возвращает число миллисекунд, прошедших с
предыдущего вызова clock.tick(), — то есть полное время кадра,
включая ожидание, если оно вообще было:
clock = pygame.time.Clock()
while rabotaet:
dt_ms = clock.tick(FPS) # сколько миллисекунд прошло с прошлого кадра
dt = dt_ms / 1000 # переводим в секунды
x += skorost_x * dt # skorost_x теперь в пикселях за СЕКУНДУ, не за кадр
Теперь skorost_x означает «пикселей в секунду» — величину,
не зависящую от FPS вообще. На 30, 60 или 120 кадрах в секунду мяч за одну и ту же реальную
секунду пройдёт одно и то же расстояние: на медленном устройстве шаг каждого кадра будет
крупнее, на быстром — мельче, но их сумма за секунду останется одинаковой.
Важно, что tick() сообщает именно полное время кадра, а
не то, сколько он прождал. Если устройство не справляется, ждать ему вообще не приходится — и
тогда tick(60) вернёт не 16, а, скажем, 30 миллисекунд: ровно
столько, сколько заняла работа кадра. Именно поэтому delta time и спасает на слабом
устройстве: чем медленнее кадр, тем больше dt и тем крупнее шаг.
(Отдельно время только полезной работы, без ожидания, доступно через
clock.get_rawtime().)
clock.tick(FPS) возвращает время в миллисекундах, а не в секундах. Если забыть разделить на 1000, скорость в коде окажется завышена ровно в тысячу раз — персонаж улетает за пределы экрана за один кадр. Это одна из самых частых ошибок при первом переходе на delta time, и симптом у неё всегда один и тот же: движение внезапно становится "телепортацией".x += v * dt и y += v * dt одновременно), то по диагонали он на самом деле перемещается в √2 ≈ 1.41 раза быстрее, чем строго по одной оси — обе составляющие складываются геометрически. Правильное решение — нормализовать вектор направления (привести его длину к 1) перед умножением на скорость, чтобы общая скорость оставалась одинаковой в любом направлении.