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

Работа с тайловыми картами — основа для создания уровней в 2D-играх. Однако часто возникают сложности с определением, на какой именно тайл кликнул игрок, особенно когда слои имеют разный масштаб, смещение или размер тайлов. Эта статья на примере официального теста Phaser показывает, как правильно получать мировые координаты тайла, учитывая все трансформации слоя. Вы научитесь точно определять взаимодействие игрока с любым элементом карты, что критически важно для механик строительства, выбора объектов или пошаговой стратегии.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    text;
    smallTileLayer;
    tileLayer2;
    offsetTileLayer;
    tileLayer;
    map;
    controls;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/features-test.json');

        this.load.spritesheet('coin', 'assets/sprites/coin.png', { frameWidth: 32, frameHeight: 32 });

        this.load.image('ground_1x1', 'assets/tilemaps/tiles/ground_1x1.png');
        this.load.image('walls_1x2', 'assets/tilemaps/tiles/walls_1x2.png');
        this.load.image('tiles2', 'assets/tilemaps/tiles/tiles2.png');
        this.load.image('dangerous-kiss', 'assets/tilemaps/tiles/dangerous-kiss.png');
    }

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

        const groundTiles = this.map.addTilesetImage('ground_1x1');
        const tiles2 = this.map.addTilesetImage('tiles2');
        const kissTiles = this.map.addTilesetImage('dangerous-kiss');

        this.tileLayer = this.map.createLayer('Tile Layer 1', groundTiles);
        this.offsetTileLayer = this.map.createLayer('Offset Tile Layer', tiles2, 50, 100);
        this.tileLayer2 = this.map.createLayer('Tile Layer 2', groundTiles);
        this.tileLayer2.setScale(0.75);
        this.smallTileLayer = this.map.createLayer('Small Tile Layer', kissTiles);
        this.smallTileLayer.setScale(2);

        this.selectLayer(this.tileLayer);

        this.input.keyboard.on('keydown_ONE', event =>
        {
            this.selectLayer(this.tileLayer);
        });

        this.input.keyboard.on('keydown_TWO', event =>
        {
            this.selectLayer(this.offsetTileLayer);
        });

        this.input.keyboard.on('keydown_THREE', event =>
        {
            this.selectLayer(this.tileLayer2);
        });

        this.input.keyboard.on('keydown_FOUR', event =>
        {
            this.selectLayer(this.smallTileLayer);
        });

        this.cameras.main.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);

        const cursors = this.input.keyboard.createCursorKeys();
        const controlConfig = {
            camera: this.cameras.main,
            left: cursors.left,
            right: cursors.right,
            up: cursors.up,
            down: cursors.down,
            speed: 0.5
        };
        this.controls = new Phaser.Cameras.Controls.FixedKeyControl(controlConfig);

        this.text = this.add.text(16, 16, '', {
            font: '20px Arial',
            backgroundColor: '#000000',
            fill: '#ffffff'
        });
        this.text.setScrollFactor(0);
    }

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

        const message = [ 'Press 1/2/3/4 to select layers' ];

        const worldPoint = this.input.activePointer.positionToCamera(this.cameras.main);

        message.push(`Mouse Position: ${worldPoint.x}, ${worldPoint.y}`);

        const tile = this.map.getTileAtWorldXY(worldPoint.x, worldPoint.y);
        if (tile)
        {
            message.push(`Tile Center Position: ${tile.getCenterX()}, ${tile.getCenterY()}`);
            message.push(`Tile Bounds: ${tile.getLeft()}, ${tile.getTop()} -> ${tile.getRight()}, ${tile.getBottom()}`);
        }

        this.text.setText(message);
    }

    selectLayer (layer)
    {
        this.map.setLayer(layer);
        this.tileLayer.alpha = 0.5;
        this.offsetTileLayer.alpha = 0.5;
        this.tileLayer2.alpha = 0.5;
        this.smallTileLayer.alpha = 0.5;
        layer.alpha = 1;
    }
}

// Visual test to make sure selecting tile world coordinates properly factor in scale, scroll and
// layer position. Expected behavior for tiles bigger/smaller than base size: you can only select
// them if you click on the bottom left of the graphic (the origin of where it is placed in the
// tilemap).

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

const game = new Phaser.Game(config);

Загрузка ресурсов и создание карты

В методе preload загружаются все необходимые ресурсы: JSON-файл карты, созданный в Tiled, наборы тайлов (tilesets) и спрайты. Ключевой метод для загрузки самой карты — this.load.tilemapTiledJSON.

В create происходит инициализация тайловой карты в сцене. Сначала создаётся объект карты, затем к нему добавляются наборы тайлов, и наконец — слои. Обратите внимание, что слои могут создаваться со смещением и разным масштабом.

