О чем этот пример
При создании игр с физикой часто требуется реалистичное взаимодействие объектов нестандартной формы. Использовать простые прямоугольники или круги для сложных спрайтов — не вариант. В этой статье разберем, как создавать составные (compound) физические тела в Phaser 3, используя движок Matter.js и данные из редактора Physics Editor. Этот подход позволяет точно соответствовать визуальной форме объекта и экономит время на ручном подборе коллайдеров.
Версия 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/green_ball.png');
this.load.image('image', 'assets/physics/compound.png');
this.load.json('shapes', 'assets/physics/compound.json');
}
create ()
{
this.matter.world.setBounds(0, 0, 800, 600, 32, true, true, false, true);
const Body = Phaser.Physics.Matter.Matter.Body;
const Bodies = Phaser.Physics.Matter.Matter.Bodies;
const Composite = Phaser.Physics.Matter.Matter.Composite;
const Parser = Phaser.Physics.Matter.PhysicsEditorParser;
const shapes = this.cache.json.get('shapes');
const composite = Composite.create();
const parts = [ 'bird', 'p', 'h', 'a', 's', 'e', 'r' ];
for (let i = 0; i < parts.length; i++)
{
const body = Body.create({ isStatic: true });
Body.setParts(body, Parser.parseVertices(shapes[parts[i]].fixtures[0].vertices));
Composite.addBody(composite, body);
}
/*
var fixtures = shapes.logo.fixtures;
for (var i = 0; i < fixtures.length; i++)
{
var body = Body.create({ isStatic: true });
Body.setParts(body, parseVertices(fixtures[i].vertices));
Composite.addBody(composite, body);
}
*/
Composite.translate(composite, { x: 0, y: 150 });
this.matter.world.add(composite);
this.add.image(400, 300, 'image');
for (let i = 0; i < 64; i++)
{
const ball = this.matter.add.image(Phaser.Math.Between(100, 700), Phaser.Math.Between(-600, 0), 'ball');
ball.setCircle();
ball.setFriction(0.005);
ball.setBounce(1);
}
}
parseVertices (vertexSets, options)
{
const Matter = Phaser.Physics.Matter.Matter;
let i, j, k, v, z;
const parts = [];
options = options || {};
for (v = 0; v < vertexSets.length; v += 1)
{
parts.push(Matter.Body.create(Matter.Common.extend({
position: Matter.Vertices.centre(vertexSets[v]),
vertices: vertexSets[v]
}, options)));
}
// flag coincident part edges
const coincidentMaxDist = 5;
for (i = 0; i < parts.length; i++)
{
const partA = parts[i];
for (j = i + 1; j < parts.length; j++)
{
const partB = parts[j];
if (Matter.Bounds.overlaps(partA.bounds, partB.bounds))
{
const pav = partA.vertices,
pbv = partB.vertices;
// iterate vertices of both parts
for (k = 0; k < partA.vertices.length; k++)
{
for (z = 0; z < partB.vertices.length; z++)
{
// find distances between the vertices
const da = Matter.Vector.magnitudeSquared(Matter.Vector.sub(pav[(k + 1) % pav.length], pbv[z])),
db = Matter.Vector.magnitudeSquared(Matter.Vector.sub(pav[k], pbv[(z + 1) % pbv.length]));
// if both vertices are very close, consider the edge concident (internal)
if (da < coincidentMaxDist && db < coincidentMaxDist)
{
pav[k].isInternal = true;
pbv[z].isInternal = true;
}
}
}
}
}
}
return parts;
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#0094f7',
parent: 'phaser-example',
physics: {
default: 'matter',
matter: {
debug: true
}
},
scene: Example
};
const game = new Phaser.Game(config);
Загрузка данных: изображение и JSON-фигуры
Основа работы — это разделение визуального представления объекта и его физической модели. Визуал загружается как обычная текстура, а данные о сложной форме — как JSON-файл, экспортированный из редактора Physics Editor.
this.load.image('image', 'assets/physics/compound.png');
this.load.json('shapes', 'assets/physics/compound.json');
Загруженный JSON содержит массив фигур (fixtures). Каждая фигура описывается массивом вершин (vertices) в формате, понятном для Matter.js. Это позволяет описывать одну сложную форму (например, логотип) как набор простых полигонов.
Создание составного тела (Composite Body)
В Matter.js составное тело создается через Composite. Это контейнер, который может объединять несколько простых тел (Body) в единую физическую сущность.
const composite = Composite.create();
В примере мы видим два подхода. Первый — перебор частей по их именам из JSON ('bird', 'p', 'h'...). Для каждой части создается статическое тело, а затем его форма задается через Body.setParts() и парсер PhysicsEditorParser.
const body = Body.create({ isStatic: true });
Body.setParts(body, Parser.parseVertices(shapes[parts[i]].fixtures[0].vertices));
Composite.addBody(composite, body);
Парсер Phaser.Physics.Matter.PhysicsEditorParser (Parser в коде) преобразует массив вершин из JSON в формат, пригодный для задания частей тела Matter.js. Это ключевой мост между редактором и игровым движком.
Альтернативный подход: ручной парсинг вершин
В закомментированном блоке кода показан альтернативный, более низкоуровневый подход. Здесь используется кастомная функция parseVertices, написанная непосредственно в классе сцены.
parseVertices (vertexSets, options) {
// ... логика создания тел из массивов вершин
// ... и обработка совпадающих границ
}
Эта функция не только создает тела из переданных наборов вершин, но и проводит дополнительную оптимизацию. Она находит границы (edges) соседних частей, которые находятся очень близко (в пределах coincidentMaxDist), и помечает их как внутренние (isInternal = true). Это помогает движку Matter.js корректно обрабатывать стыки внутри составного тела, избегая лишних вычислений столкновений внутри самого объекта.
Интеграция в мир и добавление динамических объектов
После сборки составного тела его нужно правильно разместить в мире и добавить в физическую симуляцию.
Composite.translate(composite, { x: 0, y: 150 });
this.matter.world.add(composite);
this.add.image(400, 300, 'image');
Сначала составное тело сдвигается на нужную позицию с помощью Composite.translate. Затем оно добавляется в физический мир через this.matter.world.add. Важно: визуальное изображение (image) добавляется отдельно, как обычный спрайт, и его позиция должна совпадать с позицией физического тела.
Для демонстрации взаимодействия в сцене создаются динамические шары. Обратите внимание на настройки физики:
ball.setCircle();
ball.setFriction(0.005);
ball.setBounce(1);
setCircle() задает шарообразную форму коллайдера. Ультранизкое трение (friction: 0.005) и упругость (bounce: 1) позволяют шарам долго и энергично отскакивать от статического составного тела, наглядно демонстрируя его сложную форму.
Конфигурация Physics Debug
Для отладки сложных физических форм крайне полезен режим отладки. Он включается в конфигурации игры.
physics: {
default: 'matter',
matter: {
debug: true
}
}
Когда debug: true, поверх спрайтов отрисовываются контуры всех физических тел. Вы увидите не просто прямоугольник вокруг всего логотипа, а множество полигонов, точно соответствующих каждой букве и фигуре. Это незаменимый инструмент для проверки корректности экспорта данных из Physics Editor и их загрузки в игру.
Что попробовать дальше
Использование составных тел через Physics Editor кардинально упрощает создание реалистичной физики для объектов сложной формы. Основной вывод: разделяйте визуал и данные физической модели. Экспериментируйте: попробуйте сделать составное тело динамическим (уберите isStatic: true) и посмотрите на его поведение. Или загрузите свою текстуру, создайте для нее коллайдер в Physics Editor и импортируйте в свой проект, чтобы разрушать ее шарами.
