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

Создание изометрических игр добавляет уникальный визуальный стиль, но усложняет логику взаимодействия. Классические методы получения тайлов по координатам экрана для ортогональных проекций не работают корректно. В этой статье мы разберем, как правильно получать изометрические тайлы при клике мышью, используя специальный API Phaser. Этот подход особенно полезен для стратегий, RPG или симуляторов с изометрической графикой, где важно точно определять, на какой элемент игрового мира кликнул пользователь.

Версия 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/isometric-sandbox-sheet.png');
        this.load.tilemapTiledJSON('map', 'assets/tilemaps/iso/tilemaps/sandbox.json');
    }

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

        const tileset = map.addTilesetImage('isometric-sandbox-sheet', 'tiles');

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

        // layer.setScale(4, 4);

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

        this.cameras.main.centerOn(0, 150);

        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);

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

            // const tile = layer.getTileAtWorldXY(pointer.worldX, pointer.worldY);

            // const x = pointer.worldX;
            // const y = pointer.worldY;

            // const tileXY = layer.worldToTileXY(pointer.worldX, pointer.worldY, true);

            // const tile = layer.getTileAt(tileXY.x, tileXY.y);

            const tile = layer.getIsoTileAtWorldXY(pointer.worldX, pointer.worldY, false);

            if (tile)
            {
                tile.tint = 0xff0000;
            }

            // const tileX = layer.worldToTileX(x, true);
            // const tileY = layer.worldToTileX(y, true);
            // console.log(tileXY, tileX, tileY);
            // console.log(tileXY);

        });
    }

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

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

const game = new Phaser.Game(config);

Настройка изометрической тайловой карты

Первым шагом является загрузка и создание изометрической карты. Обратите внимание на ключевые параметры конфигурации игры.

const config = {
    type: Phaser.AUTO,
    pixelArt: true,
    scene: Example
};

Флаг pixelArt: true важен для сохранения четкости пиксельной графики при масштабировании. В методе preload загружаются два основных ресурса: изображение тайлсета и JSON-файл карты, созданный в редакторе Tiled.

preload ()
{
    this.load.image('tiles', 'assets/tilemaps/iso/tilesets/isometric-sandbox-sheet.png');
    this.load.tilemapTiledJSON('map', 'assets/tilemaps/iso/tilemaps/sandbox.json');
}

В create создается объект карты, к нему привязывается загруженное изображение и создается слой для отрисовки.

create ()
{
    const map = this.add.tilemap('map');
    const tileset = map.addTilesetImage('isometric-sandbox-sheet', 'tiles');
    const layer = map.createLayer('Tile Layer 1', tileset);
}

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

Изометрические карты часто обширны, поэтому необходима плавная камера. Phaser предоставляет удобный контроллер SmoothedKeyControl.

Сначала создаем объект управления курсорными клавишами и настраиваем дополнительные клавиши для зума.

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);

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

Параметры acceleration, drag и maxSpeed позволяют тонко настроить ощущение от движения камеры, делая его плавным и отзывчивым.

Проблема получения тайла в изометрии

Основная сложность возникает при попытке определить, по какому тайлу кликнул игрок. В ортогональной проекции для этого достаточно перевести мировые координаты в координаты тайловой сетки с помощью worldToTileXY и получить тайл через getTileAt.

// Этот подход НЕ РАБОТАЕТ корректно для изометрических слоев:
const tileXY = layer.worldToTileXY(pointer.worldX, pointer.worldY, true);
const tile = layer.getTileAt(tileXY.x, tileXY.y);

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

Решение: метод getIsoTileAtWorldXY

Для изометрических тайловых слоев Phaser предоставляет специальный метод getIsoTileAtWorldXY. Он корректно обрабатывает математику изометрической проекции.

В обработчике события клика мыши используем этот метод:

this.input.on('pointerdown', pointer => {
    const tile = layer.getIsoTileAtWorldXY(pointer.worldX, pointer.worldY, false);
    if (tile)
    {
        tile.tint = 0xff0000;
    }
});

Метод принимает три аргумента: 1. worldX — координата X клика в мировом пространстве. 2. worldY — координата Y клика в мировом пространстве. 3. nonNull (опциональный) — если true, метод всегда вернет объект тайла, даже если клик был между тайлами (в этом случае вернется ближайший). В нашем примере используется false, поэтому при клике в пустое место вернется null.

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

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

Ключ к взаимодействию с изометрическим миром — использование правильного API. Метод getIsoTileAtWorldXY решает проблему точного определения тайла, абстрагируя разработчика от сложной математики изометрического преобразования. **Идеи для экспериментов:** 1. Попробуйте параметр nonNull: true в getIsoTileAtWorldXY. Как это меняет поведение при клике между тайлами? 2. Добавьте логику для выделения не одного тайла, а области (например, 3x3). 3. Используйте полученный тайл для изменения его свойств (например, tile.index для смены текстуры) или для запуска игровой логики (постройка здания, выбор юнита).