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

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

Версия 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.tilemapTiledJSON('map', 'assets/tilemaps/maps/super-mario.json');
        this.load.image('SuperMarioBros-World1-1', 'assets/tilemaps/tiles/super-mario.png');
        this.load.bitmapFont('gothic', 'assets/fonts/bitmap/gothic.png', 'assets/fonts/bitmap/gothic.xml');
    }

    create ()
    {
        this.map = this.make.tilemap({ key: 'map' });
        const tileset = this.map.addTilesetImage('SuperMarioBros-World1-1');
        const layer = this.map.createLayer('World1', tileset, 0, 0);
        layer.setScale(2);

        // You can set collision on one tile index (11 = coin)
        this.map.setCollision(11);

        // Or, you can set collision on tiles with an index between two values (14 - 16 are blocks)
        this.map.setCollisionBetween(14, 16);

        // Or, you can set collision on all indexes within an array
        this.map.setCollision([ 20, 21, 22, 23, 24, 25, 27, 28, 29, 33, 39, 40 ]);

        // Or, you can set collision on everything in the map EXCEPT the indexes specified
        // map.setCollisionByExclusion([ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]);

        // Visualize the colliding tiles
        const debugGraphics = this.add.graphics();
        debugGraphics.setScale(2);
        this.map.renderDebug(debugGraphics);

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

        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,
            speed: 0.5
        };

        this.controls = new Phaser.Cameras.Controls.FixedKeyControl(controlConfig);
    }

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

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

const game = new Phaser.Game(config);

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

Вся работа начинается с загрузки данных карты и тайлсета. В методе preload мы загружаем JSON-файл карты, созданный в Tiled, и изображение с тайлами.

this.load.tilemapTiledJSON('map', 'assets/tilemaps/maps/super-mario.json');
this.load.image('SuperMarioBros-World1-1', 'assets/tilemaps/tiles/super-mario.png');

В create мы создаем объект тайловой карты с помощью this.make.tilemap(), добавляем к ней тайлсет и создаем слой. Обратите внимание на layer.setScale(2) — это масштабирование только визуальной части, логика столкновений будет обработана отдельно.

this.map = this.make.tilemap({ key: 'map' });
const tileset = this.map.addTilesetImage('SuperMarioBros-World1-1');
const layer = this.map.createLayer('World1', tileset, 0, 0);
layer.setScale(2);

Метод 1: Столкновение для одного индекса (`setCollision`)

Самый простой способ — задать столкновение для тайла с конкретным индексом. В нашем примере индекс 11 соответствует монетке.

this.map.setCollision(11);

Это означает, что все тайлы на карте, у которых индекс равен 11, станут твердыми объектами. Этот метод идеален для одиночных, уникальных типов тайлов, которые должны блокировать движение (например, ловушки или особые предметы).

Метод 2: Диапазон индексов (`setCollisionBetween`)

Часто тайлы, выполняющие одну функцию (например, блоки земли), идут в тайлсете подряд. Вместо перечисления каждого индекса можно задать диапазон.

this.map.setCollisionBetween(14, 16);

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

Метод 3: Произвольный массив индексов (`setCollision`)

Если твердые тайлы разбросаны по тайлсету неупорядоченно, их индексы можно передать в виде массива.

this.map.setCollision([ 20, 21, 22, 23, 24, 25, 27, 28, 29, 33, 39, 40 ]);

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

Метод 4: Исключение индексов (`setCollisionByExclusion`)

Иногда проще указать, какие тайлы НЕ должны сталкиваться, особенно если проходимых тайлов меньше, чем непроходимых. Закомментированная в примере строка делает столкновения для всех тайлов, кроме перечисленных в массиве.

// map.setCollisionByExclusion([ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]);

Этот метод может быть полезен для карт, где фон состоит из множества разных индексов, а проходимая область — из одного-двух (например, платформер с детализированным задним планом).

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

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

const debugGraphics = this.add.graphics();
debugGraphics.setScale(2);
this.map.renderDebug(debugGraphics);

Метод renderDebug нарисует контуры вокруг всех тайлов, у которых включена коллизия. Масштабирование setScale(2) нужно, чтобы графика совпадала с масштабированным визуальным слоем. По клику мыши видимость этой графики можно переключать, что очень удобно во время разработки.

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

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

Phaser предлагает четыре различных подхода к настройке столкновений на тайловой карте, что покрывает практически любые потребности в дизайне уровней. Выбор метода зависит от того, как организованы ваши тайлы в наборе: используйте setCollision для одиночных тайлов, setCollisionBetween для последовательных блоков, массив — для разрозненных индексов, а исключение — когда проходимых зон меньше. Для экспериментов попробуйте

  1. Скомбинировать несколько методов в одном уровне
  2. Назначить разным группам тайлов разные физические тела или группы столкновений
  3. Динамически включать и выключать столкновения у тайлов в runtime (например, для разрушаемых блоков)