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

В сложных физических сценах с множеством объектов не всегда нужно, чтобы всё сталкивалось со всем. Phaser с физическим движком Matter.js предлагает мощный, но не всегда очевидный механизм фильтрации столкновений через группы, категории и маски. Эта статья поможет вам точно контролировать, какие объекты взаимодействуют, а какие игнорируют друг друга — от создания непроходимых стен до реализации слоёв игрового мира, которые существуют параллельно.

Версия 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/block.png');
        this.load.image('strip', 'assets/sprites/strip1.png');
        this.load.spritesheet('fish', 'assets/sprites/fish-136x80.png', { frameWidth: 136, frameHeight: 80 });
    }

    create ()
    {
        this.matter.world.setBounds();

        const canCollide = (filterA, filterB) =>
        {
            if (filterA.group === filterB.group && filterA.group !== 0)
            { return filterA.group > 0; }

            return (filterA.mask & filterB.category) !== 0 && (filterB.mask & filterA.category) !== 0;
        };

        //  Here we'll create Group 1:

        //  This is a colliding group, so objects within this Group will always collide:
        const group1 = this.matter.world.nextGroup();

        const block1 = this.matter.add.image(400, 450, 'strip').setStatic(true).setCollisionGroup(group1);
        const fish1 = this.matter.add.image(100, 100, 'fish', 0).setBounce(1).setFriction(0, 0, 0).setCollisionGroup(group1).setVelocityY(10);

        //  Here we'll create Group 2:
        //  This is a non-colliding group, so objects in this Group never collide:
        const group2 = this.matter.world.nextGroup(true);

        //  block2 won't collide with fish2 because they share the same non-colliding group id
        const block2 = this.matter.add.image(400, 400, 'strip').setStatic(true).setCollisionGroup(group2);
        const fish2 = this.matter.add.image(250, 100, 'fish', 1).setBounce(1).setFriction(0, 0, 0).setCollisionGroup(group2).setVelocityY(10);

        //  however, fish2 WILL collide with block1, as the groups are different and non-zero, so they use the category mask test

        //  by default objects are given a category of 1 and a mask of -1, meaning they will collide (i.e. block1 vs fish2) if in different groups

        //  create a new category (we can have up to 32 of them)
        const cat1 = this.matter.world.nextCategory();

        //  Assign the new category to block3 and fish3 and tell them they should collide:
        const block3 = this.matter.add.image(400, 500, 'strip').setStatic(true).setCollisionCategory(cat1).setCollidesWith(cat1);
        const fish3 = this.matter.add.image(450, 100, 'fish', 2).setBounce(1).setFriction(0, 0, 0).setVelocityY(10).setCollisionCategory(cat1).setCollidesWith(cat1);

        // console.log('block1 vs fish1', canCollide(block1.body.collisionFilter, fish1.body.collisionFilter));
        // console.log('block1 vs fish2', canCollide(block1.body.collisionFilter, fish2.body.collisionFilter));
        // console.log('block1 vs fish3', canCollide(block1.body.collisionFilter, fish3.body.collisionFilter));

        // console.log('block2 vs fish1', canCollide(block2.body.collisionFilter, fish1.body.collisionFilter));
        // console.log('block2 vs fish2', canCollide(block2.body.collisionFilter, fish2.body.collisionFilter));
        // console.log('block2 vs fish3', canCollide(block2.body.collisionFilter, fish3.body.collisionFilter));

        // console.log('block3 vs fish1', canCollide(block3.body.collisionFilter, fish1.body.collisionFilter));
        // console.log('block3 vs fish2', canCollide(block3.body.collisionFilter, fish2.body.collisionFilter));
        // console.log('block3 vs fish3', canCollide(block3.body.collisionFilter, fish3.body.collisionFilter));
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#1b1464',
    parent: 'phaser-example',
    physics: {
        default: 'matter',
        matter: {
            restingThresh: 1,
            debug: {
                renderFill: false
            },
            gravity: {
                y: 0.05
            }
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Базовый принцип: фильтр столкновений

Каждое физическое тело в Matter.js содержит объект collisionFilter. Именно его свойства (group, category, mask) определяют, будет ли происходить столкновение с другим телом. Логика проверки реализована в функции canCollide из примера.

const canCollide = (filterA, filterB) => {
    if (filterA.group === filterB.group && filterA.group !== 0)
    { return filterA.group > 0; }

    return (filterA.mask & filterB.category) !== 0 && (filterB.mask & filterA.category) !== 0;
};

Функция работает в два этапа. Сначала проверяется group: если у двух объектов одинаковая ненулевая группа, они столкнутся только в случае, когда значение группы положительное. Затем, если проверка по группе не прошла, применяется битовая проверка категорий и масок.

Группы столкновений (Collision Groups)

Группа — это целое число. Её главная особенность в том, что она проверяется *до* категорий и масок. Группы создаются с помощью метода this.matter.world.nextGroup(). Значение группы по умолчанию — 0.

// Создаёт группу для ВЗАИМНОГО столкновения объектов
const group1 = this.matter.world.nextGroup();

// Создаёт группу, в которой объекты НЕ сталкиваются друг с другом
const group2 = this.matter.world.nextGroup(true);

Метод nextGroup(isNonColliding) возвращает уникальный идентификатор группы. Если передать true, группа будет считаться "несталкивающейся". Это влияет на знак возвращаемого числа.

Объекту назначается группа через метод .setCollisionGroup(groupId).

const block1 = this.matter.add.image(400, 450, 'strip').setStatic(true).setCollisionGroup(group1);

В примере block1 и fish1 имеют одинаковую положительную группу (group1), поэтому они будут сталкиваться. block2 и fish2 имеют одинаковую отрицательную группу (group2), поэтому они *никогда* не столкнутся друг с другом, но могут столкнуться с объектами из других групп.

Категории и маски (Categories & Masks)

Если группы не определяют взаимодействие (например, они разные или равны 0), в силу вступает битовая система категорий и масок. Это позволяет создавать сложные правила.

* **Категория (category)** — это битовая маска (степень двойки: 1, 2, 4, 8...), которая идентифицирует "тип" объекта. Можно создать до 32 категорий. * **Маска (mask)** — это битовая маска, определяющая, с какими *категориями* этот объект *может* столкнуться.

Создание новой категории и её использование:

// Создаём новую уникальную категорию (например, 2, 4, 8...)
const cat1 = this.matter.world.nextCategory();

// Назначаем объекту эту категорию и говорим, что он сталкивается только с ней же
const block3 = this.matter.add.image(400, 500, 'strip')
    .setStatic(true)
    .setCollisionCategory(cat1)
    .setCollidesWith(cat1);

Метод .setCollidesWith(mask) устанавливает маску столкновений для объекта. В данном случае block3 и fish3 будут сталкиваться только между собой, но проигнорируют все остальные объекты на сцене, потому что их маска (cat1) совпадает только с категорией cat1.

Практические сценарии использования

1. **Слои игрового мира:** Создайте группу для объектов фона (декораций), через которые герой может проходить (nextGroup(true)), и отдельную группу или категорию для стен и платформ. 2. **Игрок и его снаряды:** Назначьте снарядам и игроку одну несталкивающуюся группу, чтобы пули не задевали своего владельца, но при этом имели категорию, которая позволяет им сталкиваться с врагами. 3. **Дамаг только в одну сторону:** В платформере враги могут наносить урон игроку при касании, но игрок не должен отталкиваться от них. Можно сделать врагов статичными (или кинематическими) и настроить их маску так, чтобы они не влияли на физику движения игрока, но при этом отправляли событие столкновения для обработки урона.

Важно помнить порядок приоритета: проверка **Группы** -> проверка **Категорий и Масок**. Если группа определила исход (столкновение или его отсутствие), до категорий дело не дойдёт.

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

Фильтрация столкновений в Matter.js — это ключ к созданию сложной и управляемой физики в вашей игре. Начните экспериментировать: создайте сцену, где определённый тип снарядов проходит сквозь одни препятствия, но разрушает другие. Реализуйте "призрачный" режим для персонажа, который может проходить сквозь стены, но взаимодействует с предметами. Понимание работы group, category и mask откроет вам полный контроль над физическим миром.