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

При создании игр с физикой Arcade в Phaser важно понимать влияние параметра `fps` на поведение объектов. Этот параметр определяет, сколько раз в секунду обновляется физический мир, и напрямую влияет на точность и плавность движения. В статье мы разберем пример, который наглядно демонстрирует разницу между разными частотами обновления физики (30, 60, 144 и 300 FPS) и покажет, как отслеживать внутренние метрики движка. Понимание работы `physics.arcade.fps` поможет вам создавать более предсказуемую физику, особенно в играх, требующих точного управления или симуляции, и избегать артефактов при низких или очень высоких частотах обновления.

Версия 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('fpsTest1')
            .run('fpsTest2')
            .run('fpsTest3')
            .run('fpsTest4')
            .remove();
    }
}

class FpsTest extends Phaser.Scene
{
    graphics;
    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(100);
        this.graphics = this.add.graphics().fillStyle(0xffff00, 0.5);
        this.text = this.add.text(20, 20, '', { font: '14px monospace' });
    }

    update ()
    {
        this.graphics.fillPointShape(this.sprite.body, 1);

        this.text.setText(`
steps per second: ${this.physics.world.fps}
steps last frame: ${this.physics.world.stepsLastFrame}

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

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

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

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    pixelArt: true,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: { debug: false }
    },
    scene: [
        new Boot({
            key: 'boot'
        }),
        new FpsTest({
            key: 'fpsTest1',
            physics: { arcade: { fps: 30 } },
            cameras: [ { x: 0, y: 0, width: 400, height: 300 } ]
        }),
        new FpsTest({
            key: 'fpsTest2',
            physics: { arcade: { fps: 60 } },
            cameras: [ { x: 400, y: 0, width: 400, height: 300 } ]
        }),
        new FpsTest({
            key: 'fpsTest3',
            physics: { arcade: { fps: 144 } },
            cameras: [ { x: 0, y: 300, width: 400, height: 300 } ]
        }),
        new FpsTest({
            key: 'fpsTest4',
            physics: { arcade: { fps: 300 } },
            cameras: [ { x: 400, y: 300, width: 400, height: 300 } ]
        })
    ]
};

const game = new Phaser.Game(config);

Структура примера: загрузка и запуск сцен

Пример состоит из двух классов сцен: Boot и FpsTest. Сцена Boot служит для предварительной загрузки ресурса и запуска четырех тестовых сцен с разной конфигурацией. Важно отметить, что после запуска всех сцен, начальная сцена Boot удаляется методом remove(), чтобы не занимать память.

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

new FpsTest({
    key: 'fpsTest1',
    physics: { arcade: { fps: 30 } },
    cameras: [ { x: 0, y: 0, width: 400, height: 300 } ]
})

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

Работа сцены FpsTest: создание и обновление

В методе create() сцены FpsTest происходит инициализация всех необходимых объектов. Сначала сохраняются начальный кадр игры и время, чтобы в дальнейшем рассчитать прошедшие интервалы. Затем создается физический спрайт с помощью this.physics.add.image. Ему сразу задается начальная скорость по оси X.

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

Также создается графический объект Graphics для визуализации и текстовый объект Text для вывода статистики. В методе update() происходит основная работа: отрисовка точки в позиции тела спрайта и обновление текста с метриками.

this.graphics.fillPointShape(this.sprite.body, 1);

Этот вызов рисует маленький желтый круг прямо на теле спрайта, что позволяет визуально отслеживать его положение, заданное физическим движком.

Ключевые метрики физики Arcade

В текстовом блоке отображается несколько важных значений, которые помогают понять работу движка. Основная метрика – this.physics.world.fps, которая показывает заданную частоту обновления физического мира для данной сцены (например, 30, 60, 144 или 300).

`steps per second: ${this.physics.world.fps}`

Следующее значение – this.physics.world.stepsLastFrame. Оно показывает, сколько шагов (обновлений) физики было выполнено за последний кадр отрисовки. Это число может быть больше 1, если частота кадров игры ниже заданной частоты обновления физики.

Также выводятся координата спрайта sprite.x и два метода расчета смещения тела за шаг физики: deltaX() и deltaXFinal(). Они помогают увидеть, как движок интерполирует позицию между шагами физики для плавной отрисовки.

Логика остановки и анализ данных

Сцена останавливает свою работу, когда спрайт достигает координаты X=300. Это делается вызовом this.scene.pause(). Условие проверяется в каждом кадре в методе update().

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

Благодаря такой логике мы можем сравнить, за какое время и за сколько кадров спрайты с разной частотой физики пройдут одинаковое расстояние. В текстовом выводе также отображаются общее количество прошедших кадров игры и время в миллисекундах с момента создания сцены.

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

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

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

Частота обновления физики arcade.fps в Phaser — это мощный инструмент для тонкой настройки поведения объектов. Как показывает пример, более высокая частота (например, 300 FPS) может обеспечить более плавное и точное движение, но требует больше вычислительных ресурсов. Более низкая частота (30 FPS) может вызвать «ступенчатое» движение, но подходит для менее требовательных игр. Для экспериментов попробуйте изменить начальную скорость спрайта или расстояние до точки остановки. Или создайте пятую сцену с fps: 0, что означает использование частоты кадров игры (зависимой от монитора), и сравните ее поведение с фиксированными значениями. Это поможет лучше понять компромиссы при выборе параметров физики для вашего проекта.