О чем этот пример
Вы когда-нибудь замечали, что ваша игра работает с разной скоростью на мощном ПК и слабом ноутбуке? Или что анимация 'дергается' при падении кадров в секунду (FPS)? Виновник — фиксированный шаг обновления логики, привязанный к частоте кадров. В этой статье мы разберем пример из официальной документации Phaser, который демонстрирует использование дельты времени (`dt`) в методе `update`. Этот подход позволяет создавать плавное и предсказуемое движение игровых объектов, независимо от производительности устройства пользователя.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
speed = (600 / 2) / 1000;
log;
delta;
time;
image;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('bunny', 'assets/sprites/bunny.png');
}
create ()
{
this.delta = this.add.text(0, 0);
this.image = this.add.image(0, 200, 'bunny');
this.time = this.add.text(400, 400);
this.log = [];
}
update (t, dt)
{
this.image.x += this.speed * (dt);
if (this.image.x > 1000)
{
this.image.x = 0;
}
this.log.push(this.sys.game.loop.delta.toString());
if (this.log.length > 30)
{
this.log.shift();
}
this.time.setText(`time: ${this.sys.game.loop.time.toString()}`);
this.delta.setText(this.log);
}
}
const config = {
type: Phaser.CANVAS,
width: 800,
height: 600,
parent: 'phaser-example',
backgroundColor: '#9adaea',
useTicker: true,
scene: Example
};
const game = new Phaser.Game(config);
Проблема: движение, привязанное к кадрам
В самом простом случае разработчик может обновлять позицию спрайта на фиксированное значение в каждом кадре.
// ПЛОХО: движение зависит от FPS
this.image.x += 5;
Если игра работает на 60 FPS, спрайт переместится на 5 * 60 = 300 пикселей в секунду. Если FPS упадет до 30, то скорость движения составит всего 5 * 30 = 150 пикселей в секунду. Игра буквально замедлится. Для пошаговых стратегий это может быть приемлемо, но для платформеров или аркад — катастрофа.
Ключ к решению — отделить игровую логику от рендеринга. Вместо фиксированного значения `5` нам нужно значение, которое зависит от прошедшего реального времени.
Решение: дельта времени (dt) в методе Update
Phaser передает в метод update два параметра: общее время (`t) и дельту времени (dt) — количество миллисекунд, прошедших с предыдущего вызоваupdate. Именноdt` — наш главный инструмент.
В исходном примере объявлена переменная скорости, рассчитанная в пикселях за миллисекунду:
speed = (600 / 2) / 1000; // 0.3 пикселя/мс
Эта скорость означает: "объект должен пройти 600 пикселей за 2 секунды". В методе update эта скорость умножается на dt:
update (t, dt)
{
this.image.x += this.speed * (dt);
// ...
}
**Как это работает:**
- Если между кадрами прошло ровно 16.67 мс (что соответствует ~60 FPS), спрайт сдвинется на 0.3 * 16.67 ≈ 5 пикселей.
- Если из-за нагрузки кадр занял 33 мс (~30 FPS), сдвиг составит 0.3 * 33 ≈ 10 пикселей.
В итоге за одну секунду в обоих случаях спрайт переместится на запланированные 300 пикселей (0.3 пикс/мс * 1000 мс). Движение стало независимым от частоты кадров.
Мониторинг производительности: работа с игровым циклом
Пример также показывает, как получить внутреннюю информацию об игровом цикле (game.loop) для отладки. Это полезно, чтобы наглядно видеть, как ведет себя dt.
В коде создается массив log, куда сохраняются значения this.sys.game.loop.delta (это то же самое, что и параметр dt, но полученное изнутри игрового объекта).
// Сохраняем текущую дельту в массив для вывода на экран
this.log.push(this.sys.game.loop.delta.toString());
// Ограничиваем длину лога 30 записями
if (this.log.length > 30)
{
this.log.shift();
}
// Выводим общее время работы игры и лог дельт на экран
this.time.setText(`time: ${this.sys.game.loop.time.toString()}`);
this.delta.setText(this.log);
На экране вы увидите постоянно обновляющийся список последних 30 значений dt и общее время работы игры. В стабильной системе значения delta будут колебаться вокруг 16.6 мс. При просадках FPS вы увидите резкие скачки этих значений, но движение зайчика останется плавным и постоянным благодаря нашей формуле.
Конфигурация сцены и игры
Важный нюанс кроется в конфигурации игры. Обратите внимание на параметр useTicker.
const config = {
type: Phaser.CANVAS,
width: 800,
height: 600,
parent: 'phaser-example',
backgroundColor: '#9adaea',
useTicker: true, // <-- Этот параметр критически важен
scene: Example
};
Установка useTicker: true заставляет Phaser использовать собственный внутренний цикл запроса анимации (Request Animation Frame loop) для вызова update. В этом режиме параметры `tиdtпередаются в методupdateкорректно. ЕслиuseTickerустановлен вfalse(значение по умолчанию в некоторых версиях), то для обновления логики может использоваться системный таймер с фиксированным шагом, и смысл использованияdt` теряется. Всегда проверяйте этот параметр, если планируете делать движение, зависящее от времени.
Что попробовать дальше
Использование дельты времени (dt) — фундаментальная практика в разработке игр для обеспечения consistency (согласованности) игрового процесса на любом железе. Мы разобрали, как с ее помощью сделать скорость объекта постоянной, и как мониторить работу игрового цикла.
**Идеи для экспериментов:**
1. Создайте сложное движение (например, по синусоиде), которое также зависит от dt.
2. Реализуйте таймер обратного отсчета или кд (кулдаун) способностей, используя накопление переданных в update миллисекунд.
3. Попробуйте намеренно нагрузить процессор в update тяжелыми вычислениями и убедитесь, что скорость движения спрайта на экране не изменилась.
4. Поэкспериментируйте с параметром useTicker в конфиге, установив его в false, и посмотрите, как изменится поведение.
