Фильтрация столкновений: category и mask

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


Основные понятия

Каждое тело в Matter.js имеет объект collisionFilter, содержащий три поля:

  • category – уникальная категория, которой принадлежит тело. Представляется как битовая маска (значение типа uint16), обычно в виде степени двойки:

    body.collisionFilter.category = 0x0001; // категория 1
  • mask – маска, определяющая, с какими категориями тело может сталкиваться. Тело сталкивается только с объектами, категория которых присутствует в его маске:

    body.collisionFilter.mask = 0x0002 | 0x0004; // столкновения с категориями 2 и 3
  • group – позволяет создавать группы столкновений с более сложной логикой (необязательное поле).

Фильтрация работает на битовом уровне: проверяется логическое И между category одного тела и mask другого, и наоборот. Только если обе проверки возвращают истину, происходит столкновение.


Создание категорий

Для удобства обычно создают константы:

const CATEGORY_PLAYER = 0x0001;
const CATEGORY_ENEMY = 0x0002;
const CATEGORY_BULLET = 0x0004;
const CATEGORY_WALL = 0x0008;

Каждой категории присваивается уникальная степень двойки. Это обеспечивает возможность комбинировать их через побитовое ИЛИ (|) для масок.


Настройка столкновений

Пример конфигурации игрока, врагов и пуль:

// Игрок: сталкивается с врагами и стенами
player.body.collisionFilter.category = CATEGORY_PLAYER;
player.body.collisionFilter.mask = CATEGORY_ENEMY | CATEGORY_WALL;

// Враг: сталкивается с игроком и стенами, но не с пулями других врагов
enemy.body.collisionFilter.category = CATEGORY_ENEMY;
enemy.body.collisionFilter.mask = CATEGORY_PLAYER | CATEGORY_WALL;

// Пуля игрока: сталкивается только с врагами и стенами
bullet.body.collisionFilter.category = CATEGORY_BULLET;
bullet.body.collisionFilter.mask = CATEGORY_ENEMY | CATEGORY_WALL;

В данном примере пули не сталкиваются друг с другом, поскольку их категория CATEGORY_BULLET отсутствует в маске другой пули.


Маски и комбинации

Маски позволяют создавать сложные правила столкновений:

  1. Игровые зоны с исключениями: например, враги не могут пересекать область, где находятся только союзники.
  2. Слои окружения: объекты, с которыми взаимодействуют только определённые персонажи.
  3. Проектильная фильтрация: разные типы снарядов могут игнорировать друг друга.

Для добавления нескольких категорий используется побитовое ИЛИ:

body.collisionFilter.mask = CATEGORY_PLAYER | CATEGORY_ENEMY | CATEGORY_WALL;

Для исключения категории используют побитовое НЕ и побитовое И:

body.collisionFilter.mask = ~CATEGORY_BULLET; // сталкивается со всеми, кроме пуль

Работа с группами (опционально)

Поле group позволяет объединять объекты в группы с более детальной фильтрацией:

  • Положительные значения: тела внутри группы не сталкиваются друг с другом.
  • Отрицательные значения: тела внутри группы всегда сталкиваются друг с другом, игнорируя маски.

Пример:

player.body.collisionFilter.group = 1; // Игрок и союзники не сталкиваются между собой
ally.body.collisionFilter.group = 1;
enemy.body.collisionFilter.group = -1; // Враги всегда сталкиваются друг с другом

Группы часто применяются для управления союзниками, стаями NPC или частицами.


Практические рекомендации

  1. Всегда использовать степени двойки для category и масок, чтобы фильтрация работала корректно.

  2. Минимизировать количество категорий — слишком большое число категорий и масок может усложнить отладку.

  3. Разделять объекты по логике столкновений — игровые элементы, проектiles, окружение.

  4. Проверять маски через битовую арифметику при отладке:

    console.log((bodyA.collisionFilter.category & bodyB.collisionFilter.mask) !== 0 &&
                (bodyB.collisionFilter.category & bodyA.collisionFilter.mask) !== 0);

Примеры сложной фильтрации

  1. Игрок может проходить сквозь объекты типа ghost, но сталкивается с стенами:
const CATEGORY_GHOST = 0x0010;
ghost.body.collisionFilter.category = CATEGORY_GHOST;
ghost.body.collisionFilter.mask = 0x0000; // не сталкивается ни с кем
player.body.collisionFilter.mask |= CATEGORY_WALL; // игрок сталкивается со стенами
  1. Множество типов снарядов с разной логикой:
const CATEGORY_BULLET_PLAYER = 0x0004;
const CATEGORY_BULLET_ENEMY = 0x0010;

bulletPlayer.body.collisionFilter.category = CATEGORY_BULLET_PLAYER;
bulletPlayer.body.collisionFilter.mask = CATEGORY_ENEMY | CATEGORY_WALL;

bulletEnemy.body.collisionFilter.category = CATEGORY_BULLET_ENEMY;
bulletEnemy.body.collisionFilter.mask = CATEGORY_PLAYER | CATEGORY_WALL;

Фильтрация столкновений через category и mask позволяет создавать гибкую и эффективную систему взаимодействий между объектами. Это особенно важно для больших проектов с множеством игровых сущностей, где классический метод проверки столкновений «каждый со всеми» становится неоптимальным.