О чем этот пример
Создание интерактивных объектов с физикой — ключ к увлекательным игровым механикам. Этот пример демонстрирует, как с помощью Phaser и Matter.js можно в реальном времени рисовать упругую линию из звеньев, реагирующую на физические взаимодействия. Вы научитесь создавать составные динамические тела из полигонов и соединять их упругими связями (constraints), реагирующие на столкновения. Это основа для создания реалистичных верёвок, цепей, мостов или разрушаемых конструкций в вашей игре.
Версия 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('ball', 'assets/sprites/pangball.png');
}
create ()
{
this.matter.world.setBounds(0, 0, 800, 600, 32, true, true, false, true);
const sides = 6;
const size = 14;
const distance = size * 2;
const stiffness = 0.1;
const lastPosition = new Phaser.Math.Vector2();
const options = { friction: 0.005, frictionAir: 0, restitution: 1 };
const pinOptions = { friction: 0, frictionAir: 0, restitution: 0, ignoreGravity: true, inertia: Infinity, isStatic: true };
let current = null;
let previous = null;
this.input.on('pointerdown', function (pointer)
{
lastPosition.x = pointer.x;
lastPosition.y = pointer.y;
previous = this.matter.add.polygon(pointer.x, pointer.y, sides, size, pinOptions);
}, this);
this.input.on('pointermove', function (pointer)
{
if (pointer.isDown)
{
const x = pointer.x;
const y = pointer.y;
if (Phaser.Math.Distance.Between(x, y, lastPosition.x, lastPosition.y) > distance)
{
lastPosition.x = x;
lastPosition.y = y;
current = this.matter.add.polygon(pointer.x, pointer.y, sides, size, pinOptions);
this.matter.add.constraint(previous, current, distance, stiffness);
previous = current;
}
}
}, this);
this.input.once('pointerup', function (pointer)
{
this.time.addEvent({
delay: 1000,
callback: function ()
{
const ball = this.matter.add.image(Phaser.Math.Between(100, 700), Phaser.Math.Between(-600, 0), 'ball');
ball.setCircle();
ball.setFriction(0.005).setBounce(1);
},
callbackScope: this,
repeat: 100
});
}, this);
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
physics: {
default: 'matter',
matter: {
gravity: {
y: 0.8
},
enableSleep: true,
debug: true
}
},
scene: Example
};
const game = new Phaser.Game(config);
Настройка мира и параметров
В методе create() настраивается игровой мир и определяются ключевые параметры для будущей конструкции.
this.matter.world.setBounds(0, 0, 800, 600, 32, true, true, false, true);
Здесь устанавливаются границы физического мира Matter.js. Последние четыре параметра true, true, false, true указывают, что границы есть у верхней, правой, нижней и левой стороны соответственно.
Затем объявляются константы, которые будут управлять внешним видом и поведением линии:
const sides = 6; // Количество сторон у каждого звена (шестиугольник)
const size = 14; // Радиус шестиугольника
const distance = size * 2; // Жёстко заданная длина связи между звеньями
const stiffness = 0.1; // Жёсткость связи (0-1, где 1 - абсолютно жёсткая)
const lastPosition = new Phaser.Math.Vector2(); // Вектор для отслеживания последней позиции
const options = { friction: 0.005, frictionAir: 0, restitution: 1 }; // Для мяча
const pinOptions = { friction: 0, frictionAir: 0, restitution: 0, ignoreGravity: true, inertia: Infinity, isStatic: true }; // Для звеньев
Объект pinOptions делает звенья статичными (isStatic: true), игнорирующими гравитацию и обладающими бесконечной инерцией, что позволяет линии "висеть" в воздухе, пока мы её рисуем.
Начало рисования: обработка нажатия
При первом нажатии кнопки мыши (pointerdown) создаётся отправная точка линии — первое звено.
this.input.on('pointerdown', function (pointer) {
lastPosition.x = pointer.x;
lastPosition.y = pointer.y;
previous = this.matter.add.polygon(pointer.x, pointer.y, sides, size, pinOptions);
}, this);
Функция this.matter.add.polygon() создаёт физическое тело в форме многоугольника (шестиугольника) в Matter.js. Тело сразу становится статичным, согласно pinOptions. Переменная previous хранит ссылку на это тело, чтобы к нему можно было прицепить следующее звено.
Рисование линии: обработка движения
Пока кнопка мыши зажата и происходит движение (pointermove), линия продолжает рисоваться, но не непрерывно, а с определённым шагом.
this.input.on('pointermove', function (pointer) {
if (pointer.isDown) {
const x = pointer.x;
const y = pointer.y;
if (Phaser.Math.Distance.Between(x, y, lastPosition.x, lastPosition.y) > distance) {
lastPosition.x = x;
lastPosition.y = y;
current = this.matter.add.polygon(pointer.x, pointer.y, sides, size, pinOptions);
this.matter.add.constraint(previous, current, distance, stiffness);
previous = current;
}
}
}, this);
Ключевой момент — проверка расстояния с помощью Phaser.Math.Distance.Between. Новое звено создаётся только если курсор отъехал от позиции последнего созданного звена больше, чем на distance (удвоенный радиус). Это предотвращает создание слишком частой "сетки" звеньев.
Метод this.matter.add.constraint() создаёт физическую связь (constraint) между двумя телами — предыдущим (previous) и текущим (current). Параметры distance и stiffness определяют длину и упругость этой связи. После создания связи переменная previous обновляется, и цепочка может продолжаться.
Завершение и тестирование: обработка отпускания
Когда игрок отпускает кнопку мыши (pointerup), статичность всех звеньев линии снимается, и они начинают подчиняться законам физики. Кроме того, запускается "стресс-тест" для нашей конструкции.
this.input.once('pointerup', function (pointer) {
this.time.addEvent({
delay: 1000,
callback: function () {
const ball = this.matter.add.image(Phaser.Math.Between(100, 700), Phaser.Math.Between(-600, 0), 'ball');
ball.setCircle();
ball.setFriction(0.005).setBounce(1);
},
callbackScope: this,
repeat: 100
});
}, this);
Метод this.time.addEvent создаёт таймер, который каждую секунду (1000 мс) в течение 100 раз выполняет колбэк. В колбэке создаётся спрайт мяча с физикой Matter.js (this.matter.add.image).
Важные преобразования для корректной физики мяча:
ball.setCircle(); // Заменяет прямоугольный хип-бокс тела на круглый
ball.setFriction(0.005).setBounce(1); // Устанавливает низкое трение и упругость (отскок) равную 1
Мячи появляются в случайной позиции по X (от 100 до 700) и выше верхней границы экрана по Y (от -600 до 0), создавая эффект падения на нарисованную конструкцию.
Конфигурация игры: активация Matter.js
Вся магия физики возможна благодаря правильной настройке конфигурации игры.
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
physics: {
default: 'matter', // Выбор Matter.js в качестве основного физического движка
matter: {
gravity: { y: 0.8 }, // Настройка силы гравитации
enableSleep: true, // Оптимизация: неактивные тела "засыпают"
debug: true // Визуализация хип-боксов и точек привязки для отладки
}
},
scene: Example
};
Установка debug: true — отличный инструмент для понимания, как именно физический движок видит ваши тела. Вы увидите контуры всех полигонов и связи между ними.
Что попробовать дальше
Вы создали интерактивный симулятор для рисования упругих динамических конструкций. Ключевые элементы — это связывание тел через constraint и контроль их физических свойств через параметры тела.
Идеи для экспериментов:
1. Измените stiffness на 1, чтобы получить абсолютно жёсткую конструкцию, или на 0.01 — для очень болтающейся верёвки.
2. Замените this.matter.add.polygon на this.matter.add.image с другой текстурой, чтобы линия состояла из спрайтов.
3. В обработчике pointerup уберите таймер и сделайте так, чтобы при клике все звенья переставали быть статичными (body.isStatic = false), позволяя конструкции упасть под действием гравитации.
