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

В разработке игр часто нужны инструменты для быстрого прототипирования уровней прямо во время игры. Этот пример демонстрирует, как создать простой механизм "рисования" тайлами поверх загруженной тайловой карты. Вы сможете перемещать камеру стрелками, выбирать тайл, зажав Shift, и закрашивать им другие ячейки карты. Такой подход полезен для создания внутриигровых редакторов, инструментов для дизайнеров уровней или динамического изменения окружения.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    selectedTile;
    shiftKey;
    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' });

        // The first parameter is the name of the tileset in Tiled and the second parameter is the key
        // of the tileset image used when loading the file in preload.
        const tiles = this.map.addTilesetImage('Desert', 'tiles');

        // You can load a layer from the map using the layer name from Tiled ('Ground' in this case), or
        // by using the layer index. Since we are going to be manipulating the map, this needs to be a
        // dynamic tilemap layer, not a static one.
        const layer = this.map.createLayer('Ground', tiles, 0, 0);

        this.selectedTile = this.map.getTileAt(2, 3);

        this.marker = this.add.graphics();
        this.marker.lineStyle(2, 0x000000, 1);
        this.marker.strokeRect(0, 0, this.map.tileWidth, 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);

        this.shiftKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT);

        const help = this.add.text(16, 16, 'Left-click to paint.\nShift + Left-click to select tile.\nArrows to scroll.', {
            fontSize: '18px',
            padding: { x: 10, y: 5 },
            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)
        {
            if (this.shiftKey.isDown)
            {
                this.selectedTile = this.map.getTileAt(pointerTileX, pointerTileY);
            }
            else
            {
                this.map.putTileAt(this.selectedTile, pointerTileX, pointerTileY);
            }
        }
    }
}

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 загружаются необходимые ресурсы: изображение с набором тайлов (tileset) и сам файл карты в формате JSON, экспортированный из редактора Tiled. Важно использовать load.setBaseURL для указания базового пути к ресурсам на удалённом сервере.

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) на основе загруженных данных. Затем добавляется tileset с помощью addTilesetImage, где первый аргумент — имя набора тайлов из Tiled, а второй — ключ изображения. Слой карты создаётся как динамический (createLayer), что обязательно для его последующего изменения в реальном времени.

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.marker = this.add.graphics();
this.marker.lineStyle(2, 0x000000, 1);
this.marker.strokeRect(0, 0, this.map.tileWidth, this.map.tileHeight);

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

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. Сначала обновляется положение камеры. Затем вычисляются координаты указателя мыши в мире игры и конвертируются в координаты тайловой сетки с помощью методов 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);

Когда кнопка мыши нажата, проверяется состояние клавиши Shift. Если Shift зажат — тайл под курсором становится новым выбранным тайлом (getTileAt). Если Shift не нажат — выбранный тайл помещается в ячейку под курсором (putTileAt).

if (this.input.manager.activePointer.isDown)
{
    if (this.shiftKey.isDown)
    {
        this.selectedTile = this.map.getTileAt(pointerTileX, pointerTileY);
    }
    else
    {
        this.map.putTileAt(this.selectedTile, pointerTileX, pointerTileY);
    }
}

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

Вы создали простой, но мощный инструмент для редактирования тайловых карт в реальном времени. Этот фундамент можно расширить: добавить палитру тайлов, несколько слоёв, инструменты заливки или сохранения изменённой карты. Попробуйте реализовать отмену последнего действия (undo) или рисование прямоугольных областей.