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

При работе с шестиугольными тайлмапами в 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 (четный/нечетный) на формулы, если оно отличается от используемого в вашей карте.