О чем этот пример

Вы когда-нибудь замечали, что ваша игра работает с разной скоростью на мощном ПК и слабом ноутбуке? Или что анимация 'дергается' при падении кадров в секунду (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, и посмотрите, как изменится поведение.