О чем этот пример
При работе с шестиугольными тайлмапами в Phaser 3 можно столкнуться с неочевидной ошибкой: свойства `widthInPixels` и `heightInPixels` объекта тайлмапа могут возвращать некорректные значения. Эта ошибка (issue #6790) приводит к неправильному позиционированию объектов и неверным расчётам области видимости. В статье разберём, как самостоятельно вычислить истинные размеры шестиугольного слоя, чтобы точно размещать элементы и рисовать границы.
Версия 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-outside.png');
this.load.tilemapTiledJSON('map', 'assets/tilemaps/iso/iso6790.json');
}
create ()
{
// setup tilemap
const map = this.add.tilemap('map');
const tileset = map.addTilesetImage('tileset', 'tiles');
map.createLayer('Calque 1', tileset);
this.cameras.main.setZoom(1);
this.cameras.main.centerOn(200, 100);
const tileLayer = map.layers[0];
/** Suggested Calculation **/
// if (map.orientation === 3) {
let realWidth = 0, realHeight = 0;
if (tileLayer.staggerAxis === 'y') {
const triangleHeight = (tileLayer.tileHeight - tileLayer.hexSideLength) / 2;
realWidth = tileLayer.tileWidth * ( tileLayer.width + 0.5);
realHeight = tileLayer.height * (tileLayer.hexSideLength + triangleHeight) + triangleHeight;
} else {
const triangleWidth = (tileLayer.tileWidth - tileLayer.hexSideLength) / 2;
realWidth = tileLayer.width * (tileLayer.hexSideLength + triangleWidth) + triangleWidth;
realHeight = tileLayer.tileHeight * ( tileLayer.height + 0.5);
}
// }
// add debug text
const text = this.add.text(10, 10, [
`Orientation: ${tileLayer.orientation}`,
`Stagger Axis: ${tileLayer.staggerAxis}`,
`Stagger Index: ${tileLayer.staggerIndex}`,
'',
`Width: ${map.width}`,
`TileWidth: ${map.tileWidth}`,
`Width in pixels (Red): ${map.widthInPixels}`,
`Correct Width (Green): ${realWidth}`,
'',
`Height: ${map.height}`,
`TileHeight: ${map.tileHeight}`,
`Height in pixels (Red): ${map.heightInPixels}`,
`Correct Height (Green): ${realHeight}`,
]);
text.setShadow(2, 2, '#000000', 1);
// draw rectangle of the wrong size using widthInPixels & heightInPixels
this.add.rectangle(0, 0, map.widthInPixels, map.heightInPixels, 0xff0000, 0.2).setOrigin(0,0);
// draw rectangle of the correct size
this.add.rectangle(0, 0, realWidth, realHeight, 0x00ff00, 0.2).setOrigin(0,0);
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
backgroundColor: '#1d1d1d',
scene: Example
};
const game = new Phaser.Game(config);
В чём проблема?
При загрузке шестиугольного (изометрического) тайлмапа Phaser корректно отрисовывает сцену, но внутренние расчёты размеров слоя могут быть ошибочными.
Код примера загружает тайлмап и создаёт слой. Затем он выводит на экран два полупрозрачных прямоугольника:
- Красный, построенный на основе map.widthInPixels и map.heightInPixels.
- Зелёный, построенный на основе вычисленных вручную значений realWidth и realHeight.
Как видно в примере, красный прямоугольник (стандартные свойства) не совпадает с границами зелёного (правильные вычисления) и самого тайлмапа. Это и есть проявление бага.
// Проблемные свойства (могут быть некорректны для шестиугольных карт)
map.widthInPixels
map.heightInPixels
Структура данных шестиугольного слоя
Чтобы понять, как исправить расчёты, нужно разобраться в свойствах слоя тайлмапа. После создания слоя мы можем получить к нему доступ через map.layers[0]. Ключевые свойства для расчётов:
- orientation: Ориентация тайлмапа. Для шестиугольных карт это значение `3` (Phaser.HEXAGONAL).
- staggerAxis: Ось, по которой идут ряды шестиугольников. Может быть 'y' (ряды смещены по вертикали) или 'x' (ряды смещены по горизонтали).
- staggerIndex: Индекс смещения (чётный или нечётный).
- tileWidth, tileHeight: Размеры тайла, включая пустые области.
- hexSideLength: Длина стороны шестиугольника.
- width, height: Размеры карты в тайлах.
const tileLayer = map.layers[0];
const axis = tileLayer.staggerAxis; // 'y' или 'x'
const side = tileLayer.hexSideLength;
const tileW = tileLayer.tileWidth;
const tileH = tileLayer.tileHeight;
Формула для правильного размера
Алгоритм расчёта зависит от staggerAxis. Идея в том, что шестиугольники упакованы не вплотную, а со смещением. Нужно учесть длину стороны (hexSideLength) и размер «треугольных» пустот по краям.
**Если ряды смещены по вертикали (staggerAxis === 'y'):**
1. Вычисляем высоту треугольного пустого участка сверху и снизу каждого тайла.
2. Ширина всей карты равна ширине тайла, умноженной на (количество тайлов по ширине + 0.5). Добавление 0.5 учитывает смещение рядов.
3. Высота карты рассчитывается с учётом длины стороны и треугольных участков.
**Если ряды смещены по горизонтали (staggerAxis === 'x'):**
Логика аналогична, но формулы применяются к другой оси.
let realWidth = 0, realHeight = 0;
if (tileLayer.staggerAxis === 'y') {
const triangleHeight = (tileLayer.tileHeight - tileLayer.hexSideLength) / 2;
realWidth = tileLayer.tileWidth * ( tileLayer.width + 0.5);
realHeight = tileLayer.height * (tileLayer.hexSideLength + triangleHeight) + triangleHeight;
} else {
const triangleWidth = (tileLayer.tileWidth - tileLayer.hexSideLength) / 2;
realWidth = tileLayer.width * (tileLayer.hexSideLength + triangleWidth) + triangleWidth;
realHeight = tileLayer.tileHeight * ( tileLayer.height + 0.5);
}
Практическое применение в игре
Зная реальные размеры слоя, вы можете решить несколько практических задач:
1. **Точное позиционирование объектов**, созданных через map.createFromObjects. Это и было первоначальной целью примера из баг-репорта.
2. **Корректное отображение границ игрового мира** для камеры или UI.
3. **Верный расчёт попадания** в границы карты для игровой логики.
Вместо использования сломанных свойств widthInPixels/heightInPixels, подставляйте вычисленные realWidth и realHeight.
// Пример: создание спрайта в центре шестиугольной карты
const sprite = this.physics.add.image(realWidth / 2, realHeight / 2, 'player');
// Пример: установка границ мира для физики
this.physics.world.setBounds(0, 0, realWidth, realHeight);
Что попробовать дальше
Пока ошибка в ядре Phaser не исправлена, ручной расчёт размеров шестиугольного тайлмапа — необходимость. Используйте приведённые формулы, чтобы ваши объекты и границы располагались точно. Для экспериментов попробуйте
- создать слой поверх тайлов, который будет подсвечивать вычисленную область
- написать вспомогательную функцию, которая автоматически рассчитывает размеры для любого шестиугольного слоя
- проверить работу
createFromObjectsс правильными координатами
