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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    map;
    marker;
    controls;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('tiles', 'assets/tilemaps/tiles/tmw_desert_spacing.png');
        this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/desert.json');
    }

    create ()
    {
        this.map = this.make.tilemap({ key: 'map' });
        const tiles = this.map.addTilesetImage('Desert', 'tiles');
        const layer = this.map.createLayer('Ground', tiles, 0, 0);

        this.marker = this.add.graphics();
        this.marker.lineStyle(2, 0x000000, 1);
        this.marker.strokeRect(0, 0, 6 * this.map.tileWidth, 6 * this.map.tileHeight);

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

        const help = this.add.text(16, 16, 'Left-click to shuffle tiles.', {
            fontSize: '18px',
            padding: { x: 10, y: 5 },
            font: '20px Arial',
            backgroundColor: '#000000',
            fill: '#ffffff'
        });
        help.setScrollFactor(0);
    }

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

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

        // Rounds down to nearest tile
        const pointerTileX = this.map.worldToTileX(worldPoint.x);
        const pointerTileY = this.map.worldToTileY(worldPoint.y);

        // Snap to tile coordinates, but in world space
        this.marker.x = this.map.tileToWorldX(pointerTileX);
        this.marker.y = this.map.tileToWorldY(pointerTileY);

        if (this.input.manager.activePointer.isDown)
        {
            // Shuffle the tiles within an area
            this.map.shuffle(pointerTileX, pointerTileY, 6, 6);
        }

    }
}

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

const game = new Phaser.Game(config);

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

Вся работа начинается в методах preload и create нашего класса сцены. На этом этапе мы загружаем изображение с набором тайлов и файл карты в формате JSON, созданный в Tiled.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('tiles', 'assets/tilemaps/tiles/tmw_desert_spacing.png');
    this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/desert.json');
}

После загрузки в create() мы создаем саму карту (this.map), привязываем к ней изображение тайлов и создаем слой Ground. Обратите внимание, что первым аргументом в addTilesetImage передается имя тайлсета из JSON-файла карты (Desert), а вторым — ключ загруженного изображения.

create ()
{
    this.map = this.make.tilemap({ key: 'map' });
    const tiles = this.map.addTilesetImage('Desert', 'tiles');
    const layer = this.map.createLayer('Ground', tiles, 0, 0);
}

Визуальный маркер и управление камерой

Чтобы игрок видел область, которая будет изменяться, мы создаем графический объект this.marker. Это прямоугольник, обведенный черной линией, размером 6x6 тайлов. Его позиция будет обновляться в update().

this.marker = this.add.graphics();
this.marker.lineStyle(2, 0x000000, 1);
this.marker.strokeRect(0, 0, 6 * this.map.tileWidth, 6 * this.map.tileHeight);

Поскольку карта может быть большой, мы настраиваем камеру и создаем для нее простое управление с клавиатуры. Камера получает границы, равные размеру карты в пикселях, а объект this.controls связывает стрелки клавиатуры с ее перемещением.

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

Сердце логики: метод update и преобразование координат

Вся магия происходит в методе update, который вызывается каждый кадр. Сначала мы обновляем положение камеры в соответствии с нажатыми клавишами.

this.controls.update(delta);

Затем нам нужно понять, над каким тайлом на карте находится курсор мыши. Но координаты мыши (this.input.activePointer) изначально даны относительно окна игры. Чтобы перевести их в мировые координаты с учетом прокрутки камеры, используется метод positionToCamera(). А методы worldToTileX и worldToTileY преобразуют мировые координаты в индексы тайлов на карте.

const worldPoint = this.input.activePointer.positionToCamera(this.cameras.main);
const pointerTileX = this.map.worldToTileX(worldPoint.x);
const pointerTileY = this.map.worldToTileY(worldPoint.y);

Полученные индексы тайлов мы переводим обратно в мировые координаты (уже округленные до целого тайла) и устанавливаем в них позицию маркера.

this.marker.x = this.map.tileToWorldX(pointerTileX);
this.marker.y = this.map.tileToWorldY(pointerTileY);

Динамическое изменение карты методом shuffle

Ключевой момент — изменение карты по клику. Мы проверяем, нажата ли кнопка мыши, и если да, то вызываем метод shuffle у объекта тайлмапы.

if (this.input.manager.activePointer.isDown)
{
    this.map.shuffle(pointerTileX, pointerTileY, 6, 6);
}

Метод this.map.shuffle(pointerTileX, pointerTileY, 6, 6); делает следующее: он берет область размером 6x6 тайлов, начиная с тайла под курсором (pointerTileX, pointerTileY), и случайным образом перемешивает все индексы тайлов внутри этой области. Внешний вид тайлов (текстура) меняется соответственно их новым индексам. Это создает мгновенный эффект "перетасовки" земли, не затрагивая другие области карты.

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

Мы разобрали, как динамически изменять тайловую карту в Phaser с помощью метода shuffle. Этот пример — отличная основа для экспериментов. Попробуйте изменить размер перемешиваемой области в зависимости от силы клика, используйте разные алгоритмы (например, замену тайлов на конкретный индекс для создания "выкопанных" зон) или запускайте shuffle по таймеру для создания анимированных эффектов кипящей лавы или волнующейся воды. Главное — вы теперь знаете, как переводить координаты курсора в позицию на тайлмапе и применять к ней мощные методы API.