О чем этот пример
Создание сложных коллизионных форм для платформеров или головоломок вручную — утомительно. Phaser позволяет загружать карты, созданные в редакторе Tiled, вместе с данными о столкновениях, заданными визуально. Это открывает путь к быстрому прототипированию уровней с точной физикой. В этой статье мы разберем пример, который загружает тайловую карту, автоматически настраивает коллизии на основе данных из Tiled, визуализирует эти формы и интегрирует всё с физическим движком Matter.js для создания интерактивной сцены, где шарики отскакивают от сложных препятствий.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
layer;
shapeGraphics;
debugGraphics;
controls;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.spritesheet('balls', 'assets/sprites/balls.png', { frameWidth: 17, frameHeight: 17 });
// this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/tileset-collision-shapes.json');
this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/tileset-collision-shapes-v12.json');
this.load.image('kenny_platformer_64x64', 'assets/tilemaps/tiles/kenny_platformer_64x64.png');
}
create ()
{
const map = this.make.tilemap({ key: 'map' });
const tileset = map.addTilesetImage('kenny_platformer_64x64');
this.layer = map.createLayer(0, tileset, 0, 0);
// Instead of setting collision by index, you can set any tile that has collision data to
// collide. Typically, this is done in the Tiled collision editor. All tiles in this layer have
// collision shapes.
this.layer.setCollisionFromCollisionGroup();
this.shapeGraphics = this.add.graphics();
this.drawCollisionShapes(this.shapeGraphics);
this.matter.world.convertTilemapLayer(this.layer);
this.matter.world.setBounds(map.widthInPixels, map.heightInPixels);
// Drop bouncy, Matter balls on pointer down
this.input.on('pointerdown', function ()
{
const worldPoint = this.input.activePointer.positionToCamera(this.cameras.main);
for (let i = 0; i < 4; i++)
{
const x = worldPoint.x + Phaser.Math.RND.integerInRange(-5, 5);
const y = worldPoint.y + Phaser.Math.RND.integerInRange(-5, 5);
const frame = Phaser.Math.RND.integerInRange(0, 5);
this.matter.add.image(x, y, 'balls', frame, { restitution: 1 });
}
}, this);
this.input.keyboard.on('keydown-SPACE', event =>
{
// shapeGraphics.visible = !shapeGraphics.visible;
});
const help = this.add.text(16, 16, 'Click to drop balls\nPress "space" to toggle rendering collision shapes', {
fontSize: '18px',
padding: { x: 10, y: 5 },
backgroundColor: '#000000',
fill: '#ffffff'
});
help.setScrollFactor(0);
const cursors = this.input.keyboard.createCursorKeys();
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
speed: 0.5
};
this.controls = new Phaser.Cameras.Controls.FixedKeyControl(controlConfig);
}
update (time, delta)
{
this.controls.update(delta);
}
drawCollisionShapes (graphics)
{
graphics.clear();
// Loop over each tile and visualize its collision shape (if it has one)
this.layer.forEachTile(tile =>
{
const tileWorldX = tile.getLeft();
const tileWorldY = tile.getTop();
const collisionGroup = tile.getCollisionGroup();
// console.log(collisionGroup);
if (!collisionGroup || collisionGroup.objects.length === 0) { return; }
// The group will have an array of objects - these are the individual collision shapes
const objects = collisionGroup.objects;
for (let i = 0; i < objects.length; i++)
{
const object = objects[i];
const objectX = tileWorldX + object.x;
const objectY = tileWorldY + object.y;
// When objects are parsed by Phaser, they will be guaranteed to have one of the
// following properties if they are a rectangle/ellipse/polygon/polyline.
if (object.rectangle)
{
graphics.strokeRect(objectX, objectY, object.width, object.height);
}
else if (object.ellipse)
{
// Ellipses in Tiled have a top-left origin, while ellipses in Phaser have a center
// origin
graphics.strokeEllipse(
objectX + object.width / 2, objectY + object.height / 2,
object.width, object.height
);
}
else if (object.polygon || object.polyline)
{
const originalPoints = object.polygon ? object.polygon : object.polyline;
const points = [];
for (let j = 0; j < originalPoints.length; j++)
{
const point = originalPoints[j];
points.push({
x: objectX + point.x,
y: objectY + point.y
});
}
graphics.strokePoints(points);
}
}
});
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#ffffff',
parent: 'phaser-example',
pixelArt: true,
physics: {
default: 'matter',
matter: {
gravity: { y: 1 },
enableSleep: true
}
},
scene: Example
};
const game = new Phaser.Game(config);
Загрузка карты и настройка коллизий
Ключевой момент — загрузка JSON-файла карты, созданного в Tiled, где для тайлов уже заданы коллизии (прямоугольники, эллипсы, полигоны). Phaser парсит эти данные и предоставляет удобный метод для их применения.
const map = this.make.tilemap({ key: 'map' });
const tileset = map.addTilesetImage('kenny_platformer_64x64');
this.layer = map.createLayer(0, tileset, 0, 0);
// Магическая строка: включаем коллизии для всех тайлов,
// у которых есть данные о столкновениях в Tiled
this.layer.setCollisionFromCollisionGroup();
Метод setCollisionFromCollisionGroup() проходит по всем тайлам слоя. Если у тайла в Tiled была задана collision shape (или группа shapes), Phaser помечает этот тайл как коллизионный. Это гораздо гибче, чем setCollisionByIndex, так как позволяет задавать произвольные формы, а не просто считать целый тайл сплошным прямоугольником.
Визуализация коллизионных форм для отладки
Чтобы убедиться, что формы загрузились правильно, и понимать, как устроен уровень, полезно их нарисовать. В примере для этого создается отдельный объект Graphics и используется метод drawCollisionShapes.
this.shapeGraphics = this.add.graphics();
this.drawCollisionShapes(this.shapeGraphics);
Метод drawCollisionShapes обходит все тайлы слоя с помощью forEachTile. Для каждого тайла он получает его CollisionGroup — объект, содержащий массив фигур (objects).
const collisionGroup = tile.getCollisionGroup();
if (!collisionGroup || collisionGroup.objects.length === 0) { return; }
Далее, в зависимости от типа фигуры (rectangle, ellipse, polygon/polyline), метод рассчитывает её мировые координаты и рисует контур с помощью методов graphics.strokeRect, strokeEllipse или strokePoints. Обратите внимание на поправку для эллипса: в Tiled его координаты — левый верхний угол, а в Phaser — центр.
Интеграция с физическим движком Matter.js
После того как коллизии для тайлового слоя установлены, их нужно передать физическому движку. Phaser делает это одной строкой, конвертируя весь слой в статические Matter-тела.
this.matter.world.convertTilemapLayer(this.layer);
this.matter.world.setBounds(map.widthInPixels, map.heightInPixels);
Метод convertTilemapLayer(this.layer) создает в Matter.js статические тела, форма которых в точности повторяет коллизионные данные из Tiled. Теперь это полноценная часть физического мира.
Чтобы оживить сцену, по клику мыши создаются динамические тела — шарики с высокой упругостью (restitution: 1).
this.input.on('pointerdown', function ()
{
const worldPoint = this.input.activePointer.positionToCamera(this.cameras.main);
// Создаем 4 шарика со случайным смещением и спрайтом
this.matter.add.image(x, y, 'balls', frame, { restitution: 1 });
}, this);
Шарики, созданные через this.matter.add.image, будут реалистично отскакивать от сложных форм платформ, демонстрируя корректность работы всей цепочки: Tiled -> Phaser Tilemap -> Matter.js.
Управление камерой и интерфейс
Поскольку карта может быть большой, в примере реализовано управление камерой с клавиатуры и статический текст с подсказками.
const cursors = this.input.keyboard.createCursorKeys();
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
speed: 0.5
};
this.controls = new Phaser.Cameras.Controls.FixedKeyControl(controlConfig);
В методе update происходит обновление состояния камеры:
update (time, delta)
{
this.controls.update(delta);
}
Текст помощи создается с прокруткой, привязанной к камере (setScrollFactor(0)), чтобы он всегда оставался на экране. Также закомментирован обработчик для клавиши SPACE, который мог бы переключать видимость отладочной графики (shapeGraphics).
Что попробовать дальше
Использование setCollisionFromCollisionGroup() — мощный и правильный способ переноса сложных коллизий из Tiled в Phaser. Это экономит часы ручной работы и позволяет дизайнерам уровней работать в удобном визуальном редакторе.
**Идеи для экспериментов:**
1. Создайте в Tiled карту с коллизиями разных типов (полигоны для склонов, эллипсы для валунов) и загрузите её.
2. Попробуйте динамически включать/выключать отрисовку коллизионных форм по нажатию клавиши (раскомментируйте код с keydown-SPACE).
3. Измените физические свойства шариков (массу, трение) или создайте другие динамические тела, которые будут катиться по сложному рельефу.
4. Исследуйте объект collisionGroup, который возвращает tile.getCollisionGroup(), чтобы понять его структуру для кастомной логики.
