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

Работа с шестиугольными тайловыми картами в Phaser открывает новые возможности для создания стратегических игр. Однако преобразование координат из тайловых индексов в мировые координаты имеет свои нюансы. В этой статье мы разберем, как работает метод `tileToWorldXY` для шестиугольных слоев и как визуализировать этот процесс, чтобы лучше понимать пространственную логику вашей игры.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('tiles', 'assets/tilemaps/iso/tilesets/hex-tiles.png');
        this.load.tilemapTiledJSON('map', 'assets/tilemaps/hex/10x10-test.json');
    }

    create ()
    {
        const map = this.add.tilemap('map');

        const tileset = map.addTilesetImage('hex-tiles', 'tiles');

        const layer = map.createLayer('Tile Layer 1', tileset);

        // layer.setScale(2.8, 1.4);

        const cursors = this.input.keyboard.createCursorKeys();

        const controlConfig = {
            camera: this.cameras.main,
            left: cursors.left,
            right: cursors.right,
            up: cursors.up,
            down: cursors.down,
            zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
            zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
            acceleration: 0.02,
            drag: 0.0005,
            maxSpeed: 0.7
        };

        this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);

        const t = this.add.text(32, 550, 'x: 0 y: 0');

        let x = 0;
        let y = 0;

        this.input.on('pointerdown', pointer => {

            // const p = layer.tileToWorldXY(x, y);

            // this.add.rectangle(p.x, p.y, 4, 4, 0xff0000);

            const p1 = layer.tileToWorldXY(0, 0);
            const p2 = layer.tileToWorldXY(1, 0);
            const p3 = layer.tileToWorldXY(2, 0);
            const p4 = layer.tileToWorldXY(0, 1);
            const p5 = layer.tileToWorldXY(1, 1);
            const p6 = layer.tileToWorldXY(2, 1);
            this.add.rectangle(p1.x, p1.y, 4, 4, 0xff0000);
            this.add.rectangle(p2.x, p2.y, 4, 4, 0xff0000);
            this.add.rectangle(p3.x, p3.y, 4, 4, 0xff0000);
            this.add.rectangle(p4.x, p4.y, 4, 4, 0xff0000);
            this.add.rectangle(p5.x, p5.y, 4, 4, 0xff0000);
            this.add.rectangle(p6.x, p6.y, 4, 4, 0xff0000);


        });

        this.input.keyboard.on('keydown-SPACE', () => {

            const p = layer.tileToWorldXY(x, y);

            this.add.rectangle(p.x, p.y, 4, 4, 0xff0000);

        });

        /*
        this.input.keyboard.on('keydown-LEFT', () => {

            x--;
            t.setText(`x: ${x} y: ${y}`);

        });

        this.input.keyboard.on('keydown-RIGHT', () => {

            x++;
            t.setText(`x: ${x} y: ${y}`);

        });

        this.input.keyboard.on('keydown-DOWN', () => {

            y++;
            t.setText(`x: ${x} y: ${y}`);

        });

        this.input.keyboard.on('keydown-UP', () => {

            y--;
            t.setText(`x: ${x} y: ${y}`);

        });
        */
    }

    update (time, delta)
    {
        this.controls.update(delta);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    pixelArt: true,
    scene: Example
};

const game = new Phaser.Game(config);

Загрузка шестиугольной карты и создание слоя

Пример начинается с загрузки изометрического шестиугольного тайлсета и карты, созданной в Tiled. Ключевой момент — создание тайлового слоя, который и будет использоваться для преобразования координат.

const map = this.add.tilemap('map');
const tileset = map.addTilesetImage('hex-tiles', 'tiles');
const layer = map.createLayer('Tile Layer 1', tileset);

Метод createLayer возвращает объект TilemapLayer, который содержит геометрическую информацию о сетке. Именно у этого объекта мы будем вызывать tileToWorldXY.

Как работает tileToWorldXY для шестиугольников

Метод tileToWorldXY(column, row) принимает индексы столбца и строки тайла в сетке карты (не в пикселях!) и возвращает объект Phaser.Math.Vector2 с мировыми координатами (x, y) центра этого тайла.

Для шестиугольной сетки расчет этих координат сложнее, чем для прямоугольной, из-за смещения строк и столбцов. Phaser учитывает тип сетки (например, pointy-top или flat-top), указанный в JSON-файле карты из Tiled, и автоматически применяет правильную формулу.

const worldPoint = layer.tileToWorldXY(tileX, tileY);
// worldPoint.x, worldPoint.y — координаты центра тайла в мире игры

Визуализация: расставляем маркеры по клику

В примере при клике мыши (pointerdown) вызывается код, который наглядно демонстрирует результат работы tileToWorldXY для первых шести тайлов.

const p1 = layer.tileToWorldXY(0, 0);
const p2 = layer.tileToWorldXY(1, 0);
const p3 = layer.tileToWorldXY(2, 0);
const p4 = layer.tileToWorldXY(0, 1);
const p5 = layer.tileToWorldXY(1, 1);
const p6 = layer.tileToWorldXY(2, 1);

Для каждой рассчитанной точки создается маленький красный прямоугольник, который ставится по полученным мировым координатам. Это позволяет увидеть, где именно Phaser располагает центр каждого шестиугольного тайла. Вы увидите, что центры тайлов в соседних строках смещены относительно друг друга.

Интерактивное исследование с клавиатуры

Исходный код содержит закомментированный блок, который позволяет исследовать координаты в интерактивном режиме с помощью клавиш-стрелок и пробела.

// Переменные x и y хранят текущий исследуемый тайл
let x = 0;
let y = 0;

// Обработчик пробела ставит маркер в текущую позицию (x, y)
this.input.keyboard.on('keydown-SPACE', () => {
    const p = layer.tileToWorldXY(x, y);
    this.add.rectangle(p.x, p.y, 4, 4, 0xff0000);
});

// Обработчики стрелок меняют x и y и обновляют текст на экране
// this.input.keyboard.on('keydown-LEFT', () => { x--; });

Раскомментировав этот блок, вы сможете перемещаться по сетке и ставить маркеры в любую позицию, наблюдая за паттерном расположения центров тайлов. Это отличный способ понять геометрию вашей конкретной шестиугольной карты.

Управление камерой для навигации

Поскольку карта может быть большой, в примере реализовано плавное управление камерой с помощью Phaser.Cameras.Controls.SmoothedKeyControl. Это позволяет свободно перемещаться и приближаться для детального изучения.

const controlConfig = {
    camera: this.cameras.main,
    left: cursors.left,
    right: cursors.right,
    up: cursors.up,
    down: cursors.down,
    zoomIn: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q),
    zoomOut: this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E),
    acceleration: 0.02,
    drag: 0.0005,
    maxSpeed: 0.7
};
this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);

Контроль камеры обновляется в методе update. Это стандартный подход в Phaser для обработки постоянного ввода, такого как удержание клавиш.

Что попробовать дальше

Метод tileToWorldXY — это мост между логической сеткой вашей карты и визуальным миром игры. Понимание того, как он работает для шестиугольников, критически важно для реализации механик выделения юнитов, расчета путей (pathfinding) или определения тайла под курсором. Для экспериментов попробуйте

  1. Раскомментировать блок с клавишами-стрелками и пошагово исследовать карту
  2. Изменить тип шестиугольной ориентации в Tiled и посмотреть, как изменится расчет координат
  3. Написать обратный метод — определение индекса тайла (worldToTileXY) по клику мыши, используя полученные знания о центрах