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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.atlas('gems', 'assets/tests/columns/gems.png', 'assets/tests/columns/gems.json');
        this.load.image('mushroom', 'assets/sprites/mushroom2.png');
        this.load.image('wizball', 'assets/sprites/wizball.png');
    }

    create ()
    {
        const spriteBounds = Phaser.Geom.Rectangle.Inflate(Phaser.Geom.Rectangle.Clone(this.physics.world.bounds), -100, -100);

        this.anims.create({ key: 'diamond', frames: this.anims.generateFrameNames('gems', { prefix: 'diamond_', end: 15, zeroPad: 4 }), repeat: -1 });
        this.anims.create({ key: 'prism', frames: this.anims.generateFrameNames('gems', { prefix: 'prism_', end: 6, zeroPad: 4 }), repeat: -1 });
        this.anims.create({ key: 'ruby', frames: this.anims.generateFrameNames('gems', { prefix: 'ruby_', end: 6, zeroPad: 4 }), repeat: -1 });
        this.anims.create({ key: 'square', frames: this.anims.generateFrameNames('gems', { prefix: 'square_', end: 14, zeroPad: 4 }), repeat: -1 });

        this.physics.add.staticImage(400, 300, 'mushroom');

        const anims = [ 'diamond', 'prism', 'ruby', 'square' ];

        //  Some normal dynamic bodies
        for (let i = 0; i < 10; i++)
        {
            const anim = Phaser.Utils.Array.GetRandom(anims);

            const pos = Phaser.Geom.Rectangle.Random(spriteBounds);

            const block = this.physics.add.sprite(pos.x, pos.y, 'gems');

            block.setVelocity(Phaser.Math.Between(-100, 100), Phaser.Math.Between(-100, 100));
            block.setBounce(1).setCollideWorldBounds(true);
            block.play(anim);

            //  Each type will have its own debug body color:
            if (anim === 'diamond')
            {
                block.body.debugBodyColor = 0xadfefe;
            }
            else if (anim === 'prism')
            {
                block.body.debugBodyColor = 0x09b500;
            }
            else if (anim === 'ruby')
            {
                block.body.debugBodyColor = 0xb21d0a;
            }
            else if (anim === 'square')
            {
                block.body.debugBodyColor = 0x930ebe;
            }
        }

        //  A dynamic circular body
        const ball = this.physics.add.image(100, 400, 'wizball').setCircle(46);

        ball.setVelocity(Phaser.Math.Between(-100, 100), Phaser.Math.Between(-100, 100));
        ball.setBounce(1).setCollideWorldBounds(true);

        //  A static circular body
        this.physics.add.staticImage(500, 100, 'wizball').setCircle(46);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            debug: true,
            debugShowBody: true,
            debugShowStaticBody: true,
            debugShowVelocity: true,
            debugVelocityColor: 0xffff00,
            debugBodyColor: 0x0000ff,
            debugStaticBodyColor: 0xffffff
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и анимаций

В методе preload() загружаются необходимые ассеты: атлас спрайтов gems и отдельные изображения. Ключевая настройка происходит в create(). Сначала создаётся ограничивающая область spriteBounds на основе мировых границ физики (this.physics.world.bounds), уменьшенная на 100 пикселей с каждой стороны. Это гарантирует, что спавнящиеся объекты будут появляться в отдалении от краёв экрана.

Далее создаются четыре анимации на основе кадров из атласа gems. Каждая анимация (diamond, prism, ruby, square) будет проигрываться в цикле на соответствующих спрайтах.

const spriteBounds = Phaser.Geom.Rectangle.Inflate(Phaser.Geom.Rectangle.Clone(this.physics.world.bounds), -100, -100);

this.anims.create({ key: 'diamond', frames: this.anims.generateFrameNames('gems', { prefix: 'diamond_', end: 15, zeroPad: 4 }), repeat: -1 });
this.anims.create({ key: 'prism', frames: this.anims.generateFrameNames('gems', { prefix: 'prism_', end: 6, zeroPad: 4 }), repeat: -1 });
this.anims.create({ key: 'ruby', frames: this.anims.generateFrameNames('gems', { prefix: 'ruby_', end: 6, zeroPad: 4 }), repeat: -1 });
this.anims.create({ key: 'square', frames: this.anims.generateFrameNames('gems', { prefix: 'square_', end: 14, zeroPad: 4 }), repeat: -1 });

Создание тел и базовая настройка физики

В сцене создаются объекты разных типов. this.physics.add.staticImage добавляет статичное тело (гриб), которое не двигается при столкновениях. Затем в цикле генерируется 10 динамических тел (this.physics.add.sprite). Каждому случайным образом назначается одна из четырёх анимаций и позиция в пределах spriteBounds.

Каждое тело получает случайную начальную скорость и настройку упругости (setBounce(1)), а также включается отскок от границ мира. Это создаёт хаотичное движение объектов.

this.physics.add.staticImage(400, 300, 'mushroom');

const anims = [ 'diamond', 'prism', 'ruby', 'square' ];

for (let i = 0; i < 10; i++)
{
    const anim = Phaser.Utils.Array.GetRandom(anims);
    const pos = Phaser.Geom.Rectangle.Random(spriteBounds);
    const block = this.physics.add.sprite(pos.x, pos.y, 'gems');
    block.setVelocity(Phaser.Math.Between(-100, 100), Phaser.Math.Between(-100, 100));
    block.setBounce(1).setCollideWorldBounds(true);
    block.play(anim);
}

Кастомизация цветов для отладки

Самая важная часть примера — присвоение своего цвета отладки для каждого типа анимации. После создания и базовой настройки спрайта, в блоке if проверяется название его анимации. В зависимости от результата, свойству debugBodyColor физического тела (block.body) присваивается уникальное HEX-значение цвета.

Это свойство имеет приоритет над глобальным цветом debugBodyColor, заданным в конфигурации физики. Таким образом, все «бриллианты» станут бирюзовыми, «призмы» — зелёными, и так далее. Это мгновенно визуально группирует объекты.

if (anim === 'diamond')
{
    block.body.debugBodyColor = 0xadfefe;
}
else if (anim === 'prism')
{
    block.body.debugBodyColor = 0x09b500;
}
else if (anim === 'ruby')
{
    block.body.debugBodyColor = 0xb21d0a;
}
else if (anim === 'square')
{
    block.body.debugBodyColor = 0x930ebe;
}

Работа с круглыми хитбоксами

Пример также показывает работу с круглыми телами. Динамический шар (wizball) создаётся через this.physics.add.image. Метод .setCircle(46) заменяет стандартный прямоугольный хитбокс на круглый с радиусом 46 пикселей, что больше подходит для формы мяча. Ему также задаётся скорость и упругость.

Затем создаётся статический круглый объект из того же спрайта. Обратите внимание: для статического тела цвет отладки по умолчанию будет белым (debugStaticBodyColor: 0xffffff), как и задано в глобальной конфигурации, если не переопределить его индивидуально.

const ball = this.physics.add.image(100, 400, 'wizball').setCircle(46);
ball.setVelocity(Phaser.Math.Between(-100, 100), Phaser.Math.Between(-100, 100));
ball.setBounce(1).setCollideWorldBounds(true);

this.physics.add.staticImage(500, 100, 'wizball').setCircle(46);

Включение отладочного режима в конфиге

Вся отладочная визуализация работает только при явном включении отладки в настройках физического движка Arcade. В конфигурации игры необходимо установить debug: true, а также активировать отображение нужных элементов: тел, статических тел и векторов скорости. Здесь же задаются цвета по умолчанию, которые будут использоваться, если для конкретного тела не назначен свой debugBodyColor.

physics: {
    default: 'arcade',
    arcade: {
        debug: true,
        debugShowBody: true,
        debugShowStaticBody: true,
        debugShowVelocity: true,
        debugVelocityColor: 0xffff00,
        debugBodyColor: 0x0000ff,
        debugStaticBodyColor: 0xffffff
    }
}

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

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