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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    cursors;
    pickups;
    player;
    layer;
    tileset;
    map;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('tiles', 'assets/tilemaps/tiles/gridtiles.png');
        this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/simple-map.json');
        this.load.image('player', 'assets/sprites/phaser-dude.png');
    }

    create ()
    {
        this.map = this.make.tilemap({ key: 'map', tileWidth: 32, tileHeight: 32 });
        this.tileset = this.map.addTilesetImage('tiles');
        this.layer = this.map.createLayer('Level1', this.tileset);

        this.map.setCollision([ 20, 48 ]);

        this.pickups = this.map.filterTiles(tile => tile.index === 82);

        this.player = this.add.rectangle(96, 96, 24, 38, 0xffff00);

        this.physics.add.existing(this.player);

        this.physics.add.collider(this.player, this.layer);

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

        this.cursors.up.on('down', () =>
        {
            if (this.player.body.blocked.down)
            {
                this.player.body.setVelocityY(-360);
            }
        }, this);
    }

    update ()
    {
        this.player.body.setVelocityX(0);

        if (this.cursors.left.isDown)
        {
            this.player.body.setVelocityX(-200);
        }
        else if (this.cursors.right.isDown)
        {
            this.player.body.setVelocityX(200);
        }

        this.physics.world.overlapTiles(this.player, this.pickups, this.hitPickup, null, this);
    }

    hitPickup (player, tile)
    {
        this.map.removeTile(tile, 29, false);

        this.pickups = this.map.filterTiles(tile => tile.index === 82);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 600 }
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

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

В методе preload загружаются необходимые ресурсы: изображение тайлов (tiles), JSON-файл карты из редактора Tiled (map) и спрайт игрока. Использование setBaseURL позволяет задать базовый путь для упрощения загрузки.

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('tiles', 'assets/tilemaps/tiles/gridtiles.png');
this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/simple-map.json');
this.load.image('player', 'assets/sprites/phaser-dude.png');

В методе create создается экземпляр тайловой карты. Ключевые параметры: ключ загруженной карты и размер тайла. Метод addTilesetImage связывает загруженное изображение тайлов с данными карты, а createLayer создает визуальный слой уровня.

this.map = this.make.tilemap({ key: 'map', tileWidth: 32, tileHeight: 32 });
this.tileset = this.map.addTilesetImage('tiles');
this.layer = this.map.createLayer('Level1', this.tileset);

Настройка физики и столкновений

Чтобы тайлы стали физическими препятствиями, используется метод setCollision. В него передается массив индексов тайлов, которые должны блокировать движение. В данном примере это тайлы с индексами 20 и 48.

this.map.setCollision([ 20, 48 ]);

Игрок представлен не спрайтом, а прямоугольником (add.rectangle). Это демонстрирует, что для физики важна геометрия объекта, а не его внешний вид. Метод this.physics.add.existing добавляет к объекту тело Arcade Physics.

this.player = this.add.rectangle(96, 96, 24, 38, 0xffff00);
this.physics.add.existing(this.player);

Коллайдер this.physics.add.collider связывает тело игрока со слоем карты. Теперь физический движок автоматически будет обрабатывать столкновения между ними, не давая игроку проходить сквозь тайлы-препятствия.

this.physics.add.collider(this.player, this.layer);

Управление и механика прыжка

Управление реализуется через объект cursors. Обратите внимание на механику прыжка: он возможен только тогда, когда игрок стоит на земле. Это проверяется через свойство this.player.body.blocked.down.

this.cursors = this.input.keyboard.createCursorKeys();
this.cursors.up.on('down', () =>
{
    if (this.player.body.blocked.down)
    {
        this.player.body.setVelocityY(-360);
    }
}, this);

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

this.player.body.setVelocityX(0);
if (this.cursors.left.isDown)
{
    this.player.body.setVelocityX(-200);
}
else if (this.cursors.right.isDown)
{
    this.player.body.setVelocityX(200);
}

Сбор предметов с помощью overlapTiles

Предметы для сбора (pickups) — это тайлы с определенным индексом (82). Метод filterTiles проходит по всем тайлам карты и возвращает массив тех, которые удовлетворяют условию в callback-функции.

this.pickups = this.map.filterTiles(tile => tile.index === 82);

В update каждый кадр проверяется пересечение тела игрока с массивом тайлов-предметов. Для этого используется метод this.physics.world.overlapTiles. При обнаружении пересечения вызывается функция обратного вызова hitPickup.

this.physics.world.overlapTiles(this.player, this.pickups, this.hitPickup, null, this);

В функции hitPickup собранный тайл удаляется с карты с помощью map.removeTile. Второй аргумент (29) — это индекс тайла, на который будет заменен удаленный (обычно это пустой тайл). После удаления необходимо заново получить актуальный массив предметов, так как исходный массив не обновляется автоматически.

hitPickup (player, tile)
{
    this.map.removeTile(tile, 29, false);
    this.pickups = this.map.filterTiles(tile => tile.index === 82);
}

Конфигурация игры и физики

Конфигурационный объект игры включает настройки физического движка Arcade. Здесь задана гравитация по оси Y, которая заставляет игрока падать, если под ним нет опоры. Тип рендерера установлен в Phaser.AUTO.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            gravity: { y: 600 }
        }
    },
    scene: Example
};

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

Пример демонстрирует базовый, но мощный пайплайн для работы с тайловыми картами в Phaser: от загрузки и отрисовки до настройки сложных физических взаимодействий. Для экспериментов попробуйте изменить индексы тайлов для столкновений и предметов, добавить анимацию при сборе pickups или реализовать разные типы препятствий (например, скользкий лед или шипы, наносящие урон).