Что такое тайловые карты?

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

Для наших целей мы будем использовать тайловые карты в виде сетки, в которой мы размещаем растровые изображения квадратной формы в каждой ячейке, чтобы собрать подземелье и необходимые игровые элементы. Если вы когда-либо играли в ролевую игру с ручкой и бумагой, такую как Dungeons & Dragons, и вам приходилось рисовать карту с помощью миллиметровой бумаги, вы заметите много общего между этим и тем, что будет делать наше программное обеспечение для этой главы.

Подземелье, используемое для проекта roguelike по книге, в конечном итоге будет иметь несколько уровней. Каждый уровень будет представлять собой сетку с тайловыми картами, в которой стены, комнаты, коридоры и другие элементы будут размещены для создания узнаваемого подземелья в стиле RPG. Давайте научимся рисовать плитки.

Рисование тайловой карты

Исходный код этого раздела находится в папке chapter-3/example-1-simple-tilemap или здесь. Файл HTML такой же как и в прошлых примерах: он просто загружает Phaser и наш файл game.js, в котором на самом деле происходят все интересные моменты этого раздела.

Предварительная загрузка таблицы спрайтов

Текстурный атлас (texture atlas или spritesheet — спрайт-лист) представляет собой файл изображения, который сочетает в себе множество различных графических объектов в одном файле. Веб-игры, как правило, используют их, потому что они позволяют одним файлом передать все необходимые изображения на компьютер игрока.

В нашем примере используется простой текстурный атлас, в котором все изображения имеют одинаковый размер и размещаются рядом друг с другом, как хорошо организованный набор марок на странице. Например, предположим, что каждое изображение имеет размер 10 на 10 пикселей и у вас есть десять изображений в двух строках в спрайт-листе — это означает, что у вас есть один файл изображения размером 20 на 50 пикселей со всеми вашими изображениями внутри. Многие часто называют эти изображения, содержащиеся в текстурном атласе, спрайтами. Однако вы также увидите, что это же слово применяется к элементам игры, которые движутся по экрану, что может сбивать с толку, если вы новичок в разработке игр. Мы будем называть их тайлами или плитками, если они не относятся к игровым элементам, которые представляют собой движущиеся объекты, такие как игрок или монстры. Тем не менее, все они взяты из одного файла.

Наша таблица спрайтов взята из свободно доступного пакета игровых изображений Кенни и выглядит великолепно (рис. 3-1 ). Рисунок 3-1. Образец текстурного атласа.

Как видно, в нем много разных плиток, и мы сможем комбинировать их для карты для игры в жанре roguelike. Каждое изображение в этой таблице тайлов представляет собой квадрат шириной 16 пикселей. Они разделены промежутками в 1 пиксель. Исходный код для предварительной загрузки таблицы спрайтов требует передачи всей этой информации. Теперь в файле game.js функция preload() выглядит так:

preload: function () {
    this.load.spritesheet(
        'tiles',
        'assets/colored.png',
        {
            frameWidth: 16,
            frameHeight: 16,
            spacing: 1
        });
},

Как и другие методы, функция preload() использует функцию из пространства имен this.load.* для загрузки спрайт-листа. Аргументы этой функции:

  • ключ, который мы будем позже использовать для обращения к этому текстурному атласу;
  • путь к изображению текстурного атласа;
  • объект конфигураци, в котором можно установить множество необязательных параметров, а мы просто устанавливаем размеры каждого изображения на листе и промежуток между ними.

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

Базовая карта тайлов

Чтобы отображать сетку тайловой карты, мы будем использовать двумерный массив, где каждый элемент — это число, которое соответствует тайлу в нашем текстурном атласе. Подземелье 5x5 с текстурированными стенами с каждой стороны и свободным пространством в центре будет задаваться:

let dungeon = [
    [1,1,1,1,1],
    [1,0,0,0,1],
    [1,0,0,0,1],
    [1,0,0,0,1],
    [1,1,1,1,1]
]

И это приведет к подземелью, которое выглядит так: Рисунок 3-2. Пример простого подземелья 5x5.

Если вы проверите спрайт-лист, то увидите, что область пола являются первым изображением на листе, а область стен — вторым изображением. Поскольку массивы в JavaScript имеют нулевой индекс, то они становятся изображением 0 и изображением 1 из текстурного атласа.

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

В функции create() мы соберем нашу тайловую карту. Карта, используемая в примере кода для этого раздела имеет размер 10x10 плиток.

let level = [
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
]

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

const wall = 826
const floor = 0
level = level.map(r => r.map(t => t == 1 ? wall : floor))

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

const tileSize = 16
const config = {
    data: level,
    tileWidth:  tileSize,
    tileHeight: tileSize,
}

Давайте воспользуемся этим объектом config, чтобы создать тайловую карту и прекрипить к ней tileset (набор тайлов, который содержит данные о наших плитках). Набор тайлов — это то, что сопоставит наш текстурный атлас с тайловой картой.

