О чем этот пример
При работе с шестиугольными тайлмапами в Phaser вы можете столкнуться с тем, что встроенные свойства `widthInPixels` и `heightInPixels` возвращают некорректные значения. Это происходит из-за особенностей геометрии шестиугольников и способа их укладки (staggering). В результате визуальные элементы, привязанные к этим размерам (например, фон или UI-панели), оказываются не на своем месте. В этой статье мы разберем, почему возникает эта ошибка, и предложим точный алгоритм расчета реальных размеров карты, основанный на свойствах слоя тайлов.
Версия 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/tilesets/tileset.png');
this.load.tilemapTiledJSON('map', 'assets/tilemaps/iso/hexagonal.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 у объекта карты (Tilemap) и слоя (TilemapLayer) есть свойства для получения размеров в пикселях: widthInPixels и heightInPixels. Логично предположить, что они возвращают реальную ширину и высоту всей карты, чтобы, например, нарисовать поверх нее полупрозрачный прямоугольник-подложку.
Однако для шестиугольных карт эти свойства рассчитываются так, будто тайлы — это обычные прямоугольники, без учета их смещения (stagger) и формы. В примере это наглядно показано: красный прямоугольник, построенный по widthInPixels и heightInPixels, не совпадает с зеленой областью, которая покрывает все тайлы карты.
// Так делать не стоит для шестиугольных карт:
const wrongWidth = map.widthInPixels;
const wrongHeight = map.heightInPixels;
this.add.rectangle(0, 0, wrongWidth, wrongHeight, 0xff0000, 0.2).setOrigin(0,0);
Анализ свойств шестиугольного слоя
Ключ к правильному расчету лежит в свойствах объекта слоя тайлов (tileLayer в примере). Для шестиугольных карт критически важны следующие параметры:
* staggerAxis: Определяет ось, вдоль которой происходит смещение строк или столбцов. Может быть 'y' (смещение по вертикали, ряды смещены) или 'x' (смещение по горизонтали, колонки смещены).
* hexSideLength: Длина стороны шестиугольника. Это фундаментальный параметр для геометрических вычислений.
* tileWidth и tileHeight: Габаритные размеры тайла (от края до края).
* width и height: Размеры карты в тайлах.
Этих свойств достаточно, чтобы вывести формулы для реальной ширины и высоты всей карты в пикселях.
const tileLayer = map.layers[0];
console.log(tileLayer.staggerAxis); // 'y' или 'x'
console.log(tileLayer.hexSideLength); // Числовое значение
console.log(`Карта ${tileLayer.width} x ${tileLayer.height} тайлов`);
Формулы для расчета (staggerAxis === 'y')
Когда ряды смещены по вертикали (staggerAxis: 'y'), ширина и высота рассчитываются особым образом. Визуализируйте шестиугольник: его общая высота (tileHeight) складывается из длины стороны (hexSideLength) и двух маленьких треугольников сверху и снизу.
1. Высота одного треугольника: (tileHeight - hexSideLength) / 2.
2. **Реальная ширина:** Так как четные и нечетные ряды смещены на полтайла, общая ширина равна tileWidth * (width + 0.5). Коэффициент 0.5 учитывает это смещение.
3. **Реальная высота:** Каждый ряд добавляет к высоте hexSideLength и высоту одного треугольника. Последний треугольник внизу нужно добавить отдельно. Итог: height * (hexSideLength + triangleHeight) + triangleHeight.
// Расчет для staggerAxis === 'y'
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;
}
Формулы для расчета (staggerAxis === 'x')
Когда смещение происходит по горизонтали (staggerAxis: 'x'), логика зеркальна, но применяется к ширине. В этом случае треугольники образуются по бокам шестиугольника.
1. Ширина одного треугольника: (tileWidth - hexSideLength) / 2.
2. **Реальная ширина:** Каждая колонка добавляет hexSideLength и ширину одного треугольника. К последней колонке добавляем еще один треугольник: width * (hexSideLength + triangleWidth) + triangleWidth.
3. **Реальная высота:** Из-за смещения колонок общая высота равна tileHeight * (height + 0.5).
// Расчет для staggerAxis === 'x'
} else {
const triangleWidth = (tileLayer.tileWidth - tileLayer.hexSideLength) / 2;
realWidth = tileLayer.width * (tileLayer.hexSideLength + triangleWidth) + triangleWidth;
realHeight = tileLayer.tileHeight * ( tileLayer.height + 0.5);
}
Применение правильных размеров на практике
Получив значения realWidth и realHeight, вы можете точно позиционировать любые графические элементы относительно границ тайлмапа. В примере это используется для отрисовки правильного зеленого прямоугольника-маски.
Этот расчет также жизненно важен для:
* Создания статического фона или UI-панели, точно покрывающей игровую карту.
* Правильного ограничения камеры (camera.setBounds), чтобы она не выезжала за пределы карты.
* Расчетов для размещения объектов в мировых координатах относительно краев карты.
// Рисуем правильную подложку под карту
this.add.rectangle(0, 0, realWidth, realHeight, 0x00ff00, 0.2).setOrigin(0,0);
// Устанавливаем границы камеры по реальным размерам карты
this.cameras.main.setBounds(0, 0, realWidth, realHeight);
Что попробовать дальше
Встроенные свойства widthInPixels и heightInPixels не работают корректно для шестиугольных тайлмапов из-за их сложной геометрии. Для получения точных размеров необходимо использовать свойства слоя (staggerAxis, hexSideLength и др.) и применять соответствующие геометрические формулы. Предложенный в примере код — готовое решение для этой задачи.
**Идеи для экспериментов:**
1. Оберните расчет в универсальную функцию getHexMapBounds(layer), которая возвращает объект {width, height}.
2. Попробуйте применить эти расчеты для динамического создания коллайдеров по границам карты.
3. Исследуйте, как влияет свойство staggerIndex (четный/нечетный) на формулы, если оно отличается от используемого в вашей карте.
