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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    graphics;
    blocks;
    balls;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('ball', 'assets/sprites/yellow_ball.png');
        this.load.image('block', 'assets/sprites/32x32.png');
    }

    create ()
    {
        // this.physics.world.setBounds(50, 50, 700, 500);

        this.graphics = this.add.graphics();

        this.blocks = this.physics.add.staticGroup({
            key: 'block',
            frameQuantity: 20
        });

        Phaser.Actions.PlaceOnRectangle(this.blocks.getChildren(), new Phaser.Geom.Rectangle(100, 100, 600, 400));

        this.blocks.refresh();

        this.balls = this.physics.add.group({
            key: 'ball',
            frameQuantity: 12,
            collideWorldBounds: true,
            bounceX: 1,
            bounceY: 1,
            velocityX: 200,
            velocityY: 200
        });

        Phaser.Actions.RandomRectangle(this.balls.getChildren(), this.physics.world.bounds);

        this.physics.add.collider(this.balls);
        this.physics.add.collider(this.balls, this.blocks);

        this.createWorldGui(this.physics.world);
    }

    update ()
    {
        this.physics.world.wrap(this.balls);

        this.graphics.clear().fillStyle(0).fillRectShape(this.physics.world.bounds);
    }

    createWorldGui (world)
    {
        const gui = new dat.GUI({ width: 400 });

        const bounds = gui.addFolder('bounds');
        bounds.add(world.bounds, 'x', -400, 400, 10);
        bounds.add(world.bounds, 'y', -300, 300, 10);
        bounds.add(world.bounds, 'width', 0, 800, 10);
        bounds.add(world.bounds, 'height', 0, 600, 10);

        const check = gui.addFolder('checkCollision');
        check.add(world.checkCollision, 'left');
        check.add(world.checkCollision, 'up');
        check.add(world.checkCollision, 'right');
        check.add(world.checkCollision, 'down');

        const defaults = gui.addFolder('defaults');
        defaults.add(world.defaults, 'debugShowBody');
        defaults.add(world.defaults, 'debugShowStaticBody');
        defaults.add(world.defaults, 'debugShowVelocity');
        defaults.addColor(world.defaults, 'bodyDebugColor');
        defaults.addColor(world.defaults, 'staticBodyDebugColor');
        defaults.addColor(world.defaults, 'velocityDebugColor');

        const debug = gui.addFolder('debugGraphic');
        debug.add(world.debugGraphic, 'visible');
        debug.add(world.debugGraphic, 'clear');

        gui.add(world, 'drawDebug');

        gui.add(world, 'fixedStep');

        gui.add(world, 'fps', 5, 300, 5).onChange((fps) => { world.setFPS(fps); });

        gui.add(world, 'forceX');

        const gravity = gui.addFolder('gravity');
        gravity.add(world.gravity, 'x', -300, 300, 10);
        gravity.add(world.gravity, 'y', -300, 300, 10);

        // gui.add(world, 'isPaused');

        gui.add(world, 'OVERLAP_BIAS', 0, 16, 1);

        gui.add(world, 'pause');

        gui.add(world, 'resume');

        gui.add(world, 'timeScale', 0.1, 10, 0.1);

        return gui;
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: 0x222222,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',

        // https://newdocs.phaser.io/docs/3.55.2/Phaser.Types.Physics.Arcade.ArcadeWorldConfig

        arcade: {
            checkCollision: {
                up: true,
                down: true,
                left: true,
                right: true
            },
            debug: true,
            debugBodyColor: 0xff00ff,
            debugShowBody: true,
            debugShowStaticBody: true,
            debugShowVelocity: true,
            debugStaticBodyColor: 0x0000ff,
            debugVelocityColor: 0x00ff00,
            fixedStep: true,
            forceX: false,
            fps: 60,
            gravity: {
                x: 0,
                y: 0
            },
            height: 600,
            isPaused: false,
            maxEntries: 16,
            overlapBias: 4,
            tileBias: 16,
            timeScale: 1,
            useTree: true,
            width: 800,
            x: 0,
            y: 0
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Настройка сцены и загрузка ассетов

Класс Example расширяет Phaser.Scene. В методе preload мы загружаем два спрайта: шарик (ball) и блок (block). Эти изображения будут использоваться для визуализации физических тел.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('ball', 'assets/sprites/yellow_ball.png');
    this.load.image('block', 'assets/sprites/32x32.png');
}

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

Создание статических и динамических тел

Сначала мы создаем графический объект this.graphics для отрисовки границ мира. Затем создается статическая группа this.blocks. Статические тела не подвержены силам, но с ними можно сталкиваться.

this.blocks = this.physics.add.staticGroup({
    key: 'block',
    frameQuantity: 20
});

Метод Phaser.Actions.PlaceOnRectangle расставляет все блоки по периметру заданного прямоугольника. После изменения позиций статических тел необходимо вызвать refresh(), чтобы физический движок обновил их границы для корректных коллизий.

Phaser.Actions.PlaceOnRectangle(this.blocks.getChildren(), new Phaser.Geom.Rectangle(100, 100, 600, 400));
this.blocks.refresh();

Динамическая группа this.balls создается с начальной скоростью и упругостью. Свойство collideWorldBounds: true заставляет шары отскакивать от границ мира. Метод RandomRectangle случайно размещает шары в пределах мировых границ.

this.balls = this.physics.add.group({
    key: 'ball',
    frameQuantity: 12,
    collideWorldBounds: true,
    bounceX: 1,
    bounceY: 1,
    velocityX: 200,
    velocityY: 200
});
Phaser.Actions.RandomRectangle(this.balls.getChildren(), this.physics.world.bounds);

Коллизии настраиваются двумя вызовами this.physics.add.collider: первый — для столкновений шаров друг с другом, второй — для столкновений шаров с блоками.

Управление миром через dat.GUI

Метод createWorldGui создает интерактивную панель для управления свойствами объекта this.physics.world. Это позволяет менять параметры в реальном времени без перезагрузки.

**Границы мира (bounds):** Можно двигать (`x,y) и менять размер (width,height) физического мира. Изменение границ влияет на поведение тел с включеннымcollideWorldBounds`.

bounds.add(world.bounds, 'x', -400, 400, 10);

**Направления коллизий (checkCollision):** Можно отключать проверку столкновений с определенной стороной мира. Например, отключив up, шары перестанут отскакивать от верхней границы.

check.add(world.checkCollision, 'up');

**Гравитация (gravity):** Динамическое изменение силы тяжести по осям X и Y. Положительное значение `y` тянет объекты вниз.

gravity.add(world.gravity, 'x', -300, 300, 10);

**Отладка (debugGraphic, drawDebug):** Включает визуализацию хитбоксов тел (статических и динамических) и векторов скорости. Цвета отладки также можно менять.

debug.add(world.debugGraphic, 'visible');
gui.add(world, 'drawDebug');

**Другие важные параметры:** * fixedStep: Фиксированный шаг физики. При false используется дельта-тайм. * fps: Целевая частота обновления физики. Изменяется через world.setFPS(fps). * timeScale: Глобальный множитель скорости времени для физики. * OVERLAP_BIAS: Смещение для разрешения перекрытий. * pause / resume: Кнопки для приостановки и возобновления симуляции.

Игровой цикл и визуализация границ

В методе update происходят два ключевых действия. Во-первых, метод this.physics.world.wrap(this.balls) обеспечивает телепортацию объектов на противоположную сторону мира, когда они его покидают. Это создает эффект замкнутого пространства.

update ()
{
    this.physics.world.wrap(this.balls);
    ...
}

Во-вторых, мы постоянно перерисовываем черный прямоугольник, который точно соответствует текущим границам физического мира (this.physics.world.bounds). Это дает наглядную визуальную связь между параметрами в GUI и тем, что происходит на экране.

this.graphics.clear().fillStyle(0).fillRectShape(this.physics.world.bounds);

Конфигурация физического движка

В конфигурации игры (config) в разделе physics.arcade задаются начальные параметры мира. Именно эти значения позже можно менять через панель dat.GUI. Обратите внимание на свойство debug: true, которое изначально активирует отладочную отрисовку.

arcade: {
    checkCollision: {
        up: true,
        down: true,
        left: true,
        right: true
    },
    debug: true,
    debugBodyColor: 0xff00ff,
    debugShowBody: true,
    debugShowStaticBody: true,
    debugShowVelocity: true,
    gravity: {
        x: 0,
        y: 0
    },
    timeScale: 1
}

Свойство checkCollision по умолчанию активно со всех сторон. gravity изначально равна нулю. timeScale равен 1, что означает нормальную скорость симуляции.

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

Пример демонстрирует, что физический мир в Phaser Arcade — это гибкая и интерактивная система. Используя подобный инструмент отладки, вы можете быстро подобрать идеальные значения гравитации, упругости и границ для своей игры. Попробуйте экспериментировать: создайте группу тел с разной массой, включите силу ветра (forceX) и наблюдайте за хаотическим движением, или настройте OVERLAP_BIAS для решения проблем с "дрожанием" объектов при столкновениях. Это отличный способ почувствовать физику перед тем, как интегрировать её в реальный игровой процесс.