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

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

Версия 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('blockA', 'assets/sprites/blockA.png');
        this.load.image('blockB', 'assets/sprites/blockB.png');
        this.load.image('blockC', 'assets/sprites/blockC.png');
    }

    create ()
    {
        const blockA = this.add.sprite(200, 300, 'blockA');
        const blockB = this.add.sprite(400, 300, 'blockB');
        const blockC = this.add.sprite(600, 300, 'blockC');

        const group1 = this.physics.add.group();
        const group2 = this.physics.add.group();

        group1.add(blockA);
        group2.add(blockB);
        group2.add(blockC);

        // Uncomment the lines below to enable the collisions
        // const everything = 2147483647;
        // group1.collisionMask = everything;
        // group2.collisionMask = everything;

        const category1 = this.physics.nextCategory();
        const category2 = this.physics.nextCategory();

        blockA.body.setCollisionCategory(category1);
        blockA.body.addCollidesWith(category2);

        blockB.body.setCollisionCategory(category2);
        blockB.body.addCollidesWith(category1);

        blockC.body.setCollisionCategory(category2);
        blockC.body.addCollidesWith(category1);

        function decimalToBinary (decimal)
        {
            return decimal.toString(2);
        }

        console.log(group1.collisionMask, group2.collisionMask);
        console.log(blockA.body.collisionMask, blockA.body.collisionCategory, decimalToBinary(blockA.body.collisionMask), decimalToBinary(blockB.body.collisionCategory), (blockA.body.collisionMask & blockB.body.collisionCategory) !== 0);
        console.log(blockB.body.collisionMask, blockB.body.collisionCategory, decimalToBinary(blockB.body.collisionMask), decimalToBinary(blockA.body.collisionCategory), (blockB.body.collisionMask & blockA.body.collisionCategory) !== 0);
        console.log(blockC.body.collisionMask, blockC.body.collisionCategory, decimalToBinary(blockC.body.collisionMask), decimalToBinary(blockA.body.collisionCategory), (blockC.body.collisionMask & blockA.body.collisionCategory) !== 0);

        this.physics.add.collider(group1, group2);

        this.input.once('pointerdown', () =>
        {
            blockA.body.setVelocityX(200);
        });
    }
}

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

const game = new Phaser.Game(config);

Суть проблемы: приоритет групповых настроек

В исходном примере создаются две физические группы (group1 и group2). В group1 добавляется спрайт blockA, а в group2 — спрайты blockB и blockC. Для каждого тела спрайта назначаются категории столкновений (collisionCategory) и маски (collisionMask), определяющие, с какими категориями они могут сталкиваться.

Однако, по умолчанию у физических групп значение collisionMask равно 0. Это ключевой момент: **маска столкновений группы имеет приоритет над настройками отдельных тел, входящих в неё**. Если маска группы равна 0, она игнорирует любые столкновения, даже если тела внутри неё настроены на взаимодействие.

const group1 = this.physics.add.group();
const group2 = this.physics.add.group();
// По умолчанию group1.collisionMask = 0, group2.collisionMask = 0

Как работают категории и маски в Arcade Physics

Phaser Arcade Physics использует битовые маски для эффективной проверки столкновений. Каждому телу назначается уникальная категория (степень двойки) и маска, которая определяет, с какими категориями оно может взаимодействовать. Проверка столкновения происходит через побитовое И (`&`) между маской одного тела и категорией другого.

В примере используются this.physics.nextCategory() для генерации уникальных категорий и body.addCollidesWith() для добавления категорий в маску тела.

const category1 = this.physics.nextCategory(); // Например, 1 (0001)
const category2 = this.physics.nextCategory(); // Например, 2 (0010)

blockA.body.setCollisionCategory(category1);
blockA.body.addCollidesWith(category2); // Маска blockA теперь включает category2

Функция decimalToBinary в консольном выводе помогает визуализировать эти битовые значения для отладки.

Решение: включение групповых масок

Чтобы столкновения между группами заработали, необходимо явно задать их маски столкновений. В закомментированном коде примера это делается установкой свойства collisionMask в значение everything (2147483647), что соответствует всем 31 возможной категории в движке.

Раскомментирование этих строк активирует проверку столкновений на групповом уровне.

// Раскомментируйте эти строки для включения столкновений:
const everything = 2147483647;
group1.collisionMask = everything;
group2.collisionMask = everything;

После этой настройки группа будет учитывать столкновения для своих тел согласно их индивидуальным маскам. Без этого группы остаются «слепы» друг к другу.

Практическое создание коллайдера

Даже с правильно настроенными масками групп для непосредственного запуска физики столкновений необходимо создать коллайдер. В примере это делает строка this.physics.add.collider(group1, group2). Этот метод регистрирует пару групп для проверки столкновений между всеми их членами.

this.physics.add.collider(group1, group2);

Коллайдер работает в паре с настройками категорий и масок. Он выполняет проверку, но окончательное решение о столкновении принимается на основе битовых масок каждого конкретного тела и его группы.

Анализ консольного вывода для отладки

Пример содержит подробный вывод в консоль, который является отличным инструментом для отладки сложных сценариев столкновений. Он показывает числовые значения и их бинарные представления для масок и категорий каждого тела, а также результат побитовой проверки (`&`).

console.log(blockA.body.collisionMask, blockA.body.collisionCategory, decimalToBinary(blockA.body.collisionMask), decimalToBinary(blockB.body.collisionCategory), (blockA.body.collisionMask & blockB.body.collisionCategory) !== 0);

**Как читать вывод:** Последний столбец (результат (...) !== 0) показывает true, если маска тела A допускает столкновение с категорией тела B. Если здесь true, но столкновения не происходит — следующее место для проверки это group.collisionMask.

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

Главный вывод: при работе с physics.add.group() всегда проверяйте свойство collisionMask группы. Оно должно быть установлено в значение, которое включает категории тех объектов, с которыми вы планируете сталкиваться. Для отладки используйте консольный вывод, как в примере, чтобы убедиться, что битовые проверки на уровне тел проходят успешно. **Идеи для экспериментов:** 1. Создайте третью группу и настройте избирательные столкновения только между ней и одной из существующих. 2. Попробуйте динамически менять group.collisionMask во время выполнения игры, чтобы включать или выключать целые пласты взаимодействий (например, когда игрок надевает "призрачный" предмет). 3. Используйте не все категории (everything), а вычисляемую маску, чтобы группа реагировала только на определённый набор категорий, например, group1.collisionMask = category2 | category3.