О чем этот пример
При создании 2D-игр часто возникает задача детектировать столкновения не по всей площади спрайта, а только в определённых его частях. Например, нужно определить, какой именно стороной персонаж коснулся платформы. Phaser в связке с физическим движком Matter.js позволяет решать такие задачи элегантно с помощью составных тел (Compound Bodies) и сенсоров (Sensors). Эта статья покажет, как создать объект, состоящий из центрального физического тела и четырёх сенсоров по сторонам света. Каждый сенсор будет помечен и реагировать на столкновения независимо, окрашивая статичные блоки в свой цвет. Этот подход полезен для создания продвинутых механик взаимодействия, хитбоксов для ударов или зон взятия предметов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
cursors;
player;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('block', 'assets/sprites/block.png');
this.load.image('platform', 'assets/sprites/platform.png');
}
create ()
{
const Bodies = Phaser.Physics.Matter.Matter.Bodies;
const rect = Bodies.rectangle(0, 0, 98, 98);
const circleA = Bodies.circle(-70, 0, 24, { isSensor: true, label: 'left' });
const circleB = Bodies.circle(70, 0, 24, { isSensor: true, label: 'right' });
const circleC = Bodies.circle(0, -70, 24, { isSensor: true, label: 'top' });
const circleD = Bodies.circle(0, 70, 24, { isSensor: true, label: 'bottom' });
const compoundBody = Phaser.Physics.Matter.Matter.Body.create({
parts: [ rect, circleA, circleB, circleC, circleD ],
inertia: Infinity
});
this.player = this.matter.add.image(0, 0, 'block');
this.player.setExistingBody(compoundBody);
this.player.setPosition(100, 300);
const testA = this.matter.add.image(400, 150, 'block').setStatic(true);
const testB = this.matter.add.image(600, 450, 'block').setStatic(true);
const testC = this.matter.add.image(200, 550, 'block').setStatic(true);
this.matter.world.on('collisionstart', event =>
{
// Loop through all of the collision pairs
const pairs = event.pairs;
for (let i = 0; i < pairs.length; i++)
{
const bodyA = pairs[i].bodyA;
const bodyB = pairs[i].bodyB;
// We only want sensor collisions
if (pairs[i].isSensor)
{
let blockBody;
let playerBody;
if (bodyA.isSensor)
{
blockBody = bodyB;
playerBody = bodyA;
}
else if (bodyB.isSensor)
{
blockBody = bodyA;
playerBody = bodyB;
}
// You can get to the Sprite via `gameObject` property
const playerSprite = playerBody.gameObject;
const blockSprite = blockBody.gameObject;
let color;
if (playerBody.label === 'left')
{
color = 0xff0000;
}
else if (playerBody.label === 'right')
{
color = 0x00ff00;
}
else if (playerBody.label === 'top')
{
color = 0x0000ff;
}
else if (playerBody.label === 'bottom')
{
color = 0xffff00;
}
blockSprite.setTint(color).setTintMode(Phaser.TintModes.FILL);
}
}
});
this.cursors = this.input.keyboard.createCursorKeys();
this.add.text(10, 10, 'Move with cursor keys. Hit blocks with sensors.', { font: '16px Courier', fill: '#ffffff' });
}
update ()
{
if (this.cursors.left.isDown)
{
this.player.setVelocityX(-10);
}
else if (this.cursors.right.isDown)
{
this.player.setVelocityX(10);
}
else
{
this.player.setVelocityX(0);
}
if (this.cursors.up.isDown)
{
this.player.setVelocityY(-10);
}
else if (this.cursors.down.isDown)
{
this.player.setVelocityY(10);
}
else
{
this.player.setVelocityY(0);
}
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#1b1464',
parent: 'phaser-example',
physics: {
default: 'matter',
matter: {
debug: true,
gravity: {
y: 0
}
}
},
scene: Example
};
const game = new Phaser.Game(config);
Подготовка и создание составного тела
В методе create() происходит основная настройка физики и создание игровых объектов. Ключевой момент — создание составного тела из нескольких частей с помощью Matter.js.
Сначала импортируется фабрика тел Bodies из Matter. Затем создаётся центральный прямоугольник и четыре круга-сенсора, которые будут расположены слева, справа, сверху и снизу от центра. Важно отметить, что для кругов в конфигурации задано свойство isSensor: true. Это означает, что эти тела будут генерировать события столкновения, но не оказывать физического сопротивления (не отталкивать объекты). Каждому сенсору присваивается уникальная метка (label) для последующей идентификации.
const Bodies = Phaser.Physics.Matter.Matter.Bodies;
const rect = Bodies.rectangle(0, 0, 98, 98);
const circleA = Bodies.circle(-70, 0, 24, { isSensor: true, label: 'left' });
const circleB = Bodies.circle(70, 0, 24, { isSensor: true, label: 'right' });
const circleC = Bodies.circle(0, -70, 24, { isSensor: true, label: 'top' });
const circleD = Bodies.circle(0, 70, 24, { isSensor: true, label: 'bottom' });
На следующем шаге все эти части объединяются в одно физическое тело с помощью Phaser.Physics.Matter.Matter.Body.create. Параметр parts принимает массив созданных тел, а inertia: Infinity делает тело бесконечно инертным, что упрощает управление им с клавиатуры без влияния вращающего момента.
const compoundBody = Phaser.Physics.Matter.Matter.Body.create({
parts: [ rect, circleA, circleB, circleC, circleD ],
inertia: Infinity
});
Связывание тела со спрайтом и настройка мира
После создания физического тела его нужно связать с визуальным спрайтом в Phaser. Создаётся спрайт игрока (this.player) с помощью this.matter.add.image. Изначально ему назначается стандартное тело, но метод setExistingBody() заменяет его на наше составное тело.
this.player = this.matter.add.image(0, 0, 'block');
this.player.setExistingBody(compoundBody);
this.player.setPosition(100, 300);
Также на сцене размещаются три статичных блока, которые будут служить мишенями для сенсоров. Статичность задаётся методом .setStatic(true), что означает, что эти тела не будут двигаться под воздействием физики.
const testA = this.matter.add.image(400, 150, 'block').setStatic(true);
const testB = this.matter.add.image(600, 450, 'block').setStatic(true);
const testC = this.matter.add.image(200, 550, 'block').setStatic(true);
Конфигурация физического мира Matter задана в объекте config при создании игры. Здесь отключена гравитация (gravity: { y: 0 }), чтобы игрок не падал, и включён режим отладки (debug: true), который визуализирует физические тела и сенсоры.
Обработка событий столкновения сенсоров
Сердце примера — обработчик события collisionstart, который срабатывает при начале столкновения любых двух тел в мире Matter. Это событие вешается на this.matter.world.
this.matter.world.on('collisionstart', event => {
const pairs = event.pairs;
for (let i = 0; i < pairs.length; i++) {
// ... логика обработки
}
});
Внутри цикла мы перебираем все пары столкнувшихся тел (pairs). Нас интересуют только те столкновения, где одно из тел является сенсором (проверка pairs[i].isSensor).
Далее мы определяем, какое именно тело в паре — сенсор, а какое — статичный блок. Это нужно, потому что порядок тел bodyA и bodyB в паре не определён.
if (pairs[i].isSensor) {
let blockBody;
let playerBody;
if (bodyA.isSensor) {
blockBody = bodyB;
playerBody = bodyA;
} else if (bodyB.isSensor) {
blockBody = bodyA;
playerBody = bodyB;
}
// ...
}
После идентификации тел, мы получаем доступ к спрайтам через свойство gameObject физического тела. Это мощная особенность интеграции Phaser с Matter, позволяющая связать физическую логику с графикой.
const playerSprite = playerBody.gameObject;
const blockSprite = blockBody.gameObject;
Идентификация сенсора и визуальная обратная связь
Получив тело сенсора (playerBody), мы можем прочитать его метку (label), которую задали при создании. В зависимости от значения метки, мы выбираем цвет для окрашивания блока.
let color;
if (playerBody.label === 'left') {
color = 0xff0000;
} else if (playerBody.label === 'right') {
color = 0x00ff00;
} else if (playerBody.label === 'top') {
color = 0x0000ff;
} else if (playerBody.label === 'bottom') {
color = 0xffff00;
}
Затем статичный блок окрашивается в выбранный цвет с помощью метода setTint(). Параметр Phaser.TintModes.FILL в setTintMode() гарантирует, что цвет будет применён равномерно по всей площади спрайта.
blockSprite.setTint(color).setTintMode(Phaser.TintModes.FILL);
Таким образом, каждый блок будет менять цвет в зависимости от того, каким именно сенсором (левым, правым, верхним или нижним) игрок его коснулся. Это наглядно демонстрирует, какая именно зона взаимодействия сработала.
Управление и игровая логика
Управление игроком реализовано в методе update(), который вызывается на каждом кадре. Состояние клавиш-стрелок проверяется через объект this.cursors. В зависимости от нажатой клавиши, игроку задаётся скорость по оси X или Y с помощью setVelocityX() и setVelocityY().
if (this.cursors.left.isDown) {
this.player.setVelocityX(-10);
} else if (this.cursors.right.isDown) {
this.player.setVelocityX(10);
} else {
this.player.setVelocityX(0);
}
// ... аналогично для up/down
Поскольку у составного тела установлена бесконечная инерция (inertia: Infinity), применение скорости приводит к прямолинейному движению без вращения. Это типичное поведение для персонажа, управляемого с клавиатуры в топ-даун или платформерном стиле.
Что попробовать дальше
Составные тела и сенсоры Matter.js открывают перед разработчиком игр на Phaser широкие возможности для создания сложных и точных механик взаимодействия. Вы можете использовать этот подход не только для детектирования сторон столкновения, но и для создания сложных хитбоксов у персонажей (например, отдельные сенсоры для ударов руками и ногами), триггерных зон вокруг объектов или невидимых контактных датчиков.
Для экспериментов попробуйте: изменить форму и количество сенсоров, сделать сенсоры разного размера, добавить логику не только на начало (collisionstart), но и на окончание (collisionend) столкновения, чтобы убирать tint, или использовать сенсоры для сбора предметов, активации ловушек или открытия дверей.
