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

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

Версия 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.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 ()
    {
        this.physics.world.setBounds(0, 0, 864, 632);

        //  Test 1

        const test1A = this.physics.add.image(200, 100 - 16, 'blockAP').setCollideWorldBounds().setInteractive();
        const test1B = this.physics.add.image(600, 100 - 16, 'blockBP').setCollideWorldBounds().setInteractive();

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

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

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

        //  Test 2

        const test2A = this.physics.add.image(200, 196 - 16, 'blockANP').setCollideWorldBounds().setInteractive();
        const test2B = this.physics.add.image(600, 196 - 16, 'blockBNP').setCollideWorldBounds().setInteractive();

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

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

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

        //  Test 3

        const test3A = this.physics.add.image(200, 292 - 16, 'blockAP').setCollideWorldBounds().setInteractive();
        const test3B = this.physics.add.image(600, 292 - 16, 'blockBNP').setCollideWorldBounds().setInteractive();

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

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

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

        //  Test 4

        const test4A = this.physics.add.image(200, 388 - 16, 'blockANP').setCollideWorldBounds().setInteractive();
        const test4B = this.physics.add.image(600, 388 - 16, 'blockBP').setCollideWorldBounds().setInteractive();

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

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

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

        //  Test 5

        const test5A = this.physics.add.image(200, 484 - 16, 'blockAP').setCollideWorldBounds().setInteractive();
        const test5B = this.physics.add.image(600, 484 - 16, 'blockBNM').setCollideWorldBounds().setInteractive();

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

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

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

        //  Test 6

        const test6A = this.physics.add.image(200, 580 - 16, 'blockANP').setCollideWorldBounds().setInteractive();
        const test6B = this.physics.add.image(600, 580 - 16, 'blockBNM').setCollideWorldBounds().setInteractive();

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

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

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

        //  Test 7

        // var test7A = this.physics.add.image(848, 400, 'blockANM').setCollideWorldBounds().setInteractive();
        // var test7B = this.physics.add.image(848, 200, 'blockBP').setCollideWorldBounds().setInteractive();

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

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

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

        //  Test 8

        // var test8A = this.physics.add.image(976, 400, 'blockANM').setCollideWorldBounds().setInteractive();
        // var test8B = this.physics.add.image(976, 200, 'blockBNP').setCollideWorldBounds().setInteractive();

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

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

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

        //  Runner

        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);

            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);

            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: 864,
    height: 632,
    parent: 'phaser-example',
    backgroundColor: '#2d2d2d',
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 0 },
            debug: true
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

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

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

this.physics.world.setBounds(0, 0, 864, 632);
const test1A = this.physics.add.image(200, 100 - 16, 'blockAP').setCollideWorldBounds().setInteractive();

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

Тест 1: Базовая физика (оба pushable)

Это контрольная группа. Оба спрайта, A и B, созданы с настройками по умолчанию. В Arcade Physics все тела по умолчанию являются "толкаемыми" (pushable: true). Это означает, что при столкновении они оба могут сдвигаться с места, передавая друг другу импульс.

// test1A и test1B созданы без изменения pushable
this.physics.add.collider(test1A, test1B);

Когда вы кликаете по кнопке "A to B" или "B to A" и затем запускаете спрайты, они столкнутся и отскочат, оба изменив свою траекторию. Это стандартное поведение для двух динамических объектов.

Тест 2: Отключение толкания (оба НЕ pushable)

Здесь у обоих спрайтов свойство pushable явно установлено в false. Это ключевое изменение.

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

Несмотря на то что оба тела остаются динамическими (на них действует скорость, отскок), они **не могут сдвинуть друг друга при столкновении**. Визуально это выглядит так, будто они сталкиваются, но не передают импульс, а словно "соскальзывают" или остаются на месте, сохраняя свою скорость. Это полезно для объектов, которые должны игнорировать взаимное толкание, например, частицы или призрачные сущности.

Тесты 3 и 4: Асимметричные взаимодействия

В этих тестах комбинируются pushable и !pushable объекты. Например, в Тесте 3 объект A толкаемый, а объект B — нет.

// Тест 3
test3B.setPushable(false);
// Тест 4
test4A.setPushable(false);

Результат зависит от того, кто является "первым" (активным) телом в коллайдере (это определяется выбором кнопки "A to B" или "B to A"). Если активное тело не толкаемое (pushable: false), оно не сдвинет пассивное, даже если пассивное тело по умолчанию толкаемое. Это создаёт интересные односторонние взаимодействия, например, когда игрок (pushable: true) может толкнуть ящик, но сам ящик не может сдвинуть игрока таким же образом.

Тесты 5 и 6: Неподвижные тела (Immovable)

Свойство immovable — это другой, более мощный инструмент. Если тело помечено как immovable: true, оно полностью игнорирует импульсы от столкновений и остаётся на месте, как стена или платформа.

test5B.setImmovable(true);
// В Тесте 6 комбинация
test6A.setPushable(false);
test6B.setImmovable(true);

В Тесте 5 спрайт B неподвижен. Когда A (pushable: true) сталкивается с ним, A отскакивает, а B не шелохнётся. В Тесте 6 объект A ещё и не толкаемый. Важно: immovable имеет приоритет над любыми настройками pushable. Неподвижное тело никогда не сдвинется от столкновения.

Как запустить эксперимент: роль коллайдера

Столкновения в примере регистрируются не сразу, а по клику на одну из кнопок-текстов "A to B" или "B to A". Это сделано для демонстрации важного нюанса: **порядок аргументов в коллайдере имеет значение**.

// Вариант "A to B" - A считается первым (активным) телом
this.physics.add.collider(test1A, test1B);
// Вариант "B to A" - B считается первым (активным) телом
this.physics.add.collider(test1B, test1A);

Активное тело (первый аргумент) в некоторых типах взаимодействий (особенно с pushable: false) может влиять на результат. Кликая на разные кнопки, вы меняете этот порядок и можете наблюдать разное поведение в асимметричных тестах (3, 4).

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

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