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

В разработке игр на Phaser часто требуется контролировать, как объекты взаимодействуют при столкновениях. Должен ли персонаж сдвигать ящики? Может ли пуля сдвинуть врага? Пример из официальной документации наглядно демонстрирует разницу между двумя ключевыми свойствами физических тел: `setPushable()` и `setImmovable()`. Эта статья поможет вам понять тонкости их работы и избежать неожиданного поведения в ваших играх.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    info;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.setPath('assets/sprites');

        this.load.image('blockAP');
        this.load.image('blockBP');
        this.load.image('blockANP');
        this.load.image('blockBNP');
        this.load.image('blockANM');
        this.load.image('blockBNM');
    }

    create ()
    {
        let colliderSet = false;

        const setVelocity = (body, v) =>
        {
            if (colliderSet)
            {
                body.setVelocityY(v);
            }
            else
            {
                this.info.setColor('#ff0000');
            }
        };

        this.physics.world.setBounds(0, 0, 800 + 256, 600);

        //  Test 1

        const test1A = this.physics.add.image(80, 500, 'blockAP').setCollideWorldBounds().setInteractive();
        const test1B = this.physics.add.image(80, 150, 'blockBP').setCollideWorldBounds().setInteractive();

        test1A.setBounce(0.5);
        test1B.setBounce(0.5);

        test1A.on('pointerdown', () =>
        {
            setVelocity(test1A, -200);
        });

        test1B.on('pointerdown', () =>
        {
            setVelocity(test1B, 200);
        });

        //  Test 2

        const test2A = this.physics.add.image(208, 500, 'blockANP').setCollideWorldBounds().setInteractive();
        const test2B = this.physics.add.image(208, 150, 'blockBNP').setCollideWorldBounds().setInteractive();

        test2A.setBounce(0.5);
        test2B.setBounce(0.5);
        test2A.setPushable(false);
        test2B.setPushable(false);

        test2A.on('pointerdown', () =>
        {
            setVelocity(test2A, -200);
        });

        test2B.on('pointerdown', () =>
        {
            setVelocity(test2B, 200);
        });

        //  Test 3

        const test3A = this.physics.add.image(336, 500, 'blockAP').setCollideWorldBounds().setInteractive();
        const test3B = this.physics.add.image(336, 150, 'blockBNP').setCollideWorldBounds().setInteractive();

        test3A.setBounce(0.5);
        test3B.setBounce(0.5);
        test3B.setPushable(false);

        test3A.on('pointerdown', () =>
        {
            setVelocity(test3A, -200);
        });

        test3B.on('pointerdown', () =>
        {
            setVelocity(test3B, 200);
        });

        //  Test 4

        const test4A = this.physics.add.image(464, 500, 'blockANP').setCollideWorldBounds().setInteractive();
        const test4B = this.physics.add.image(464, 150, 'blockBP').setCollideWorldBounds().setInteractive();

        test4A.setBounce(0.5);
        test4B.setBounce(0.5);
        test4A.setPushable(false);

        test4A.on('pointerdown', () =>
        {
            setVelocity(test4A, -200);
        });

        test4B.on('pointerdown', () =>
        {
            setVelocity(test4B, 200);
        });

        //  Test 5

        const test5A = this.physics.add.image(592, 500, 'blockAP').setCollideWorldBounds().setInteractive();
        const test5B = this.physics.add.image(592, 150, 'blockBNM').setCollideWorldBounds().setInteractive();

        test5A.setBounce(0.5);
        test5B.setBounce(0.5);
        test5B.setImmovable(true);

        test5A.on('pointerdown', () =>
        {
            setVelocity(test5A, -200);
        });

        test5B.on('pointerdown', () =>
        {
            setVelocity(test5B, 200);
        });

        //  Test 6

        const test6A = this.physics.add.image(720, 500, 'blockANP').setCollideWorldBounds().setInteractive();
        const test6B = this.physics.add.image(720, 150, 'blockBNM').setCollideWorldBounds().setInteractive();

        test6A.setBounce(0.5);
        test6B.setBounce(0.5);
        test6A.setPushable(false);
        test6B.setImmovable(true);

        test6A.on('pointerdown', () =>
        {
            setVelocity(test6A, -200);
        });

        test6B.on('pointerdown', () =>
        {
            setVelocity(test6B, 200);
        });

        //  Test 7

        const test7A = this.physics.add.image(848, 500, 'blockANM').setCollideWorldBounds().setInteractive();
        const test7B = this.physics.add.image(848, 150, 'blockBP').setCollideWorldBounds().setInteractive();

        test7A.setBounce(0.5);
        test7B.setBounce(0.5);
        test7A.setImmovable(true);
        test7B.setPushable(true);

        test7A.on('pointerdown', () =>
        {
            setVelocity(test7A, -200);
        });

        test7B.on('pointerdown', () =>
        {
            setVelocity(test7B, 200);
        });

        //  Test 8

        const test8A = this.physics.add.image(976, 500, 'blockANM').setCollideWorldBounds().setInteractive();
        const test8B = this.physics.add.image(976, 150, 'blockBNP').setCollideWorldBounds().setInteractive();

        test8A.setBounce(0.5);
        test8B.setBounce(0.5);
        test8A.setImmovable(true);
        test8B.setPushable(false);

        test8A.on('pointerdown', () =>
        {
            setVelocity(test8A, -200);
        });

        test8B.on('pointerdown', () =>
        {
            setVelocity(test8B, 200);
        });

        //  Runner

        this.info = this.add.text(16, 16, 'Click the collider first:');

        const atob = this.add.text(280, 16, 'A to B').setInteractive();
        const btoa = this.add.text(400, 16, 'B to A').setInteractive();

        atob.once('pointerdown', () =>
        {

            atob.setColor('#ffff00');
            btoa.setAlpha(0.2);
            colliderSet = true;

            this.physics.add.collider(test1A, test1B);
            this.physics.add.collider(test2A, test2B);
            this.physics.add.collider(test3A, test3B);
            this.physics.add.collider(test4A, test4B);
            this.physics.add.collider(test5A, test5B);
            this.physics.add.collider(test6A, test6B);
            this.physics.add.collider(test7A, test7B);
            this.physics.add.collider(test8A, test8B);

        });

        btoa.once('pointerdown', () =>
        {

            btoa.setColor('#ffff00');
            atob.setAlpha(0.2);
            colliderSet = true;

            this.physics.add.collider(test1B, test1A);
            this.physics.add.collider(test2B, test2A);
            this.physics.add.collider(test3B, test3A);
            this.physics.add.collider(test4B, test4A);
            this.physics.add.collider(test5B, test5A);
            this.physics.add.collider(test6B, test6A);
            this.physics.add.collider(test7B, test7A);
            this.physics.add.collider(test8B, test8A);

        });
    }
}

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

