В приложениях с виртуальным пространством и играх требуется как-то организовывать сцену. Одним из подходов хранения и организации являются тайловые карты (далее будет использоваться также калька с английского tilemap - тайлмапа). Основная идея тайловых карт в выделении палитры тайлов отдельно и сведении описания карты к двумерному массиву чисел - индексов тайлов из палитры. Но одного тайла в одной клетке часто бывает мало - мы хотим иметь возможность располагать на фоновом тайле какие-то элементы декора (деревья, пентаграммы, черепа). Для этого тайлмапы делают многослойными, слои отрисовываются в определённом порядке.
Пример карты, палитры и слоёв в редакторе Tiled |
Тайлмапы различаются формой тайлов и их раcположением. Помимо очевидного замощения квадратами, пространство также можно замостить шестиугольникам, изобразить тайлы в изометрической проекции, по разному задать координатные оси.
Примеры изометрических карт |
Для редактирования тайловых карт существует редактор с открытым исходным кодом Tiled. Он поддерживает все вышеперечисленные возможности, а также добавление метаниформации к слоям и тайлам, что позволяет размечать их в редакторе (например, можно добавить флаг о наличии коллайдера, его форму, месте спауна NPC и прочее).
В игровом движке Phaser 3 реализована поддержка тайловых карт, в том числе парсинг их из формата Tiled (json), CSV и массивов, включая расширения. С версии 3.50 помимо ортогональной карты поддерживаются также гексагональные, изометрические, straggered. Имеют интеграции с физическими движками Arcade и Matter и богатое API для взаимодействия и настройки отображения и поведения.
Работа с тайловыми картами вынесена в пространство имён Tilemap. Основными объектами для работы являются:
- Tilemap - хранит слои, общие настройки.
- TilemapLayer - хранит тайлы, позволяет манипулировать ими.
- Tile - отвечает за конкретный тайл, хранит его настройки - индекс в палитре, коллизии, отрисовка, пользовательские свойства.
- Tileset - хранит палитру и её настройки.
- 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
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 |
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 |
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;