this.map = this.add.tilemap('map');
const groundTiles = this.map.addTilesetImage('ground_1x1');
this.tileLayer = this.map.createLayer('Tile Layer 1', groundTiles);
this.offsetTileLayer = this.map.createLayer('Offset Tile Layer', tiles2, 50, 100);
this.smallTileLayer = this.map.createLayer('Small Tile Layer', kissTiles);
this.smallTileLayer.setScale(2);

Выбор активного слоя и управление камерой

В примере реализован механизм выбора активного слоя с клавиш 1-4. Метод selectLayer делает выбранный слой полностью непрозрачным, а остальные — полупрозрачными. Важно, что для корректной работы получения тайлов по координатам необходимо указать, с каким слоем мы работаем, с помощью this.map.setLayer(layer).

selectLayer (layer)
{
    this.map.setLayer(layer);
    this.tileLayer.alpha = 0.5;
    this.offsetTileLayer.alpha = 0.5;
    layer.alpha = 1;
}

Для перемещения по большой карте используется камера с контроллерами Phaser.Cameras.Controls.FixedKeyControl. Границы камеры устанавливаются в соответствии с размерами карты в пикселях.

this.cameras.main.setBounds(0, 0, this.map.widthInPixels, this.map.heightInPixels);
const controlConfig = {
    camera: this.cameras.main,
    left: cursors.left,
    right: cursors.right,
    up: cursors.up,
    down: cursors.down,
    speed: 0.5
};
this.controls = new Phaser.Cameras.Controls.FixedKeyControl(controlConfig);

Получение мировых координат и тайла под курсором

Сердце примера — метод update. Здесь вычисляется, над каким тайлом находится курсор мыши, с учётом прокрутки камеры, масштаба и смещения слоя.

1. **Преобразование координат указателя:** Координаты мыши (this.input.activePointer) преобразуются в мировые координаты относительно текущей камеры с помощью метода positionToCamera. Это важно, так как камера может быть смещена.

const worldPoint = this.input.activePointer.positionToCamera(this.cameras.main);

2. **Получение тайла по мировым координатам:** Для получения тайла используется метод карты getTileAtWorldXY. Ключевой момент: этот метод автоматически учитывает текущий активный слой (установленный через setLayer), его масштаб (scale) и позицию.

const tile = this.map.getTileAtWorldXY(worldPoint.x, worldPoint.y);

3. **Получение границ тайла:** Если тайл найден, можно получить его точные границы в мире с помощью методов getLeft, getTop, getRight, getBottom, а также координаты центра.

if (tile)
{
    message.push(`Tile Center Position: ${tile.getCenterX()}, ${tile.getCenterY()}`);
    message.push(`Tile Bounds: ${tile.getLeft()}, ${tile.getTop()} -> ${tile.getRight()}, ${tile.getBottom()}`);
}

Как отмечено в комментарии к исходнику, для тайлов, размер которых отличается от базового, выбор работает только при клике на их точку происхождения (origin), которая обычно находится в левом нижнем углу графики. Это поведение, заданное в Tiled.

Важные нюансы API Phaser

Для успешной работы с координатами запомните несколько моментов:

* setLayer vs layerIndex: Метод this.map.setLayer() принимает объект слоя (TilemapLayer), а не его индекс или имя. Это устанавливает слой как «текущий» для последующих операций с картой, таких как getTileAtWorldXY. * **Масштаб и смещение:** При создании слоя через createLayer можно сразу указать его смещение (x, y). Масштаб меняется методом layer.setScale(). Оба этих параметра автоматически учитываются при запросе тайлов по мировым координатам. * **Точка происхождения (origin) тайла:** Если ваш тайл в Tiled имеет размер, отличный от размера клетки сетки, или смещён, логика выбора (getTileAtWorldXY) будет основываться на его точке происхождения. Это может быть неочевидно для игрока. Для сложных случаев может потребоваться дополнительная проверка попадания в видимые границы спрайта.

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

Phaser предоставляет мощный и гибкий API для работы с координатами тайлов, который сам учитывает масштаб, смещение слоя и позицию камеры. Главное — правильно установить активный слой и преобразовать координаты указателя в мировую систему камеры. Для экспериментов попробуйте

  1. Создать слой с вращением (через setAngle) и проверить, как это повлияет на определение тайла
  2. Реализовать выделение прямоугольной области из тайлов для стратегической игры, используя getTileAtWorldXY в двух углах
  3. Добавить интерактивные объекты (например, монеты из спрайтшита) поверх тайлов и обрабатывать клик по ним, сравнивая логику с выбором тайла