О чем этот пример
В сложных физических сценах с множеством объектов не всегда нужно, чтобы всё сталкивалось со всем. 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 откроет вам полный контроль над физическим миром.
