Игровой цикл, время кадра и скорость движения: считаем на простом примере, откуда берутся рывки и почему FPS — не вся история.
Игра всё время повторяет небольшую работу
На экране герой идёт по площадке. Кажется, что он движется непрерывно, но программа многократно обновляет состояние мира и показывает очередное изображение. Ей нужно учитывать ввод, менять положение объектов, проверять события и готовить кадр. Организация этой повторяющейся работы называется игровым циклом.
В самом простом представлении цикл выглядит так: прочитать управление, обновить мир, нарисовать результат. Реальные движки могут разделять эти этапы между потоками и подсистемами, но для разбора движения достаточно простой модели. Главный вопрос: что означает команда «передвинуть героя на три»? Три пикселя за секунду, за кадр или за физический шаг — это три разных правила.
Ошибка, которую легко посчитать
Допустим, программа каждый кадр прибавляет к координате героя 3 пикселя. При 30 кадрах за секунду он проходит 90 пикселей, при 60 — 180, при 120 — уже 360. На более быстром компьютере правила игры фактически меняются. Это не измерение конкретной игры, а прямое следствие выбранной нами формулы.
Чтобы задать одинаковую скорость, запишем её в пикселях за секунду. Пусть это будет 180. Тогда за интервал 1/30 секунды герой проходит 6 пикселей, а за 1/60 — 3. Отдельные шаги разные, но расстояние за одинаковое суммарное время совпадает. Важно не потерять единицы: если время получено в миллисекундах, его сначала переводят в секунды.
| Частота | Время кадра | Шаг при 180 px/с |
|---|---|---|
| 30 FPS | ≈ 33,3 мс | 6 px |
| 60 FPS | ≈ 16,7 мс | 3 px |
| 120 FPS | ≈ 8,3 мс | 1,5 px |
Проверяем формулу без игрового движка
В примере ниже мы не рисуем героя. Вместо этого моделируем одну секунду движения с равными интервалами. Это позволяет отдельно проверить арифметику. В реальном браузере интервалы не идеально одинаковые, поэтому программа измеряет прошедшее время, а не просто подставляет ожидаемые 60 FPS.
После запуска измени скорость с 180 на 240. Все три результата должны измениться одинаково. Затем замени прибавление speed * dt на постоянное число 3: разница сразу станет заметна. Так можно воспроизвести ошибку и убедиться, что исправлена именно логика движения, а не только визуальное впечатление.
const speed = 180; // пикселей за секунду
for (const fps of [30, 60, 120]) {
let x = 0;
const dt = 1 / fps;
for (let frame = 0; frame < fps; frame++) {
x += speed * dt;
}
console.log(fps, Math.round(x)); // везде 180
}Почему средние 60 FPS могут дёргаться
Средняя частота не показывает, насколько равномерно приходят кадры. Представь две прогулки: в первой человек делает ровные шаги, во второй то бежит, то останавливается. Средняя скорость может совпасть, а впечатление будет разным. С кадрами похожая история: редкая длинная пауза заметна даже при неплохом среднем значении.
Поэтому при поиске рывков полезно смотреть время отдельных кадров. Причиной паузы может стать тяжёлый расчёт, загрузка ресурса или другая работа программы. Не стоит сразу уменьшать все текстуры: сначала воспроизведи момент и найди, какая операция задерживает обновление. Пустая тестовая сцена помогает отделить ошибку основного цикла от сложности конкретного уровня.
Зачем физике отдельный шаг
Переменное время кадра удобно для простого перемещения, но сложная физика требует дополнительных решений. Если за один большой шаг быстрый объект окажется с одной стороны тонкой стены на другой, примитивная проверка только конечного положения может пропустить столкновение. Меньшие фиксированные шаги расчёта помогают контролировать такую логику, хотя сами по себе не решают все проблемы столкновений.
У физического обновления и показа изображения могут быть разные частоты. Между рассчитанными состояниями можно интерполировать положение для плавного отображения. Это уже следующий уровень сложности: для учебного движущегося квадрата достаточно сначала правильно учитывать время. Не нужно внедрять сложную систему до появления задачи, которую она действительно решает.
Почему ограничение большого dt — отдельное решение
После долгой паузы можно ограничить интервал обновления, например не позволять одному шагу учитывать больше небольшой доли секунды. Это убирает огромный скачок, но меняет смысл времени: часть прошедшего интервала игра просто не отыгрывает. Для декоративного объекта это часто приемлемо; для таймера соревнования такое поведение может быть ошибкой.
Представь дверь, которая должна закрыться через десять секунд. Если её таймер уменьшается только на ограниченный dt, после сворачивания вкладки дверь останется открытой дольше ожидаемого. Если же хранить момент закрытия и сравнивать его с подходящими часами, поведение будет другим. Поэтому движение, игровое время и реальные сроки нельзя автоматически обслуживать одной формулой.
В учебной игре заранее выбери правило: мир приостанавливается или продолжает жить? Покажи паузу, если она есть. Для важных сетевых событий клиентский таймер не должен быть единственным источником истины. Даже без сложного сервера полезно записать это решение словами: оно определяет, что считать правильным результатом теста после возвращения на вкладку.
Что проверить в браузерной игре
Для покадровой анимации браузер предоставляет requestAnimationFrame. Его вызовы обычно связаны с обновлением экрана, а в фоновых вкладках часто приостанавливаются. После возвращения на вкладку может пройти большой интервал: игра должна осмысленно обработать паузу, а не неожиданно перенести героя через весь уровень.
Проверь три ситуации: обычную игру, искусственно замедленное выполнение и возвращение после переключения вкладки. Смотри отдельно на скорость, плавность и управление. Если герой проходит одинаковое расстояние за секунду, но прыжок всё ещё ведёт себя по-разному, значит, время учтено не во всех формулах. Последовательная проверка каждого движения полезнее одного общего показателя FPS.