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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    map;
    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.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, 'Click a tile to swap all instances of it with a sign.', {
            fontSize: '18px',
            padding: { x: 10, y: 5 },
            backgroundColor: '#000000',
            fill: '#ffffff'
        });
        help.setScrollFactor(0);
    }

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

        if (this.input.manager.activePointer.isDown)
        {
            const worldPoint = this.input.activePointer.positionToCamera(this.cameras.main);
            const tile = this.map.getTileAtWorldXY(worldPoint.x, worldPoint.y);

            // This will swap all of instances of the selected tile with a sign (tile id = 46). E.g. if
            // you clicked on a rock, all rocks would become signs and all signs would become rocks.
            this.map.swapByIndex(tile.index, 46);

            // You can also replace within a specific region (tileX, tileY, width, height):
            // map.replaceByIndex(tile.index, 46, 5, 5, 15, 15);
        }

    }
}

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

const game = new Phaser.Game(config);

Подготовка карты и камеры

В методе create() загружается тайловая карта из JSON-файла и создается слой. Камера настраивается на границы карты, чтобы можно было прокручивать обширный уровень.

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

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

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

Обработка клика мыши и получение тайла

В update() проверяется, нажата ли кнопка мыши. Важно преобразовать координаты указателя в мировые координаты с учетом камеры, так как камера может быть смещена.

if (this.input.manager.activePointer.isDown)
{
    const worldPoint = this.input.activePointer.positionToCamera(this.cameras.main);
    const tile = this.map.getTileAtWorldXY(worldPoint.x, worldPoint.y);
}

Метод positionToCamera() переводит экранные координаты (относительно canvas) в мировые. Затем getTileAtWorldXY() возвращает объект тайла под курсором. Без этого преобразования клик будет обработан неверно при скролле карты.

Массовый обмен тайлов методом swapByIndex

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

this.map.swapByIndex(tile.index, 46);

Здесь tile.index — индекс тайла, по которому кликнули (например, камень), а 46 — индекс тайла-знака. В результате все камни на карте станут знаками, а все знаки — камнями. Метод работает глобально, затрагивая каждый слой, где используются эти индексы.

Если нужно заменить тайлы только в определенной области, можно использовать replaceByIndex(), указав прямоугольную зону в тайловых координатах:

// map.replaceByIndex(tile.index, 46, 5, 5, 15, 15);

Практическое применение и нюансы

Такой подход эффективен для игровых механик, где требуется массовое изменение окружения. Например: - Замена разрушенных плит на обломки. - Активация переключателей, меняющих вид уровня. - "Закрашивание" территории, как в стратегиях.

Важно: метод swapByIndex() работает с индексами тайлов в наборе, а не с их координатами на карте. Индекс — это порядковый номер тайла в тайлсете (спрайте), начиная с 0. Узнать индекс можно через свойство tile.index или из редактора карт (Tiled).

Для визуальной обратной связи добавлен текст с подсказкой, который зафиксирован на экране (setScrollFactor(0)).

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

Методы swapByIndex() и replaceByIndex() дают мощный контроль над тайловыми картами в реальном времени. Экспериментируйте: создайте головоломку, где игрок меняет типы terrain, чтобы пройти, или реализуйте систему разрушения стен. Попробуйте комбинировать обмен тайлов с анимациями или частицами для более яркого эффекта.