Тайловые карты в Phaser 3

 

В приложениях с виртуальным пространством и играх требуется как-то организовывать сцену. Одним из подходов хранения и организации являются тайловые карты (далее будет использоваться также калька с английского tilemap - тайлмапа). Основная идея тайловых карт в выделении палитры тайлов отдельно и сведении описания карты к двумерному массиву чисел - индексов тайлов из палитры. Но одного тайла в одной клетке часто бывает мало - мы хотим иметь возможность располагать на фоновом тайле какие-то элементы декора (деревья, пентаграммы, черепа). Для этого тайлмапы делают многослойными, слои отрисовываются в определённом порядке.

Пример карты, палитры и слоёв в редакторе Tiled

Тайлмапы различаются формой тайлов и их раcположением. Помимо очевидного замощения квадратами, пространство также можно замостить шестиугольникам, изобразить тайлы в изометрической проекции, по разному задать координатные оси.

Примеры изометрических карт

Для редактирования тайловых карт существует редактор с открытым исходным кодом Tiled. Он поддерживает все вышеперечисленные возможности, а также добавление метаниформации к слоям и тайлам, что позволяет размечать их в редакторе (например, можно добавить флаг о наличии коллайдера, его форму, месте спауна NPC и прочее).

В игровом движке Phaser 3 реализована поддержка тайловых карт, в том числе парсинг их из формата Tiled (json), CSV и массивов, включая расширения. С версии 3.50 помимо ортогональной карты поддерживаются также гексагональные, изометрические, straggered. Имеют интеграции с физическими движками Arcade и Matter и богатое API для взаимодействия и настройки отображения и поведения.

Работа с тайловыми картами вынесена в пространство имён Tilemap. Основными объектами для работы являются:

  • Tilemap - хранит слои, общие настройки.
  • TilemapLayer - хранит тайлы, позволяет манипулировать ими.
  • Tile - отвечает за конкретный тайл, хранит его настройки - индекс в палитре, коллизии, отрисовка, пользовательские свойства.
  • Tileset - хранит палитру и её настройки.

Создать карту можно либо пустую, настроив только её размеры, либо передать массив с индексами тайлов нулевого слоя, либо передать ключ заранее скачанного json-файла с описанием карты в формате tiled. После создания карты необходимо добавить тайлсеты и создать слои. Для создания слоёв есть разные методы: 
  • createLayer(name, tileset, ...) - принимает имя существующего в json-файле слоя тайлов, если такого нет, то возвращает null;
  • createBlankLayer(name, tileset, ...) - создаёт новый пустой слой.
Пример кода создания тайлмапы с начальным слоем из массива и добавлением пустого слоя:

const map = scene.make.tilemap({

            tileHeight: 32,

            tileWidth: 32,

            data: arrayOfArrayOfNumbers,

        });

const tileset = map.addTilesetImage('main', 'tiles', 32, 32, 2, 6); // 2 и 6 - margin и spacing

const backgroundLayer = map.createBlankLayer('test', tileset, 0, 0);

backgroundLayer.fill(sandTileIndex); // sandTileIndex = 638, подсмотрели в Tiled

const mainLayer = map.createLayer(0, tileset); // 0 - имя слоя, который был передан в конфиге как data


При получении слоя мы можем менять его содержимое. Например, мы хотим добавить появление креста на месте смерти NPC. Для этого достаточно подписаться на его уничтожение, преобразовать координаты из пикселей в тайловые и установить там нужный индекс:


const layerCaves = map.createBlankLayer('caves', tileset);

wizard.on('destroy', function (this: EvilWizard) {

const pos = layerCaves.worldToTileXY(this.x, this.y);

layerCaves.putTileAt(caveTileIndex, pos.x, pos.y);

});


Вот панк был, и вот его не стало...


Помимо установки значения тайлов, мы можем менять их свойства. Создадим с помощью тайлмапы интерфейс с отображением жизней, допустив, что мы хотим перекрашивать сердечки в зависимости от наложенного на игрока эффекта. Создадим тайлсет с белыми сердечками.
Сердечки без tint
А затем создадим карту и будем обновлять её каждый раз, когда hp игрока меняется:

const uiMap = this.make.tilemap({ tileWidth: 32, tileHeight: 32, width: 10, height: 1 });

const tileset = uiMap.addTilesetImage('ui', 'ui', 32, 32, 0, 2);

const ui = uiMap.createBlankLayer('hp', tileset);

// перекрасим все тайлы в красный

ui.getTilesWithin().forEach(tile => (tile.tint = 0xff0f0f));

// сдвинем за пределы карты - потом будем смотреть туда отдельной камерой

ui.setPosition(0, this.height);

const hp = new Array(player.hp / 2).fill(2);

ui.putTilesAt(hp, 0, 0);

player.addListener('hp', (newHP: number) => {

for (let i = 0; i < hp.length; i++) {

hp[i] = newHP < 1 ? 0 : Math.min(newHP, 2);

newHP -= 2;

}

ui.putTilesAt(hp, 0, 0);

});


Сердечки с tint = #FF0F0F

ui.getTilesWithin().forEach(tile => (tile.tint = 0xff0f0f)); - благодаря этой строчке каждое сердечко покраснело, потому что белый цвет умножился на красный. Мы можем заменять tint на другие цвета для демонстрации наложенных эффектов - зелёный для отравления, синий для заморозки и другие, при этом нам не понадобится дорисовывать тайлсет для каждого эффекта.

Для работы с физикой есть много методов, но в случае аркадной физики нам всегда понадобится добавлять коллайдер для слоя this.physics.add.collider(npcGroup, layer) и отдельно в слое указать, тайлы с какими индексами коллизируют:

layer.setCollision([номера тайлов, означающих стены]);

layer.setCollisionBetween(start: number, stop: number, collides?: boolean);

layer.setCollisionByProperty(properties: object, collides?: boolean);

layer.setCollisionByExclusion(indexes: number[], collides?: boolean);

layer.setCollisionFromCollisionGroup(collides?: boolean, recalculateFaces);

Или в конкретном тайле можно указать отдельно для каждой грани, является ли она проходимой или нет:

tile.setCollision(left: boolean, right: boolean, up: boolean, down: boolean).

Для отладки коллизий есть методы отрисовки коллайдеров слоя и карты.

map.renderDebug(scene.add.graphics(), colorsSettings, layer);

Без и с отображением отладочной информации

Также есть ряд вспомогательных методов: для заполнения случайными тайлами с учётом распределения, для поиска и фильтрации тайлов, для создания из тайлов спрайтов, для замены спрайтов по индексу и многое другое. Но чего важного пока нет без плагинов: анимаций.

layer.createFromTiles(indexes: number[], replacements: number[], spriteConfig?): Sprite[];

layer.filterTiles(callback: Function, context?: object

 tileX?: number, tileY?: number

 width?: number, height?: number

 filteringOptions?: Phaser.Types.Tilemaps.FilteringOptions): Phaser.Tilemaps.Tile[];

layer.randomize(tileX?: number, tileY?: number, width?: number, height?: number, indexes?: number[]);

layer.shuffle(tileX?: number, tileY?: number, width?: number, height?: number): this;

layer.replaceByIndex(findIndex, newIndex, tileX?, tileY?, width?, height?);layer.swapByIndex(tileA, tileB, tileX?, tileY?, width?, height?): this;


В данной статье мы рассмотрели базовый набор методов для создания, модификации и интеграции тайловых карт в Phaser 3.