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

При разработке игр с физикой в Phaser Arcade вы можете столкнуться с выбором: использовать фиксированный или переменный шаг симуляции. Этот параметр напрямую влияет на детерминированность и стабильность физических расчётов. Понимание разницы поможет вам избежать проблем с 'дрожанием' объектов на разных устройствах и создать более предсказуемое игровое поведение. В статье мы разберём наглядный пример, который демонстрирует работу обоих режимов в одной сцене.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Boot extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('flower', 'assets/sprites/flower-exo.png');
    }

    update ()
    {
        this.scene.run('fixed step').run('variable step').remove();
    }
}

class FpsTest extends Phaser.Scene
{
    debug;
    sprite;
    startFrame;
    startTime;
    text;

    create ()
    {
        this.startFrame = this.game.getFrame();

        this.startTime = this.game.getTime();

        this.sprite = this.physics.add.image(100, 225, 'flower').setVelocityX(300);

        this.debug = this.add.graphics().fillStyle(0xffff00, 0.5);

        this.text = this.add.text(20, 20);
    }

    update ()
    {
        this.text.setText(`
fixed step: ${this.physics.world.fixedStep}

sprite.x: ${this.sprite.x.toFixed(3)}
sprite.body.deltaX(): ${this.sprite.body.deltaX().toFixed(3)}

frames elapsed: ${this.game.getFrame() - this.startFrame}
time elapsed: ${this.game.getTime() - this.startTime} ms`);

        this.debug.fillPointShape(this.sprite.body, 2);

        if (this.sprite.x >= 700)
        {
            this.scene.pause();
        }
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    pixelArt: true,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: { debug: true }
    },
    scene: [
        new Boot({
            key: 'boot'
        }),
        new FpsTest({
            key: 'fixed step',
            physics: { arcade: { fixedStep: true } },
            cameras: [ { x: 0, y: 0, width: 800, height: 300 } ]
        }),
        new FpsTest({
            key: 'variable step',
            physics: { arcade: { fixedStep: false } },
            cameras: [ { x: 0, y: 300, width: 800, height: 300 } ]
        })
    ]
};

const game = new Phaser.Game(config);

Суть параметра fixedStep

В физическом движке Arcade параметр fixedStep определяет, как будет обновляться симуляция. Когда он установлен в true, физика рассчитывается с фиксированной частотой, независимо от фактической частоты кадров игры (FPS). Это обеспечивает стабильность и предсказуемость, что критически важно для мультиплеерных или основанных на точной физике игр.

При значении false (по умолчанию) шаг физики привязывается к времени, прошедшему с предыдущего кадра (delta time). Это может дать более плавное визуальное движение на мощных устройствах, но делает поведение менее детерминированным, особенно при проседании FPS.

Пример показывает два экземпляра одной и той же сцены, работающих параллельно с разными настройками этого параметра.

Архитектура примера: Загрузочная сцена и дублирование

Пример использует три сцены. Загрузочная сцена Boot выполняет предзагрузку ресурса и запускает два сравниваемых экземпляра основной сцены.

class Boot extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('flower', 'assets/sprites/flower-exo.png');
    }

    update ()
    {
        this.scene.run('fixed step').run('variable step').remove();
    }
}

В методе update() сцена Boot запускает обе тестовые сцены (с фиксированным и переменным шагом), а затем удаляет саму себя, так как её задача выполнена. Обратите внимание на цепочку вызовов run().

Два экземпляра сцены FpsTest создаются в конфигурации игры с разными key и настройками физики. Каждой сцене назначена своя камера, чтобы они отображались на разных половинах экрана.

new FpsTest({
    key: 'fixed step',
    physics: { arcade: { fixedStep: true } },
    cameras: [ { x: 0, y: 0, width: 800, height: 300 } ]
}),
new FpsTest({
    key: 'variable step',
    physics: { arcade: { fixedStep: false } },
    cameras: [ { x: 0, y: 300, width: 800, height: 300 } ]
})

Логика тестовой сцены FpsTest

Сцена FpsTest создаёт физический спрайт, текстовое поле для отладки и графический объект для визуализации позиции.

create ()
{
    this.startFrame = this.game.getFrame();
    this.startTime = this.game.getTime();

    this.sprite = this.physics.add.image(100, 225, 'flower').setVelocityX(300);
    this.debug = this.add.graphics().fillStyle(0xffff00, 0.5);
    this.text = this.add.text(20, 20);
}

Методы this.game.getFrame() и this.game.getTime() сохраняют начальные значения для последующего расчёта пройденных кадров и времени.

В каждом кадре метод update() обновляет текст, отображающий ключевую информацию, и рисует точку в позиции тела спрайта.

update ()
{
    this.text.setText(`
fixed step: ${this.physics.world.fixedStep}

sprite.x: ${this.sprite.x.toFixed(3)}
sprite.body.deltaX(): ${this.sprite.body.deltaX().toFixed(3)}

frames elapsed: ${this.game.getFrame() - this.startFrame}
time elapsed: ${this.game.getTime() - this.startTime} ms`);

    this.debug.fillPointShape(this.sprite.body, 2);

    if (this.sprite.x >= 700)
    {
        this.scene.pause();
    }
}

Здесь this.physics.world.fixedStep показывает текущее значение параметра для этой сцены. sprite.body.deltaX() возвращает изменение позиции по X за последний шаг физики. Когда спрайт достигает X=700, сцена ставится на паузу.

Что покажет сравнение на экране

Запустив пример, вы увидите два одинаковых спрайта, движущихся синхронно вверху и внизу экрана. Ключевое отличие — в значениях, которые выводятся в текстовых полях.

* **Спрайт с fixedStep: true:** Значение sprite.body.deltaX() будет постоянным (или почти постоянным) на каждом шаге физики, независимо от небольших колебаний FPS. Пройденное расстояние напрямую зависит от количества *шагов физики*, а не от реального времени. * **Спрайт с fixedStep: false:** Значение deltaX() будет колебаться в зависимости от времени, прошедшего между кадрами (delta time). При стабильном FPS 60 движение будет плавным, но если игра начнёт 'тормозить', изменение позиции за шаг увеличится, чтобы компенсировать пропущенное время.

Жёлтые точки, оставляемые методом fillPointShape, визуализируют позицию тела на каждом шаге физики. При фиксированном шаге расстояние между точками будет одинаковым. При переменном — может отличаться.

Что попробовать дальше

Используйте fixedStep: true для игр, где важна точность и повторяемость физической симуляции (платформеры, головоломки, мультиплеер). fixedStep: false (режим по умолчанию) лучше подходит для визуальных проектов, где плавность важнее абсолютной точности, и вы готовы смириться с небольшими вариациями в поведении на разных устройствах. **Идеи для экспериментов:** 1. Добавьте в сцену искусственную нагрузку (тяжёлые вычисления в update), чтобы симулировать просадку FPS, и понаблюдайте за расхождением в поведении спрайтов. 2. Попробуйте создать сцену с множеством сталкивающихся тел и сравните стабильность их взаимодействия в двух режимах. 3. Исследуйте, как параметр влияет на работу методов body.velocity и body.acceleration на длинных дистанциях.