const map = this.make.tilemap(config);
const tileset = map.addTilesetImage('tiles', 'tiles', tileSize, tileSize, 0, 1);

Тайловая карта создается путем передачи объекта config в метод this.make.tilemap(), а затем унаследованный метод созданной карты используется для добавления к ней набора тайлов . Вы можете создавать всевозможные игровые объекты, используя методы из this.make.*, которые являются частью класса GameObjectCreator.

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

Эта функция addTilesetImage получает много аргументов; большинство из них являются необязательными, но так как у нас есть пространство в 1 пиксель между тайлами, нам нужно указать их.

Параметры метода addTilesetImage:

  • tilesetName — строка, являющаюся именем создаваемого набора тайлов;
  • key — строка-ключ изображения из Phaser.Cache, используемого для этого набора тайлов;
  • tileWidth — ширина тайла;
  • tileHeight — высота тайла;
  • tileMargin — поле вокруг тайла;
  • tileSpacing — расстояние между каждой плиткой на текстурном атласе;
  • gid — Если добавляется несколько наборов тайлов на пустую карту, то здесь указывается начальный GID, который этот набор будет использовать.

Первый аргумент addTilesetImage - это имя набора тайлов, экспортированное в данные map. У нас нет данных карты, поскольку мы не используем редактор карт. Мы передаем тайлы, которые являются тем же ключом, который мы использовали при загрузке текстурного атласа. Второй аргумент - это ключ кэшированного изображения из preload(), то есть 'tiles'. Если мы не передадим этот второй параметр, он будет использовать первый как ключ для поиска изображения, что немного сбивает с толку. Мы просто передаем их оба, чтобы было понятно, что мы делаем. Остальные аргументы - это все данные, которые будут присутствовать при экспорте из редактора карты, и все они нам необходимо явно передать, поскольку мы собираем все вручную. Третий и четвертый аргументы — это размер тайла: их ширина и высота. Пятый и шестой аргументы связаны с полем вокруг таблицы спрайтов и промежутком между изображениями. Все значения в пикселях.

На тайловых картах Phaser может быть несколько слоев, что очень похоже на работу со слоями Adobe Photoshop. Слои можно использовать для разделения игровых элементов на фоновый и передний слои, чтобы они могли располагаться друг на друге.

До версии Phaser v3.50.0 слои разделялись на статический и динамический. статический слой был обладал большей производительностью, но не позволял применять эффекты к тайлам. Для создания таких слоев использовались соответственно методы: createStaticLayer и createDynamicLayer. В новых версиях библиотеки оба этих метода являются устаревшими и по факту остались только динамические слои и создаются методом createLayer:

const ground = map.createLayer(0, tileset, 0, 0);

Несмотря на то, что мы назначаем наш слой переменной ground, в дальнейшем мы ничего не будем с ней делать. Мы это делаем просто чтобы задокументировать, что это нижний слой, где расположены пол и стены. Первый аргумент createLayer() — это идентификатор слоя; это может быть число или строка и используется другими функциями для ссылки на слой. Мы используем число 0, потому что присвоение ему строкового имени используется только при загрузке карт, экспортированных из редактора карт Tiled. Второй аргумент — это ранее созданный набор плиток. Третий и четвертый — координаты x и y, по которым надо размещать слоя в мире.

Если вы загрузите этот пример в свой браузер, вы увидите тайловую карту, похожую на рисунок 3-3. Рисунок 3-3. Базовая карта тайлов

И вот так вы рисуете тайловую карту. В этом разделе было много что стоит обмозговать, и очень полезно проверить связанную документацию по функциям Phaser. Еще одно важное упражнение, которое нужно сделать сейчас — это поэкспериментировать с этим массивом карт и различными значениями. Можете ли вы разместить в этой комнате четыре колонны? Скелет на земле?

А как насчет рисования темницы? Что ж, это наш следующий пример.

Базовое подземелье

На данном этапе важно понять, почему мы оставили часть книги, посвященную процедурной генерации, для будущих глав. Многие думают, что главная особенность roguelike — это процедурная генерация; честно говоря, мы согласны с этим.

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

Следующий пример находится в папке chapter-3/example-2-basic-dungeon или здесь. Это практически тот же код, что и в предыдущем примере. Единственное изменение состоит в том, что мы изменили массив level, чтобы он стал настоящей картой, похожей на подземелье, а не простой сеткой 10 на 10. Еще одно небольшое изменение заключалось в изменении загрузки файла game.js в HTML, так как нам надо пометить его как модуль JavaScript, чтобы мы могли использовать внутри него импорт для загрузки данных карты из другого файла:

<script src="game.js" type="module"></script>

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

Загрузите его, и вы увидите подземелье, как на рис. 3-4. Рисунок 3-4. Базовое подземелье.