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

Отладка физических взаимодействий — ключевой навык при разработке игр. Этот пример из баг-трекера Phaser наглядно демонстрирует, как комбинации свойств `setPushable` и `setImmovable` влияют на результат столкновения Arcade Physics. Разобравшись в этом, вы сможете точно контролировать поведение объектов: делать одни неподвижными, другие — невосприимчивыми к толчкам, и предсказывать результаты их взаимодействий.

Версия 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.setCircle(32);
        test1B.setCircle(32);

        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.setCircle(32);
        test2B.setCircle(32);

        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.setCircle(32);
        test3B.setCircle(32);

        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.setCircle(32);
        test4B.setCircle(32);

        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.setCircle(32);
        test5B.setCircle(32);

        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.setCircle(32);
        test6B.setCircle(32);

        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.setCircle(32);
        test7B.setCircle(32);

        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.setCircle(32);
        test8B.setCircle(32);

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

Суть примера: исследуем комбинации свойств

В примере создаётся 8 пар спрайтов (A и B) с физикой Arcade. Каждый спрайт настраивается с помощью трёх ключевых методов:

* setPushable(false/true) — определяет, может ли объект быть сдвинут другим физическим телом. По умолчанию true. * setImmovable(true) — делает тело полностью неподвижным при столкновениях. По умолчанию false. * setCircle(32) — задаёт круглую форму коллайдера, что упрощает анализ взаимодействий.

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

// Пример настройки пары спрайтов
test2A.setPushable(false);
test2B.setPushable(false);

// Коллайдер будет добавлен позже
// this.physics.add.collider(test2A, test2B);

Механика запуска и функция-хелпер

Перед добавлением коллайдеров клик по любому спрайту ничего не даст. Это контролируется флагом colliderSet и функцией setVelocity. После выбора направления (клик по 'A to B' или 'B to A') флаг становится true, коллайдеры добавляются, и спрайты реагируют на клики.

let colliderSet = false;

const setVelocity = (body, v) => {
    if (colliderSet) {
        body.setVelocityY(v); // Придаём импульс
    } else {
        this.info.setColor('#ff0000'); // Сообщение краснеет, если коллайдера нет
    }
};

// Обработчик клика по спрайту
test1A.on('pointerdown', () => {
    setVelocity(test1A, -200); // Толкаем вверх
});

Выбор направления 'A to B' или 'B to A' определяет порядок объектов в вызове this.physics.add.collider. В Arcade Physics этот порядок может влиять на расчёт импульсов при определённых комбинациях pushable и immovable.

Анализ ключевых тестовых случаев

Рассмотрим несколько пар, чтобы понять логику.

**Тест 1 (Оба pushable):** Оба спрайта имеют setPushable(true) (значение по умолчанию). При столкновении они ведут себя как обычные физические тела, отталкиваясь друг от друга согласно законам сохранения импульса. Порядок в коллайдере не важен.

// Оба pushable (по умолчанию)
const test1A = this.physics.add.image(80, 500, 'blockAP').setPushable(true);
const test1B = this.physics.add.image(80, 150, 'blockBP').setPushable(true);

**Тест 2 (Оба не-pushable):** Оба спрайта с setPushable(false). Они сталкиваются, но не могут сдвинуть друг друга. Визуально это выглядит как наложение или вибрация на границе контакта, так как тела пытаются реагировать на столкновение, но не могут передать импульс для движения.

**Тест 5 (Pushable vs Immovable):** Спрайт A — pushable, спрайт B — setImmovable(true). Immovable-тело игнорирует любые импульсы от столкновений. При ударе A о B, A отскочит (с учётом setBounce), а B останется неподвижным, как стена. Свойство pushable здесь не играет роли для B, так как immovable имеет более высокий приоритет.

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

Внутри Arcade Physics есть оптимизации и условная логика, которая по-разному обрабатывает пары, где одно из тел immovable или оба !pushable. В некоторых этих специфических случаях (особенно когда ни одно тело не является immovable, но оба или одно !pushable) порядок, в котором тела передаются в коллайдер, может влиять на то, как рассчитывается и применяется сила отталкивания. Этот пример был создан именно для выявления и документирования таких пограничных случаев.

// Разный порядок аргументов
this.physics.add.collider(test1A, test1B); // Порядок A, B
this.physics.add.collider(test1B, test1A); // Порядок B, A

На практике, для большинства игровых ситуаций (стены, платформы, движущиеся враги) достаточно чётко определять, кто является препятствием (immovable: true), а кто — динамическим объектом. Тогда порядок в коллайдере не будет иметь значения.

Практические выводы для разработки

1. **Для статичных препятствий (стены, земля) всегда используйте setImmovable(true).** Это наиболее производительный и предсказуемый способ. 2. **Свойство setPushable(false)** полезно для объектов, которые должны участвовать в столкновениях (например, для правильного отскока), но не должны сдвигаться от удара другими динамическими телами. Пример: тяжёлый ящик, который можно толкать только игроку, но не врагам. 3. **Визуализация — ваш друг.** Включите отладку Arcade Physics (debug: true), чтобы видеть хитбоксы и векторы скорости. Это незаменимо при настройке сложных взаимодействий.

physics: {
    default: 'arcade',
    arcade: {
        gravity: { y: 200 },
        debug: true // Показывает контуры тел
    }
}

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

Этот пример — отличная песочница для понимания тонкостей Arcade Physics. Поэкспериментируйте: попробуйте менять свойства bounce и mass, добавьте гравитацию или сделайте тела прямоугольными с помощью setSize. Попробуйте создать цепную реакцию, где одно !pushable тело, получив импульс от игрока, передаёт его другому такому же телу. Глубокое понимание этих механик позволит вам создавать сложную и предсказуемую физику в ваших играх на Phaser.