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

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

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

Живой запуск

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

Исходный код


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

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        // this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/tileset-collision-property.json');
        this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/tileset-collision-property-v12.json');
        this.load.image('kenny_platformer_64x64', 'assets/tilemaps/tiles/kenny_platformer_64x64.png');
    }

    create ()
    {
        this.map = this.make.tilemap({ key: 'map' });
        const tileset = this.map.addTilesetImage('kenny_platformer_64x64');
        const layer = this.map.createLayer(0, tileset, 0, 0);
        layer.setScale(0.75);

        // Instead of setting collision by index, you can set collision via properties that you set up
        // in Tiled. You can assign properties to tiles in the tileset editor. Note: you need
        // to NOT have the collision editor or terrain editor open when you set them up.
        // This map has tiles with a boolean "collides" property, so we can do the following:
        layer.setCollisionByProperty({ collides: true });

        // You can also specify multiple properties, or an array of possible property values. The next
        // line would set any tile to collide if it has "collides" set to true or if it has a "type"
        // equal to 'lava' or 'water':
        // layer.setCollisionByProperty({ collides: true, type: [ 'lava', 'water' ] });

        // Visualize the colliding tiles
        this.debugGraphics = this.add.graphics();
        this.drawDebug();

        this.input.on('pointerdown', () =>
        {
            this.debugGraphics.visible = !this.debugGraphics.visible;
            if (this.debugGraphics.visible)
            {
                this.drawDebug();
            }
        });

        const help = this.add.text(16, 16, 'Click to toggle rendering collision information.', {
            fontSize: '18px',
            padding: { x: 10, y: 5 },
            backgroundColor: '#000000',
            fill: '#ffffff'
        });
        help.setScrollFactor(0);

        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 (time, delta)
    {
        this.controls.update(delta);
    }

    drawDebug ()
    {
        this.debugGraphics.clear();
        this.map.renderDebug(this.debugGraphics, { tileColor: null });
    }
}

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

const game = new Phaser.Game(config);

Загрузка карты и настройка слоя

Первым делом загружаем карту и тайлсет. Важно, что в Tiled тайлам были назначены пользовательские свойства. В данном примере используется свойство collides.

preload ()
{
    this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/tileset-collision-property-v12.json');
    this.load.image('kenny_platformer_64x64', 'assets/tilemaps/tiles/kenny_platformer_64x64.png');
}

После загрузки создаем тайловую карту, добавляем к ней тайлсет и создаем слой. Метод layer.setScale(0.75) уменьшает слой для наглядности.

create ()
{
    this.map = this.make.tilemap({ key: 'map' });
    const tileset = this.map.addTilesetImage('kenny_platformer_64x64');
    const layer = this.map.createLayer(0, tileset, 0, 0);
    layer.setScale(0.75);
}

Ключевой метод: setCollisionByProperty

Вместо того чтобы перечислять индексы тайлов для коллизий, мы используем метод setCollisionByProperty(). Он принимает объект, где ключи — это имена свойств из Tiled, а значения — условия для этих свойств.

В простейшем случае, если у тайла есть свойство collides со значением true, он станет коллизионным:

layer.setCollisionByProperty({ collides: true });

Метод также поддерживает более сложные условия. Можно проверять несколько свойств или массив допустимых значений. Например, следующий код сделает тайл коллизионным, если collides равен true ИЛИ свойство type равно 'lava' или 'water':

// layer.setCollisionByProperty({ collides: true, type: [ 'lava', 'water' ] });

**Важно:** Свойства должны быть назначены тайлам в редакторе тайлсета Tiled, при этом не должны быть открыты редакторы коллизий (Collision Editor) или террейнов (Terrain Editor).

Визуализация для отладки

Чтобы визуально убедиться, что коллизии назначены правильно, используем отладочную отрисовку.

Создаем графический объект debugGraphics и вызываем метод drawDebug, который использует this.map.renderDebug() для отрисовки контуров коллизионных тайлов.

this.debugGraphics = this.add.graphics();
this.drawDebug();

Функция drawDebug очищает графику и заново рисует отладочную информацию. Параметр { tileColor: null } означает, что заливаться будут только контуры коллизий.

drawDebug ()
{
    this.debugGraphics.clear();
    this.map.renderDebug(this.debugGraphics, { tileColor: null });
}

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

this.input.on('pointerdown', () =>
{
    this.debugGraphics.visible = !this.debugGraphics.visible;
    if (this.debugGraphics.visible)
    {
        this.drawDebug();
    }
});

Управление камерой и интерфейс

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

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

В методе update обновляем состояние контролов, чтобы камера двигалась.

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

Также добавим текстовую подсказку на экран, которая не будет скроллиться с камерой благодаря setScrollFactor(0).

const help = this.add.text(16, 16, 'Click to toggle rendering collision information.', {
    fontSize: '18px',
    padding: { x: 10, y: 5 },
    backgroundColor: '#000000',
    fill: '#ffffff'
});
help.setScrollFactor(0);

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

Использование setCollisionByProperty делает код более читаемым и тесно связывает логику игры с данными уровня, созданными в Tiled. Вы больше не зависите от хрупких индексов тайлов. **Идеи для экспериментов:** 1. Создайте в Tiled свойство damage: number и обрабатывайте его при столкновении игрока с тайлом. 2. Используйте массив значений для свойства type, чтобы одним вызовом метода задавать коллизии для группы тайлов (например, все жидкости). 3. Комбинируйте несколько свойств для создания сложных условий, таких как { isSolid: true, isSlippery: false }.