О чем этот пример
В играх часто нужно, чтобы одни объекты сталкивались друг с другом, а другие — нет. Например, снаряды союзников не должны наносить урон своим, а враги определённых типов должны взаимодействовать только с определёнными защитными сооружениями. Phaser 3 предлагает элегантное решение — систему категорий столкновений на основе битовых масок. Эта статья покажет, как настроить сложные правила столкновений, легко управлять ими во время выполнения игры и избежать нежелательных взаимодействий между объектами.
Версия 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('mine', 'assets/sprites/mine.png');
this.load.image('arrow', 'assets/sprites/arrow.png');
}
create ()
{
this.physics.world.setBounds(0, 0, 800, 600);
// --- 1) Define Categories (bitmask values) ---
this.categories = {
playerMelee: 1, // 2^0 -> 0b0001
playerRanged: 2, // 2^1 -> 0b0010
enemyMelee: 4, // 2^2 -> 0b0100
enemyRanged: 8 // 2^3 -> 0b1000
};
// --- 2) Create Four Groups ---
this.playerMeleeGroup = this.physics.add.group();
this.playerRangedGroup = this.physics.add.group();
this.playerMeleeGroup.setCollisionCategory(this.categories.playerMelee);
this.playerMeleeGroup.setCollidesWith([ this.categories.playerMelee, this.categories.playerRanged ]);
this.playerRangedGroup.setCollisionCategory(this.categories.playerRanged);
this.playerRangedGroup.setCollidesWith([ this.categories.playerMelee, this.categories.playerRanged ]);
// --- 3) Add Some Containers, One for Each Type ---
// Player Melee
this.playerMelee1 = this.createUnit(
100, 300, 0x00ff00, "playerMelee1",
this.categories.playerMelee,
[ this.categories.playerMelee, this.categories.playerRanged ],
this.playerMeleeGroup
);
// Player Ranged
this.playerRanged1 = this.createUnit(
200, 300, 0x008800, "playerRanged1",
this.categories.playerRanged,
[ this.categories.playerMelee, this.categories.playerRanged ],
this.playerRangedGroup
);
// -- Player Melee <-> Player Ranged (friendly collisions) --
this.physics.add.collider(
this.playerMeleeGroup,
this.playerRangedGroup,
this.handleFriendlyCollision,
undefined,
this
);
// --- 5) DEMO: Remove Collision After 6s ---
this.time.addEvent({
delay: 6000,
callback: () =>
{
// e.g. Player Ranged no longer collides with player Melee
console.log('remove ranged collision from melee');
this.playerRanged1.body.removeCollidesWith(this.categories.playerMelee);
// console.log('remove player Melee');
// this.playerMelee1.body.removeCollidesWith(this.categories.playerRanged);
},
callbackScope: this
});
// --- 6) Drag Behavior (optional) ---
// Make containers draggable
this.input.setDraggable([
this.playerMelee1,
this.playerRanged1,
]);
this.input.on('dragstart', (pointer, obj) =>
{
console.log("dragstart on", obj.name);
// Stop velocity if you don't want it fighting the drag
obj.body.setVelocity(0, 0);
});
this.input.on('drag', (pointer, obj, dragX, dragY) =>
{
obj.setPosition(dragX, dragY);
});
this.input.on('dragend', (pointer, obj) =>
{
console.log("dragend on", obj.name);
// Re-enable velocity if desired
// obj.body.setVelocityX(50);
});
// this.removeCollisions();
}
removeCollisions ()
{
console.log('remove player Ranged');
this.playerRanged1.body.removeCollidesWith(this.categories.playerMelee);
console.log('remove player Melee');
this.playerMelee1.body.removeCollidesWith(this.categories.playerRanged);
}
// --- Create a container with a rectangle, apply physics, etc. ---
createUnit (x, y, color, name, collisionCat, collidesWithCats, group, velocityX = 50)
{
const container = this.add.container(x, y);
container.setName(name);
container.setSize(50, 50);
// Make an internal rectangle to visualize it
const rect = this.add.rectangle(0, 0, 50, 50, color);
container.add(rect);
// check if `name` includes the string 'melee'
// if so, add a mine sprite to the container
const sprite = this.add.image(0, 0, name.includes('Melee') ? 'mine' : 'arrow');
container.add(sprite);
// For input + dragging
container.setSize(50, 50);
container.setInteractive();
// Add Arcade Physics
this.physics.world.enable(container);
container.body.setCollideWorldBounds(true);
container.body.setVelocityX(velocityX);
// Assign categories
container.body.setCollisionCategory(collisionCat);
container.body.setCollidesWith(collidesWithCats);
// Add to the correct group
group.add(container);
return container;
}
// --- Collision Callbacks ---
handleFriendlyCollision (go1, go2)
{
console.log("Friendly collision between:", go1.name, "and", go2.name);
}
handleOpponentCollision (go1, go2)
{
console.log("Opponent collision between:", go1.name, "and", go2.name);
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
pixelArt: true,
physics: {
default: "arcade",
arcade: {
gravity: { y: 0 },
debug: true
}
},
scene: Example
};
const game = new Phaser.Game(config);
Что такое битовые маски и категории?
В Arcade Physics Phaser каждый физический объект (body) имеет два ключевых свойства для управления столкновениями:
* collisionCategory — уникальная категория объекта, представленная степенью двойки (битовым флагом).
* collidesWith — битовая маска, определяющая, с какими другими категориями этот объект будет сталкиваться.
Столкновение происходит, если бит категории одного объекта присутствует в маске collidesWith другого, и наоборот. Это позволяет создавать сложные правила, например, "снаряд игрока сталкивается только с врагами, но не с другими игроками".
В примере категории определены как степени двойки:
this.categories = {
playerMelee: 1, // 2^0 -> бит 0 (0b0001)
playerRanged: 2, // 2^1 -> бит 1 (0b0010)
enemyMelee: 4, // 2^2 -> бит 2 (0b0100)
enemyRanged: 8 // 2^3 -> бит 3 (0b1000)
};
Настройка групп и правил столкновений
Логично настраивать категории для групп объектов. В примере создаются две группы для ближнего и дальнего боя игрока. Метод setCollisionCategory() назначает группе её битовую категорию.
Ключевой метод setCollidesWith() принимает массив числовых категорий или одну категорию. Phaser сам объединит их в итоговую битовую маску.
this.playerMeleeGroup = this.physics.add.group();
this.playerMeleeGroup.setCollisionCategory(this.categories.playerMelee);
this.playerMeleeGroup.setCollidesWith([ this.categories.playerMelee, this.categories.playerRanged ]);
Это означает: все объекты в группе playerMeleeGroup принадлежат категории playerMelee и будут сталкиваться с объектами категорий playerMelee И playerRanged. Так можно легко смоделировать "дружественные" столкновения между юнитами одного игрока.
Создание объектов и применение категорий
Вспомогательная функция createUnit создаёт контейнер с физическим телом, назначает ему категорию и маску столкновений, а затем добавляет в соответствующую группу.
Обратите внимание: категория и маска назначаются непосредственно телу объекта (body). Это даёт гибкость — объекты внутри одной группы могут иметь слегка различающиеся правила.
container.body.setCollisionCategory(collisionCat);
container.body.setCollidesWith(collidesWithCats);
group.add(container);
Физический коллайдер this.physics.add.collider настраивается между группами. При срабатывании коллизии вызывается функция handleFriendlyCollision, которая просто логирует событие. Здесь можно добавить игровую логику: нанесение урона, отталкивание, звуковые эффекты.
Динамическое изменение правил во время игры
Одна из самых мощных возможностей системы — изменение масок столкновений на лету. В примере через 6 секунд после старта вызывается метод removeCollidesWith() для тела одного из объектов.
this.time.addEvent({
delay: 6000,
callback: () => {
// Убираем столкновение между playerRanged1 и всеми объектами категории playerMelee
this.playerRanged1.body.removeCollidesWith(this.categories.playerMelee);
},
callbackScope: this
});
Это мгновенно изменяет поведение: снаряд дальнего боя перестаёт сталкиваться с юнитами ближнего боя, "проходя" сквозь них. Таким образом можно реализовать временные эффекты (например, "сквозные" снаряды), смену фаз боя или разрушение логических связей между объектами.
Практические советы и отладка
1. **Порядок степеней двойки:** Используйте 1, 2, 4, 8, 16, 32... Максимум 32 категории (так как маска — 32-битное число).
2. **Отладка:** Включите debug: true в настройках физики. Тела объектов будут подсвечиваться разными цветами. Цвет рамки показывает категорию объекта.
3. **Гибридный подход:** Можно назначать категории как группам, так и отдельным объектам. Правила объекта имеют приоритет над правилами группы.
4. **Проверка масок:** Метод body.collidesWith содержит итоговую числовую маску. Для проверки, сталкивается ли объект с категорией `X`, используйте побитовую операцию:
if (body.collidesWith & categoryX) {
// Столкновение с категорией X возможно
}
Что попробовать дальше
Система категорий столкновений Phaser 3 — это эффективный и производительный инструмент для создания сложной игровой физики. Она избавляет от необходимости писать множество проверок вручную и позволяет управлять взаимодействиями на высоком уровне абстракции. **Идеи для экспериментов:** 1. Реализуйте снаряды, которые проходят сквозь врагов первого типа, но наносят урон второму. 2. Создайте "призрачный" режим для игрока, временно отключающий все его столкновения. 3. Смоделируйте щит, который блокирует только снаряды дальнего боя. 4. Динамически меняйте категорию объекта (например, после подбора бонуса "вражеский юнит переходит на вашу сторону").