const game = new Phaser.Game(config);

Разбираем пример: структура и настройка

Пример представляет собой тестовый стенд из восьми пар спрайтов, расположенных горизонтально. Каждая пара проверяет разные комбинации свойств pushable и immovable. Все спрайты создаются как физические тела с помощью this.physics.add.image() и настраиваются одинаково: они отскакивают от границ мира и реагируют на клики мыши.

const test1A = this.physics.add.image(80, 500, 'blockAP').setCollideWorldBounds().setInteractive();
const test1B = this.physics.add.image(80, 150, 'blockBP').setCollideWorldBounds().setInteractive();

test1A.setBounce(0.5);
test1B.setBounce(0.5);

Особенность примера — интерактивный выбор направления коллизии. Перед тестом нужно кликнуть на текстовую кнопку 'A to B' или 'B to A', чтобы установить коллайдеры. Это позволяет увидеть, как свойства тел влияют на взаимодействие в зависимости от того, какой объект является первым аргументом в collider.

this.physics.add.collider(test1A, test1B); // Коллизия от A к B

Что такое Pushable? Контроль над 'толкаемостью'

Свойство pushable управляет тем, может ли тело быть сдвинуто другим телом при столкновении. По умолчанию все тела в Arcade Physics являются 'pushable' (толкаемыми). Однако, если вызвать setPushable(false), тело становится неподвижным для толчков со стороны других тел. Важно: это не делает его полностью статичным — его всё ещё можно двигать, задавая скорость напрямую или через другие силы.

test2A.setPushable(false);
test2B.setPushable(false);

В примере пары 2, 3, 4, 6 и 8 используют это свойство. Если pushable равно false, тело ведёт себя как неподвижная стена для других тел, которые пытаются его сдвинуть в результате физического взаимодействия (но не в результате прямого управления).

Что такое Immovable? Абсолютная неподвижность

Свойство immovable — более сильное. Когда тело помечается как immovable (неподвижное), оно полностью игнорирует воздействие импульсов и сил от столкновений с другими телами. Его можно переместить только напрямую, изменив его координаты или скорость. Это идеально подходит для платформ, стен или крупных неподвижных препятствий.

test5B.setImmovable(true);

В примере это свойство используется в парах 5, 6, 7 и 8. Обратите внимание, что immovable переопределяет pushable. Если тело неподвижное (immovable: true), то оно автоматически не может быть сдвинуто, независимо от значения pushable.

Ключевое отличие: Pushable vs Immovable

Основная разница в том, *как* тело сопротивляется движению. * **pushable: false** — тело не получает импульс от столкновения. Оно не сдвинется, если в него врежется другое тело. Однако, если два таких тела (pushable: false) столкнутся друг с другом, они оба могут вести себя неожиданно, так как ни одно не хочет уступать. Их взаимодействие рассчитывается системой физики и может зависеть от масс и скоростей. * **immovable: true** — тело полностью исключается из расчётов импульсов при столкновении. Оно действует как бесконечно массивный объект. Все силы от столкновения применяются исключительно к *другому* телу, что делает его предсказуемым выбором для статичных препятствий.

Пример наглядно показывает это в тестах 5-8, где комбинируются оба свойства.

Почему важен порядок в collider?

В Arcade Physics при вызове this.physics.add.collider(body1, body2) система проверяет свойства pushable и immovable в контексте этого конкретного столкновения. Порядок аргументов может влиять на логику расчётов, особенно когда оба тела имеют pushable: false. Пример позволяет протестировать это, выбрав направление 'A to B' или 'B to A'.

// При collider(A, B) система сначала проверяет свойства A.
this.physics.add.collider(test1A, test1B);
// При collider(B, A) система сначала проверяет свойства B.
this.physics.add.collider(test1B, test1A);

Это демонстрирует, что проектирование физических взаимодействий должно быть осознанным. Для полной предсказуемости, особенно в сложных сценах, лучше явно задавать immovable для статичных объектов.

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

Свойства setPushable() и setImmovable() — мощные инструменты для тонкой настройки физики в Phaser. Используйте pushable: false для объектов, которые не должны сдвигаться от толчков, но могут двигаться сами (например, управляемый танк). Используйте immovable: true для абсолютно статичных объектов уровня, таких как стены и платформы. Для экспериментов попробуйте создать сцену, где игрок (pushable: false) может толкать ящики, но не может сдвинуть массивную статую (immovable: true), и посмотрите, как меняется поведение при изменении порядка аргументов в коллайдерах.