О чем этот пример

При работе с шестиугольными тайлмапами в 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 не исправлена, ручной расчёт размеров шестиугольного тайлмапа — необходимость. Используйте приведённые формулы, чтобы ваши объекты и границы располагались точно. Для экспериментов попробуйте

  1. создать слой поверх тайлов, который будет подсвечивать вычисленную область
  2. написать вспомогательную функцию, которая автоматически рассчитывает размеры для любого шестиугольного слоя
  3. проверить работу createFromObjects с правильными координатами