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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    showDebug;
    player;
    debugGraphics;
    cursors;
    text;
    kenny64x64Layer;
    ground32x32Layer;
    tree32x64Layer;
    map;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/multiple-tile-sizes-collision.json');

        this.load.image('ground_1x1', 'assets/tilemaps/tiles/ground_1x1.png');
        this.load.image('walls_1x2', 'assets/tilemaps/tiles/walls_1x2.png');
        this.load.image('kenny_platformer_64x64', 'assets/tilemaps/tiles/kenny_platformer_64x64.png');
        this.load.image('dangerous-kiss', 'assets/tilemaps/tiles/dangerous-kiss.png');

        this.load.image('player', 'assets/sprites/phaser-dude.png');
    }

    create ()
    {
        this.map = this.add.tilemap('map');

        const groundTiles = this.map.addTilesetImage('ground_1x1', 'ground_1x1', 32, 32);
        const kennyTiles = this.map.addTilesetImage('kenny_platformer_64x64', 'kenny_platformer_64x64', 64, 64);
        const treeTiles = this.map.addTilesetImage('walls_1x2', 'walls_1x2', 32, 64);

        this.kenny64x64Layer = this.map.createLayer('Kenny 64x64 Layer', kennyTiles);
        this.ground32x32Layer = this.map.createLayer('Ground 32x32 Layer', groundTiles);
        this.tree32x64Layer = this.map.createLayer('Tree 32x64 Layer', treeTiles);

        this.ground32x32Layer.setCollisionByExclusion([ -1 ]);
        this.tree32x64Layer.setCollisionByExclusion([ -1 ]);
        this.kenny64x64Layer.setCollision([ 73 ]);

        this.player = this.physics.add.sprite(700, 100, 'player').setBounce(0.1);

        this.physics.add.collider(this.player, this.tree32x64Layer);
        this.physics.add.collider(this.player, this.ground32x32Layer);
        this.physics.add.collider(this.player, this.kenny64x64Layer);

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

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

        this.input.keyboard.on('keydown-C', event =>
        {
            this.showDebug = !this.showDebug;
            this.drawDebug();
        });

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

        this.text = this.add.text(16, 16, '', {
            fontSize: '20px',
            fill: '#ffffff'
        });
        this.text.setScrollFactor(0);
        this.updateText();
    }

    update (time, delta)
    {
        // Horizontal movement
        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);
        }

        // Jumping
        if ((this.cursors.space.isDown || this.cursors.up.isDown) && this.player.body.onFloor())
        {
            this.player.body.setVelocityY(-300);
        }
    }

    drawDebug ()
    {
        this.debugGraphics.clear();

        if (this.showDebug)
        {
            this.ground32x32Layer.renderDebug(this.debugGraphics, {
                tileColor: null, // Non-colliding tiles
                collidingTileColor: new Phaser.Display.Color(211, 36, 255, 100), // Colliding tiles
                faceColor: new Phaser.Display.Color(211, 36, 255, 255) // Colliding face edges
            });

            this.kenny64x64Layer.renderDebug(this.debugGraphics, {
                tileColor: null, // Non-colliding tiles
                collidingTileColor: new Phaser.Display.Color(244, 255, 36, 100), // Colliding tiles
                faceColor: new Phaser.Display.Color(244, 255, 36, 255) // Colliding face edges
            });

            this.tree32x64Layer.renderDebug(this.debugGraphics, {
                tileColor: null, // Non-colliding tiles
                collidingTileColor: new Phaser.Display.Color(36, 255, 237, 100), // Colliding tiles
                faceColor: new Phaser.Display.Color(36, 255, 237, 255) // Colliding face edges
            });
        }

        this.updateText();
    }

    updateText ()
    {
        this.text.setText(
            `Arrow keys to move. Space to jump\nPress "C" to toggle debug visuals: ${this.showDebug ? 'on' : 'off'}`
        );
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 576,
    backgroundColor: '#00000',
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: { gravity: { y: 400 }, debug: true }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Загрузка разноразмерных тайлсетов

Ключевой момент — правильное указание размеров тайла при добавлении тайлсета на карту. В методе create() мы видим три вызова this.map.addTilesetImage(), каждый с разными параметрами ширины и высоты.

const groundTiles = this.map.addTilesetImage('ground_1x1', 'ground_1x1', 32, 32);
const kennyTiles = this.map.addTilesetImage('kenny_platformer_64x64', 'kenny_platformer_64x64', 64, 64);
const treeTiles = this.map.addTilesetImage('walls_1x2', 'walls_1x2', 32, 64);

Первый аргумент — имя тайлсета из JSON-файла карты Tiled, второй — ключ загруженного изображения. Третий и четвертый аргументы — ширина и высота одного тайла в пикселях. Без этих данных Phaser не сможет корректно расположить тайлы на карте. После этого создаются слои, каждый со своим тайлсетом.

Настройка коллизий для разных слоев

Phaser предлагает гибкие методы для задания коллизий. В примере используются два подхода: setCollisionByExclusion() и setCollision().

this.ground32x32Layer.setCollisionByExclusion([ -1 ]);
this.tree32x64Layer.setCollisionByExclusion([ -1 ]);
this.kenny64x64Layer.setCollision([ 73 ]);

Метод setCollisionByExclusion([ -1 ]) делает коллизионными все тайлы на слое, кроме тайлов с индексом -1 (пустых ячеек). Это удобно для сплошных слоев, типа земли.

Метод setCollision([ 73 ]) задает коллизии только для конкретного индекса тайла (в данном случае, 73). Это полезно, если на слое только некоторые объекты (например, шипы или монеты) должны сталкиваться. После настройки слоев, коллайдеры регистрируются в физическом движке для спрайта игрока.

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

Для отладки коллизий в Phaser есть мощный инструмент — метод renderDebug() у слоя тайлов. В примере его вызов привязан к клавише `C`. Каждому слою задается свой цвет для наглядности.

this.ground32x32Layer.renderDebug(this.debugGraphics, {
    tileColor: null,
    collidingTileColor: new Phaser.Display.Color(211, 36, 255, 100),
    faceColor: new Phaser.Display.Color(211, 36, 255, 255)
});

tileColor — цвет неколлизионных тайлов (null означает не рисовать). collidingTileColor — цвет заливки коллизионных тайлов (четвертый параметр — альфа-канал). faceColor — цвет граней (краев) коллизионных тайлов. Раздельная настройка помогает быстро оценить, какие тайлы и с каких сторон участвуют в столкновениях.

Физика и управление игроком

Движение игрока реализовано в методе 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.player.body.onFloor()). Это предотвращает "двойной прыжок" в воздухе. Камера следует за игроком в пределах всей карты, границы которой заданы через this.map.widthInPixels и this.map.heightInPixels.

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

Использование тайлсетов разного размера открывает огромные возможности для дизайна уровней, не ограничивая разработчика одной сеткой. Главное — корректно указать размеры тайлов и настроить коллизии под конкретные нужды каждого слоя. Для экспериментов попробуйте: добавить новый слой с тайлами 16x16, изменить индекс коллизии для слоя Kenny, создать несколько типов коллизий (например, смертельные шипы и прыжковые платформы) на одном слое, используя разные индексы в setCollision().