Глава 20 · Разработка игр с Pygame

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(), — то есть полное время кадра, включая ожидание, если оно вообще было:

delta_time.py
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, и по Y на полную скорость (x += v * dt и y += v * dt одновременно), то по диагонали он на самом деле перемещается в √2 ≈ 1.41 раза быстрее, чем строго по одной оси — обе составляющие складываются геометрически. Правильное решение — нормализовать вектор направления (привести его длину к 1) перед умножением на скорость, чтобы общая скорость оставалась одинаковой в любом направлении.
Практика: движение, независимое от FPS
Проверяется прямо в браузере — установка Pygame не требуется
Открыть практику →