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

При создании игр с физикой часто нужно знать не просто факт столкновения, а его детали: объект уперся, скользит или провалился в другой спрайт. Встроенный физический движок Arcade Phaser предоставляет для этого три ключевых свойства тела (Body): `blocked`, `touching` и `embedded`. Этот пример наглядно демонстрирует их поведение в динамике и учит визуализировать эту информацию для отладки и создания сложных игровых механик.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    block1;
    block2;
    block3;
    flower;
    graphics;
    text;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('block', 'assets/sprites/block.png');
        this.load.image('flower', 'assets/sprites/flower-exo.png');
        this.load.image('atari', 'assets/sprites/atari800.png');
    }

    create ()
    {
        this.block1 = this.physics.add.image(400, 150, 'block').setImmovable(true);
        this.block2 = this.physics.add.image(400, 300, 'block').setImmovable(true);
        this.block3 = this.physics.add.staticImage(400, 450, 'block');

        this.flower = this.physics.add.image(400, 100, 'flower').setCollideWorldBounds(true);

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

        this.text = this.add.text(0, 0, '-');

        this.physics.add.collider(this.flower, [
            this.block1,
            this.block2,
            this.block3
        ]);

        this.time.delayedCall(2000, () =>
        {
            this.block1.disableBody(true, true);
            this.flower.setGravity(0, 600);
        });

        this.time.delayedCall(4000, () =>
        {
            this.block2.disableBody(true, true);
        });

        this.time.delayedCall(6000, () =>
        {
            this.block3.disableBody(true, true);
        });
    }

    update ()
    {
        this.graphics.clear();
        this.graphics.lineStyle(1, 0x666666);
        this.graphics.strokeRectShape(this.physics.world.bounds);

        this.draw(this.block1);
        this.draw(this.block2);
        this.draw(this.block3);
        this.draw(this.flower);

        this.text.setText([
            'Flower:',
            '',
            JSON.stringify(
                Phaser.Utils.Objects.Pick(
                    this.flower.body,
                    [ 'blocked', 'touching', 'embedded' ]
                ),
                null,
                2
            )
        ]);
    }

    draw (gameObject)
    {
        if (!gameObject.active) { return; }

        const { body } = gameObject;

        this.graphics.lineStyle(11, 0xffff00, 0.5);

        this.drawFaces(body, body.touching);

        this.graphics.lineStyle(11, 0xff0000, 0.5);

        this.drawFaces(body, body.blocked);

        if (body.embedded)
        {
            this.graphics.lineStyle(5, 0x00ccff, 0.5);
            this.graphics.lineBetween(body.left, body.top, body.right, body.bottom);
            this.graphics.lineBetween(body.left, body.bottom, body.right, body.top);
        }
    }

    drawFaces (body, faces)
    {
        if (faces.left)
        {
            this.graphics.lineBetween(body.left, body.top, body.left, body.bottom);
        }

        if (faces.up)
        {
            this.graphics.lineBetween(body.left, body.top, body.right, body.top);
        }

        if (faces.right)
        {
            this.graphics.lineBetween(body.right, body.top, body.right, body.bottom);
        }

        if (faces.down)
        {
            this.graphics.lineBetween(body.left, body.bottom, body.right, body.bottom);
        }
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            debug: false,
            x: 200, y: 50, width: 400, height: 500
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Настройка сцены: три препятствия и один активный спрайт

В методе create() создаются четыре физических тела. Три из них — статичные или неподвижные препятствия (block1, block2, block3). Обратите внимание на разницу в создании: первые два используют .setImmovable(true), а третий создается сразу как staticImage. Практический результат одинаков — они не будут двигаться при столкновениях, но техническая реализация отличается.

Активный спрайт flower создается как динамическое тело, которое может сталкиваться с границами мира. Коллайдер связывает flower со всеми тремя блоками.

this.block1 = this.physics.add.image(400, 150, 'block').setImmovable(true);
this.block2 = this.physics.add.image(400, 300, 'block').setImmovable(true);
this.block3 = this.physics.add.staticImage(400, 450, 'block');

this.flower = this.physics.add.image(400, 100, 'flower').setCollideWorldBounds(true);

this.physics.add.collider(this.flower, [
    this.block1,
    this.block2,
    this.block3
]);

Динамическое изменение мира: удаление препятствий

Сценарий использует таймеры this.time.delayedCall, чтобы последовательно удалять блоки из физического мира. Это демонстрирует, как свойства тела flower реагируют на исчезновение опоры.

Метод disableBody(true, true) полностью отключает тело и скрывает игровой объект. После удаления первого блока на flower включается гравитация, заставляя его падать на следующие препятствия.

this.time.delayedCall(2000, () =>
{
    this.block1.disableBody(true, true);
    this.flower.setGravity(0, 600);
});

Свойства тела: blocked, touching и embedded

Вся магия происходит в методе update(), где текст выводит значения трех ключевых свойств тела flower.

* blocked: Указывает, движение по какой из осей (up, down, left, right) полностью заблокировано другим телом прямо сейчас. Например, если спрайт стоит на платформе, blocked.down будет true. * touching: Показывает, по какой границе происходит контакт (касание) с другим телом в текущем кадре. * embedded: Флаг, который становится true, если тело "внедрилось" или "провалилось" внутрь другого тела больше, чем позволяет допуск. Это признак потенциальной ошибки или очень высокой скорости.

Для вывода используется утилита Phaser.Utils.Objects.Pick, которая создает новый объект только с нужными свойствами из this.flower.body.

this.text.setText([
    'Flower:',
    '',
    JSON.stringify(
        Phaser.Utils.Objects.Pick(
            this.flower.body,
            [ 'blocked', 'touching', 'embedded' ]
        ),
        null,
        2
    )
]);

Визуализация для отладки

Метод draw() — это мощный инструмент отладки. Для активных тел он рисует цветные рамки вокруг граней, где активны те или иные флаги.

* **Желтый цвет (touching)**: Показывает, по каким граням есть простое касание. * **Красный цвет (blocked)**: Показывает, какие грани заблокированы для движения. * **Голубой "крест" (embedded)**: Появляется, если тело встроилось в другое, визуализируя факт серьезного пересечения.

Метод drawFaces рисует линии только по тем сторонам (left, up, right, down), для которых в переданном объекте faces установлено true.

drawFaces (body, faces)
{
    if (faces.left)
    {
        this.graphics.lineBetween(body.left, body.top, body.left, body.bottom);
    }
    // ... Аналогично для up, right, down
}

Конфигурация мира и его границ

В конфиге игры важно обратить внимание на настройки физического мира Arcade. Свойства x, y, width, height смещают и ограничивают физические границы мира (this.physics.world.bounds), которые в методе update() рисуются серой рамкой. Это позволяет создать область для симуляции, отличную от всего холста.

physics: {
    default: 'arcade',
    arcade: {
        debug: false, // Визуальная отладка движка выключена, мы рисуем свою
        x: 200, y: 50, width: 400, height: 500
    }
},

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

Свойства blocked, touching и embedded — это основа для создания продвинутой логики взаимодействий. Например, blocked.down можно использовать для проверки, стоит ли персонаж на земле, а touching.left — для определения, кажется ли он со стеной. Флаг embedded критически важен для детектирования и обработки "состояний проваливания", которые могут сломать геймплей. Попробуйте поэкспериментировать: измените скорость падения flower, чтобы вызвать срабатывание embedded, или создайте механику "скольжения" по стене, проверяя blocked.left и touching.left одновременно.