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

Физический движок Arcade в Phaser предоставляет мощные инструменты для симуляции столкновений и реакций объектов. Однако иногда стандартного поведения недостаточно. В этой статье мы разберем, как тонко управлять тем, может ли один объект толкать другой, используя методы `setPushable()` и обработку коллизий. Это особенно полезно при создании головоломок, платформеров или игр с переключением режимов взаимодействия, где игрок временно теряет способность влиять на окружение.

Версия 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.image('block', 'assets/sprites/crate32.png');
        this.load.image('ice', 'assets/sprites/block-ice.png');
        this.load.image('be', 'assets/sprites/beball1.png');
    }

    create ()
    {
        this.player = this.physics.add.sprite(400, 300, 'be');

        this.player.setCollideWorldBounds(true);
        this.player.setPushable(false);

        const boxes = [];

        for (let i = 0; i < 16; i++)
        {
            const x = Phaser.Math.Between(0, 800);
            const y = Phaser.Math.Between(0, 600);

            const box = this.physics.add.image(x, y, 'block');

            box.setCollideWorldBounds(true);
            // box.setDrag(1000);
            box.body.slideFactor.set(0, 0);

            boxes.push(box);
        }

        for (let i = 0; i < 16; i++)
        {
            const x = Phaser.Math.Between(0, 800);
            const y = Phaser.Math.Between(0, 600);

            const box = this.physics.add.image(x, y, 'ice').setScale(0.125);

            box.setCollideWorldBounds(true);
            box.setDrag(100);
            box.setBounce(1);

            boxes.push(box);
        }

        let playerIsNPC = false;

        this.cursors = this.input.keyboard.createCursorKeys();

        this.physics.add.collider(this.player, boxes, null, (player, box) => {

            if (playerIsNPC)
            {
                box.setPushable(false);
            }
            else
            {
                box.setPushable(true);
            }

            return true;

        });

        this.input.on('pointerdown', () => {

            playerIsNPC = !playerIsNPC;

            if (playerIsNPC)
            {
                this.player.setTint(0xff0000);
            }
            else
            {
                this.player.clearTint();
            }

        });
    }

    update ()
    {
        this.player.setVelocity(0, 0);

        if (this.cursors.left.isDown)
        {
            this.player.setVelocityX(-200);
        }
        else if (this.cursors.right.isDown)
        {
            this.player.setVelocityX(200);
        }

        if (this.cursors.up.isDown)
        {
            this.player.setVelocityY(-200);
        }
        else if (this.cursors.down.isDown)
        {
            this.player.setVelocityY(200);
        }
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 0 },
            debug: true
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Инициализация сцены и загрузка ассетов

В методе preload() загружаются три спрайта: обычный блок (block), ледяной блок (ice) и шарик-игрок (be). Базовая ссылка устанавливается на репозиторий с примерами Phaser, что удобно для прототипирования.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('block', 'assets/sprites/crate32.png');
    this.load.image('ice', 'assets/sprites/block-ice.png');
    this.load.image('be', 'assets/sprites/beball1.png');
}

Создание игрока и настройка его физических свойств

В методе create() создается физический спрайт игрока. Ключевые настройки: - setCollideWorldBounds(true) гарантирует, что игрок не выйдет за границы мира. - setPushable(false) делает игрока *непушейбл* — его не смогут сдвинуть другие физические тела при коллизии, даже если у них есть скорость. Это его базовое состояние.

this.player = this.physics.add.sprite(400, 300, 'be');
this.player.setCollideWorldBounds(true);
this.player.setPushable(false);

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

Создаются два типа блоков: обычные и ледяные. Каждый блок — это физический спрайт (this.physics.add.image). Их свойства настраиваются индивидуально: - **Обычные блоки:** Установлен slideFactor в (0, 0), что полностью отключает скольжение при столкновениях. Закомментирован setDrag(1000) — это означало бы очень высокое сопротивление движению. - **Ледяные блоки:** Масштабируются, имеют небольшой setDrag(100) и setBounce(1) (идеальный отскок). Это делает их скользящими и пружинистыми.

const box = this.physics.add.image(x, y, 'block');
box.setCollideWorldBounds(true);
box.body.slideFactor.set(0, 0);

const box = this.physics.add.image(x, y, 'ice').setScale(0.125);
box.setCollideWorldBounds(true);
box.setDrag(100);
box.setBounce(1);

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

Здесь реализуется основная механика. Создается коллайдер между игроком и массивом всех блоков. Четвертым аргументом передается функция обратного вызова processCallback, которая выполняется *перед* каждым расчетом столкновения.

Функция проверяет флаг playerIsNPC. Если он true (игрок в режиме NPC), то для сталкиваемого блока вызывается setPushable(false), делая его неподвижным для игрока. Если false (обычный режим), блок становится толкаемым (setPushable(true)). Возврат true указывает движку, что столкновение между этой конкретной парой объектов должно быть обработано.

this.physics.add.collider(this.player, boxes, null, (player, box) => {
    if (playerIsNPC)
    {
        box.setPushable(false);
    }
    else
    {
        box.setPushable(true);
    }
    return true;
});

Переключение режима игрока по клику

Обработчик события клика мыши инвертирует флаг playerIsNPC. Визуально режим NPC подсвечивается красным оттенком (setTint), а обычный режим — сбрасывает его (clearTint).

this.input.on('pointerdown', () => {
    playerIsNPC = !playerIsNPC;
    if (playerIsNPC)
    {
        this.player.setTint(0xff0000);
    }
    else
    {
        this.player.clearTint();
    }
});

Управление движением игрока

В update() движение игрока привязано к стрелкам клавиатуры. Важно: в каждом кадре скорость сначала обнуляется (setVelocity(0,0)), а затем устанавливается в зависимости от нажатых клавиш. Это дает резкий, отзывчивый контроль без инерции.

update ()
{
    this.player.setVelocity(0, 0);
    if (this.cursors.left.isDown) this.player.setVelocityX(-200);
    else if (this.cursors.right.isDown) this.player.setVelocityX(200);
    if (this.cursors.up.isDown) this.player.setVelocityY(-200);
    else if (this.cursors.down.isDown) this.player.setVelocityY(200);
}

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

Использование setPushable() в коллбэке коллайдера — мощный прием для динамического изменения правил физического взаимодействия в рантайме. Это открывает путь к созданию сложных игровых механик. Для экспериментов попробуйте

  1. Привязать переключение режима не к клику, а к сбору предмета или таймеру
  2. Сделать разные группы объектов, реагирующих на режим игрока по-разному
  3. Комбинировать setPushable с изменением других свойств тела, например setImmovable или массы