О чем этот пример
При разработке игр с изометрическими или обычными тайловыми картами часто возникает задача взаимодействия с областью: подсветка зоны действия заклинания, выбор юнитов в радиусе или определение тайлов под курсором сложной формы. Метод `getTilesWithinShape` из API Phaser 3 Tilemap Layers позволяет решить эту задачу элегантно, без необходимости вручную перебирать все тайлы на карте. В этой статье разберем, как работает этот метод на практическом примере с динамической областью поиска, управляемой мышью.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
controls;
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('tiles', 'assets/tilemaps/iso/iso-64x64-outline.png');
this.load.image('tiles2', 'assets/tilemaps/iso/iso-64x64-building.png');
this.load.tilemapTiledJSON('map', 'assets/tilemaps/iso/isorpg.json');
}
create ()
{
this.overlappingTiles = [];
this.map = this.add.tilemap('map');
const tileset1 = this.map.addTilesetImage('iso-64x64-outside', 'tiles');
const tileset2 = this.map.addTilesetImage('iso-64x64-building', 'tiles2');
this.layer1 = this.map.createLayer('Tile Layer 1', [ tileset1, tileset2 ]);
this.graphics = this.add.graphics({ lineStyle: { width: 2, color: 0x00ff00 } });
this.radius = 64;
this.circle = new Phaser.Geom.Circle(400, 300, this.radius);
this.rectangle = new Phaser.Geom.Rectangle(0, 0, this.radius * 2, this.radius * 2);
const cursors = this.input.keyboard.createCursorKeys();
// this.layer1.setScale(2, 2);
// this.cameras.main.setZoom(2);
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
acceleration: 0.04,
drag: 0.0005,
maxSpeed: 0.7
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
this.input.on('pointermove', this.updateCircle, this);
this.text = this.add.text(16, 16, '', {
font: '16px Arial',
backgroundColor: '#000000',
fill: '#ffffff'
});
this.text.setScrollFactor(0);
console.log(this.text);
}
updateCircle ()
{
const point = this.input.activePointer.positionToCamera(this.cameras.main);
this.overlappingTiles.forEach((tile) => { tile.tint = 0xffffff; });
this.overlappingTiles = [];
this.circle.setPosition(point.x, point.y);
this.rectangle.setPosition(point.x - this.radius, point.y - this.radius);
this.graphics.clear();
this.graphics.strokeCircleShape(this.circle);
this.graphics.strokeRectShape(this.rectangle);
var tiles = this.layer1.getTilesWithinShape(this.circle);
// var tiles = this.layer1.getTilesWithinShape(this.rectangle);
tiles.forEach(tile =>
{
this.overlappingTiles.push(tile);
});
this.overlappingTiles.forEach((tile) => { tile.tint = 0xff0000; });
}
update (time, delta)
{
this.controls.update(delta);
const worldPoint = this.input.activePointer.positionToCamera(this.cameras.main);
const message = [ `Mouse Position: ${worldPoint.x}, ${worldPoint.y}` ];
const tile = this.map.getTileAtWorldXY(worldPoint.x, worldPoint.y);
if (tile)
{
message.push(`Tile Center Position: ${tile.getCenterX()}, ${tile.getCenterY()}`);
message.push(`Tile Bounds: ${tile.getLeft()}, ${tile.getTop()} -> ${tile.getRight()}, ${tile.getBottom()}`);
}
this.text.setText(message);
// this.updateCircle(worldPoint);
}
}
const config = {
type: Phaser.WEBGL,
width: 800,
height: 600,
backgroundColor: '#2d2d2d',
parent: 'phaser-example',
pixelArt: true,
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и загрузка ресурсов
Пример начинается с создания сцены, которая загружает тайловую карту и два набора тайлов (tilesets). Карта загружается из JSON-файла, созданного в редакторе Tiled.
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('tiles', 'assets/tilemaps/iso/iso-64x64-outline.png');
this.load.image('tiles2', 'assets/tilemaps/iso/iso-64x64-building.png');
this.load.tilemapTiledJSON('map', 'assets/tilemaps/iso/isorpg.json');
}
В методе create создается экземпляр тайловой карты (this.map), к нему добавляются tilesets, и на их основе формируется слой (this.layer1). Этот слой и будет основным объектом для поиска тайлов.
create ()
{
this.overlappingTiles = [];
this.map = this.add.tilemap('map');
const tileset1 = this.map.addTilesetImage('iso-64x64-outline', 'tiles');
const tileset2 = this.map.addTilesetImage('iso-64x64-building', 'tiles2');
this.layer1 = this.map.createLayer('Tile Layer 1', [ tileset1, tileset2 ]);
}
Создание геометрических фигур и элементов управления
Для визуализации области поиска и взаимодействия создаются две геометрические фигуры: круг (Phaser.Geom.Circle) и прямоугольник (Phaser.Geom.Rectangle). Изначально используется круг. Также настраивается управление камерой с клавиатуры для навигации по большой карте и графический объект (this.graphics) для отрисовки контура фигуры.
this.graphics = this.add.graphics({ lineStyle: { width: 2, color: 0x00ff00 } });
this.radius = 64;
this.circle = new Phaser.Geom.Circle(400, 300, this.radius);
this.rectangle = new Phaser.Geom.Rectangle(0, 0, this.radius * 2, this.radius * 2);
const cursors = this.input.keyboard.createCursorKeys();
const controlConfig = {
camera: this.cameras.main,
left: cursors.left,
right: cursors.right,
up: cursors.up,
down: cursors.down,
acceleration: 0.04,
drag: 0.0005,
maxSpeed: 0.7
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
Событие pointermove привязано к методу updateCircle, который будет пересчитывать позицию фигуры и искать тайлы.
Ядро логики: поиск тайлов в области
Ключевой метод всего примера — this.layer1.getTilesWithinShape(this.circle). Он принимает в качестве аргумента объект геометрической фигуры (круг, прямоугольник, треугольник и т.д.) и возвращает массив тайлов, чьи границы пересекаются с этой фигурой.
Логика в updateCircle:
1. Сбрасывается подсветка ранее найденных тайлов.
2. Позиция круга обновляется в соответствии с положением курсора в мировых координатах (с учетом камеры).
3. Контур фигуры перерисовывается.
4. Вызывается getTilesWithinShape для получения нового списка тайлов.
5. Найденные тайлы подсвечиваются красным оттенком (tint).
updateCircle ()
{
const point = this.input.activePointer.positionToCamera(this.cameras.main);
this.overlappingTiles.forEach((tile) => { tile.tint = 0xffffff; });
this.overlappingTiles = [];
this.circle.setPosition(point.x, point.y);
this.graphics.clear();
this.graphics.strokeCircleShape(this.circle);
var tiles = this.layer1.getTilesWithinShape(this.circle);
tiles.forEach(tile =>
{
this.overlappingTiles.push(tile);
});
this.overlappingTiles.forEach((tile) => { tile.tint = 0xff0000; });
}
**Важно:** метод getTilesWithinShape работает с *мировыми координатами* фигуры. Поэтому позиция круга задается через point.x, point.y, которые уже переведены из координат камеры в мировые с помощью positionToCamera.
Отладка и отображение информации
В методе update реализован дополнительный инструмент для отладки. Он постоянно определяет тайл под точкой курсора (используя this.map.getTileAtWorldXY) и выводит в текстовое поле его координаты и границы.
update (time, delta)
{
this.controls.update(delta);
const worldPoint = this.input.activePointer.positionToCamera(this.cameras.main);
const message = [ `Mouse Position: ${worldPoint.x}, ${worldPoint.y}` ];
const tile = this.map.getTileAtWorldXY(worldPoint.x, worldPoint.y);
if (tile)
{
message.push(`Tile Center Position: ${tile.getCenterX()}, ${tile.getCenterY()}`);
message.push(`Tile Bounds: ${tile.getLeft()}, ${tile.getTop()} -> ${tile.getRight()}, ${tile.getBottom()}`);
}
this.text.setText(message);
}
Это позволяет наглядно сравнить результат точечного поиска (getTileAtWorldXY) и поиска по области (getTilesWithinShape). Текстовый элемент зафиксирован на экране (setScrollFactor(0)), чтобы его было видно при движении камеры.
Гибкость использования: от круга к прямоугольнику
Исходный код демонстрирует закомментированную альтернативу — использование прямоугольника вместо круга. Чтобы переключиться, достаточно закомментировать строку с кругом и раскомментировать строку с прямоугольником в методе updateCircle.
// var tiles = this.layer1.getTilesWithinShape(this.circle);
var tiles = this.layer1.getTilesWithinShape(this.rectangle);
Это показывает, что метод универсален и может работать с любым объектом, наследующимся от Phaser.Geom.Shape. Также в коде заданы базовые размеры прямоугольника, но его позиция динамически обновляется вместе с курсором.
Что попробовать дальше
Метод getTilesWithinShape — мощный и производительный инструмент для работы с тайловыми областями в Phaser 3. Он избавляет разработчика от необходимости писать сложные алгоритмы пересечения и работает напрямую с оптимизированными структурами данных тайловой карты.
**Идеи для экспериментов:**
1. Используйте другие фигуры, например, Phaser.Geom.Triangle или Phaser.Geom.Ellipse.
2. Реализуйте выбор юнитов в радиусе, комбинируя поиск тайлов с проверкой объектов, находящихся на этих тайлах.
3. Измените логику подсветки: например, окрашивайте тайлы в разные цвета в зависимости от их свойства (проходимость, тип местности).
4. Создайте фигуру произвольной формы с помощью Phaser.Geom.Polygon и используйте её для поиска.
