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

Работа с гексагональными сетками — мощный инструмент для создания стратегий и ролевых игр. Однако их геометрия сложнее обычной прямоугольной. В примере демонстрируется, как Phaser позволяет получить точные координаты углов любого гексагонального тайла на карте. Это знание — ключ к реализации механик выделения, расчёта пути (pathfinding) или отрисовки пользовательских границ вокруг ячеек.

Версия 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.setAlpha(0.2);

        // 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 g = this.add.graphics();

        g.lineStyle(2, 0x00ff00);

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

        let x = 0;
        let y = 0;

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

            const corners = layer.getTileCorners(x, y);

            g.moveTo(corners[0].x, corners[0].y);

            g.beginPath();

            corners.forEach(corner => {

                g.lineTo(corner.x, corner.y);

            });

            g.closePath();
            g.strokePath();

        });

        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-A', () => {

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

        });

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

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

        });

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

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

        });

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

            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);
layer.setAlpha(0.2);

Прозрачность слоя (setAlpha(0.2)) установлена для того, чтобы позже нарисованные поверх зелёные контуры были хорошо видны. Управление камерой реализовано через SmoothedKeyControl, что позволяет плавно перемещаться и зумировать карту клавишами-стрелками и клавишами Q/E.

Ядро примера: метод getTileCorners

Основная логика сосредоточена в обработчике клика мыши. По клику для текущих координат тайла (x, y) вызывается ключевой метод.

const corners = layer.getTileCorners(x, y);

Метод getTileCorners() объекта тайлового слоя (layer) возвращает массив объектов. Каждый объект содержит координаты `xиy` одного из углов запрошенного гексагонального тайла. Для гексагона их будет шесть. Эти координаты уже пересчитаны в мировую систему координат (world coordinates) с учётом положения и масштаба слоя.

Визуализация результата

Полученный массив углов используется для отрисовки контура гексагона с помощью Graphics API.

g.moveTo(corners[0].x, corners[0].y);
g.beginPath();
corners.forEach(corner => {
    g.lineTo(corner.x, corner.y);
});
g.closePath();
g.strokePath();

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

Для удобства исследования в примере реализовано управление индексами тайла (x, y) с клавиатуры (WASD) и два вспомогательных действия.

const p = layer.tileToWorldXY(x, y);
this.add.rectangle(p.x, p.y, 4, 4, 0xff0000);

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

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

Методы getTileCorners() и tileToWorldXY() предоставляют разработчику точный контроль над геометрией гексагональной сетки. Это фундамент для множества игровых механик. Для экспериментов попробуйте: изменить масштаб слоя (раскомментировав layer.setScale), чтобы увидеть, как методы корректно работают с трансформациями; реализовать выделение тайла под курсором мыши; или использовать полученные углы для расчёта столкновений нестандартной формы.