В свободное время я восстанавливаю старенькую, но довольно известную игру Pharaoh. Это ситибилдер, выпущенный в прошлом веке и разработанный Impressions Games. Технология рендеринга в этой игре была значительным достижением для своего времени и способствовала созданию впечатляющей атмосферы Древнего Египта, которая погружает игрока в проработанное окружение, удивляет вниманием к мелким деталям и передаёт богатство и разнообразие древнеегипетских пейзажей. В этой статье я опишу алгоритм отрисовки города, зданий, объектов, анимации и формат карты оригинальной игры.
Отрисовка изометрических карт
Классическая схема отрисовки карты в изометрии — это выстраивание геометрии по осям X, Y, Z, углы между которыми равны. Собственно, только такой алгоритм отрисовки и можно называть изометрическим. В играх есть и другие виды аксонометрических проекций, где только два угла равные или все три отличаются. Но люди по старинке продолжают всё это называть изометрией — ну и пусть хулиганят, главное чтобы игры красивые получались. Так, например, игры серии Caesar/Pharaoh используют классическую схему 120-120-120.
Почему вообще разработчики первых игр использовали этот вид отрисовки? Да потому что дёшево, и именно изометрия даёт самый простой вид тайла, с соотношением сторон 2:1.
Он проще всего (кроме видов сбоку и сверху) поддаётся обработке в пакетах создания 3D-моделей. Ещё одно преимущество отрисовки объектов в таком виде — если мы реализуем переключение вида карты, то не придётся делать дополнительные текстуры видов для этих объектов с других сторон.
Есть и другие виды аксонометрических проекций, и все они в той или иной степени отметились в популярных играх. Большинство игр получили свой запоминающийся вид в силу технических ограничений инструментов своего времени. Джейсон Андерсон в одном из интервью рассказал, что движок первых двух Fallout имеет такое соотношение сторон тайла (5:3), потому что пакет Softimage 3D медленно работал в режиме рендера изометрии, а когда уже купили Maya, то решили не переделывать.
Не менее популярен вид диметрической проекции, когда два из трёх углов между осями равны, как например в Civilization 2 или Age of Empires II.
Или не менее популярный вид, когда угол между осями XZ равен 90°, как например в Ultima Online/Boktai.
Проекция в Stardew Valley обходится ещё дешевле, не предполагая третьей стороны у объектов, что позволяет делать тайлы прямо в Paint. По словам Eric Barone, он первую локацию действительно нарисовал в Paint, потом разбил на квадраты и начал с ними работать. Шутка, конечно! Есть специальные инструменты для создания такого вида. Это очень удобно для создания опенворлдов и разного вида инди, когда недостаточно средств на 3D-художника, но планируется большое количество контента. Основной же проблемой изометрических спрайтов является их неудобность для масштабирования: спрайт может быть нарисован хорошо только для одной дистанции, попытки приблизить или отдалить камеру приводят к разного рода графическим артефактам.
Что это такое?
В играх, где рендеринг основан на изометрии (аксонометрии), каждый визуальный элемент разбивается на мелкие части (тайлы) определённого размера. Из таких тайлов на основании данных уровня (обычно это двумерный массив или массивы) формируется игровой мир. Тайлы составлены рядом в определённом порядке, и часто края зашумлены, чтобы обеспечить бесшовные стыки. Так, например, разные тайлы могут формировать различные фигуры — это уже похоже на примитивную карту.
Текстуры, однако, не могут быть не прямоугольными, поэтому те части тайла, которые не должны отображаться на экране, делаются прозрачными. В силу ограничений технических средств золотого века развития компьютерных игр, операция наложения прозрачных частей текстур была достаточно дорогой, поэтому использовались различные техники отсечения прозрачных пикселей. База тайла в игре Pharaoh составляет 60×30 пикселей — это минимальный размер тайла, который может отобразить движок игры без искажений или ошибок. В оригинальной игре Pharaoh используется отсечение пикселей на этапе рендеринга, в ремейке текстуры преобразуются в формат с прозрачностью, это выполняется на этапе загрузки ресурсов.
Со времён первых изометрических игр были определены основные правила для таких карт, которые позволяют избегать перегруженных алгоритмов рендеринга.
- Изометрическая сетка должна быть квадратной для упрощения алгоритма отрисовки.
- Тайлы должны быть небольшого размера, не более 90 пикселей шириной — потом количество прозрачных пикселей становится проблемой для софтварного отсечения.
- Графика должна хорошо биться на простые изометрические изображения, которые не вылезают за пределы тайла, иначе появляются ошибки порядка отрисовки, что сильно усложняет рендер.
- Тайл в идеале должен быть либо проходимым, либо непроходимым. Иначе сложно работать с тайлами, содержащими и проходимые, и непроходимые области, что опять же сильно усложняет рендер — а как мы помним, он был на 90% софтовым.
- Края тайлов в идеале должны быть бесшовными, чтобы их можно было стыковать без оглядки на порядок, иначе придётся заводить алгоритмы стыковки тайлов.
- Тени создавать сложно, и для этого приходится делать второй набор текстур, которые сначала отрисовываются на слое земли, а потом на верхнем слое отрисовывается сам объект. С тенями в игре всё ещё было сложно — рендер не поддерживал слои, поэтому тени были запечены сразу в текстуры.
Существует два основных алгоритма отрисовки карты в изометрии: diagonal-path и zig-zag-line.
Diagonal-path
Первый более прост в реализации, его код для отрисовки и поиска пути довольно прост (считаем, что карта представлена в виде двумерного массива), но обладает неприятным свойством: по краям карты присутствуют незаполненные области, и приходится либо дорисовывать их неигровыми тайлами, либо просто оставлять как есть. Алгоритм отрисовки такой карты максимально простой — рисуем тайлы по диагонали сверху вниз, и так для каждой строки массива.
Показать код
map = [
[1, 1, 1, 1],
[1, 0, 0, 1],
[1, 0, 0, 1],
[1, 1, 1, 1],
];
for (y = 0; y < map.size; y++) {
for (x = 0; x < map[y].size; x++) {
screen_x = (x * tile_width / 2) - (y * tile_width / 2)
screen_y = (x * tile_height / 2) + (y * tile_height / 2)
// Draw tile (screen_x, screen_y)
}
}Получаем в итоге вот такую картинку.
Zig-Zag-Line
Он лучше подходит для прямоугольных экранов, не слишком сильно отличается по коду и лучше выглядит — так что неудивительно, что авторы в итоге взяли именно его для игры. К сожалению, у него тоже есть недостаток: путь от одной точки к другой может потребовать диагональных перемещений, и алгоритмы поиска пути должны быть адаптированы для работы на такой карте. Идея заключается в смещении по x на ширину тайла для каждого нового тайла в строке и увеличении y на половину высоты тайла для каждой новой строки; но если индекс строки нечётный, дополнительно надо сдвинуть x на половину ширины тайла влево, чтобы избежать наложения новой строки на уже отрисованную. Псевдокод будет следующим:
Показать код
map = [
[1, 1, 1, 1],
[1, 0, 0, 1],
[1, 0, 0, 1],
[1, 1, 1, 1],
];
for (y = 0; y < map.size; y++) {
for (x = 0; x < map[y].size; x++) {
screen_x = x * tile_width + (y & 1 ? tile_width / 2 : 0);
screen_y = y * tile_height / 2 - (sprite_height - tile_height);
}
}Получаем в итоге вот такую картинку.
Анимация работы обоих алгоритмов.
Переходим к отрисовке города
Кроме непосредственно тайлов земли, на карте расположены здания, анимации статичных объектов, а также присутствуют подвижные объекты (люди, животные и другие). Есть объекты, сквозь которые можно пройти (арка, ворота, мосты по отношению к кораблям), есть объекты, которые расположены поверх других объектов (и рендер должен учитывать это) — например мосты. Дополнительная анимация тайлов и объектов, такие как тайлы воды или каналов. В конечном счёте на карте могут быть расположены неквадратные объекты, отображение которых тоже имеет свои особенности, потому что они не могут быть нарисованы за один проход. Всё это усложняет процесс отрисовки и требует дополнительных правил и условий.
Сортировка по глубине
Если попробовать нарисовать несколько объектов на одном тайле, то можно заметить проблему с сортировкой по глубине. Правильная сортировка гарантирует, что объекты, находящиеся ближе к игроку, будут отрисовываться поверх более далёких объектов. На уровне координат тайлов это решается алгоритмом отрисовки, но на уровне тайла приходится прибегать к дополнительной сортировке по Y-координате: чем выше объект на экране, тем раньше его следует отрисовать. Это неплохо работает для любых объектов на сцене, но требует дополнительного прохода при отрисовке. Ниже схематично показано, как это может выглядеть, если считать клетки пикселями в тайле.
Нет порядка сортировки.
Сортировка по Y-координате.
Более продвинутая техника отображения, которая применялась в последующих играх серии, состоит в технологии слоёв, когда каждый тип объектов рисовался на своём слое (земля, деревья, люди, здания и т.д.); чем крупнее объект, тем выше слой он использовал для отрисовки. Потом эти слои накладывались, и получалось финальное изображение. Эта технология появилась частично в Зевсе и полностью расцвела в Императоре, но требовала значительно большего объёма памяти. Так, например, в Императоре использовалось 8 слоёв карты (земля/вода, эффекты на земле, люди, крупные объекты на земле, здания, монументы, эффекты зданий, эффекты). Каждый из слоёв требовал столько же памяти, как и основной слой. Если вы играли в Зевса/Императора, то могли заметить, что они содержат намного меньше артефактов отображения, чем игры до них. К тому же в Императоре был слой для теней, поэтому картинка выглядит более естественной.
Проблемы с отрисовкой объектов на этом не заканчиваются: чем больше места объект занимает на карте, тем больше видны артефакты отображения по его краям. Кроме этого, процент пикселей, которые надо отсечь на этапе наложения текстур, становится проблемой для производительности. В этом случае разработчики обычно режут большую текстуру на несколько более мелких, и появляется составной объект, который может не иметь правильной ромбовидной формы. Размер 4×4 тайла считается максимальным для отображения крупного объекта.
Составное здание.
Часть здания, подогнанная под размеры тайла 4×4.
Формат карты Фараона
Размер карты в игре всегда N (228×228) тайлов, но она может быть заполнена лишь частично, поэтому создаётся впечатление, что все карты разного размера. Карта состоит из множества двумерных массивов соответствующего размера (int, short или char), каждый из которых содержит определённый набор свойств тайла.
Друг за другом читаются следующие массивы из файла карты.
Показать формат
UINT32 images[N] - индекс текстуры из атласа
UINT8 edges[N] - границы тайла; из-за того что карта может быть меньше размером,
чем максимальная, так определяется положение граничных тайлов;
массив остался ещё с Caesar2 и практически не используется
UINT16 buildings[N] - массив индексов зданий, сами здания хранятся в другом массиве
размером не более 4000 элементов
UINT32 terrain[N] - массив битов типов земли (дорога, сады, канал, поле, вода и др.)
UINT8 canals[N] - массив тайлов ирригационной системы, каналы могут быть размещены
поверх тайлов земли
UINT16 figures[N] - массив индексов стартовой фигуры на тайле; массив фигур на тайле
представляет собой связанный список, каждая фигура имеет ссылку
на следующую
UINT8 sprite[N] - массив текущего индекса анимации, прибавляется к базовому из images
для динамичных тайлов вроде воды или деревьев
UINT8 random[N] - случайное число, которое задаётся на старте карты, используется
при очистке земли, чтобы рандомно обновлять тайлы
INT8 desirability[N]- используется домами для определения, насколько хорошо окружение
UINT8 elevation[N] - уровень подъёма, используется для мостов и крупных объектов,
чтобы правильно отображать объекты над землёй
UINT16 damage[N] - уровень разрушений; использовалось в Цезаре для разрушений, но
осталось и в Фараоне, чтобы не ломать формат
UINT8 canal_backup[N]- undo-массив, чтобы поддержать функцию отмены строительства
UINT8 floodplain_fertility[N] - массив плодородности тайлов, на которых можно строить
фермы
UINT8 vegetation_growth[N] - массив прогресса травы и деревьев, для тайлов, где это
возможно; сам алгоритм роста деревьев в Фараоне не используется
UINT8 moisture[N] - массив уровня воды в тайле
UINT8 floodplain_growth[N] - прогресс роста травы на плодородных тайлах возле реки
и ещё несколько вспомогательныхЭтот формат остался практически неизменным с игр Caesar2 и Caesar3. Позже в Фараоне начинает набирать популярность формат сохранения чанками, когда идёт сначала тип чанка (блока с данными), а дальше сохраняются данные под определённый формат — например, здание, тайл, фигура и др.
Такой формат определённо более удобен для хранения разнородных данных разного размера. Причины использования формата на основе массивов определённого размера просты: они идеально ложатся на память и не требуют дополнительной обработки — ребята юзали ECS, когда это ещё не было популярным. В условиях, когда нужно загружать громадные (для игр своего времени) карты, это было одним из решений, чтобы не ждать по 5 минут на загрузке уровня. Второй причиной использования этого формата была необходимость быстро шарить данные между большим числом объектов карты: например, данные о желательности земли шарятся между несколькими домами, не требуя поиска в массиве тайлов информации о нём.
Основная информация о тайле на карте размещается в массиве images, алгоритмы игры меняют индексы текстур в этом массиве, и они обновляются на следующем фрейме. Здания, размещённые на карте, могут менять индексы в своей области, поверх обычно накладывается 1–2 слоя анимации.
UINT32 images[] — индекс текстуры из атласа
Основной массив для отображения тайлов на карте: любое изменение в жизни города отображалось на карте. Будь то рост травы, анимация в тайлах воды или уборка урожая с ферм.
UINT16 buildings[] — массив индексов зданий
Зелёными тайлами отмечен главный тайл здания, от которого считается доступность дороги, до которого строится путь из любой точки карты и от которого считаются доступные действия с областью здания.
UINT32 terrain[] — массив битов типов земли
Наличие дорог определяет граф перемещения телег по городу с узлами в местах перекрёстков; удаление или создание дорог вызывает его перестройку. Что было особенно заметно на поздних этапах игры с разветвлённой системой дорог — игроки предпочитали строить длинные прямые участки, чтобы повысить скорость расчёта перемещений жителей по городу. Сейчас, конечно, миллионов электроконей под капотом мегагерцев хватает, чтобы вытянуть любой возможный граф.
UINT8 moisture[] — массив уровня воды в тайле
Как вы видите, визуально в игре он пересекается с плотностью травы — или её отсутствием, если воды на тайле нет.
UINT8 floodplain_fertility[] — массив плодородности тайлов
От значений в этом массиве зависит количество урожая, который будет собран с фермы. Использование ферм снижает этот параметр, так что со временем ферма приносит всё меньше и меньше продукции. Разливы Нила восполняют это значение.
Как вы видите, никакой магии — одни голые цифры.
Заключение
В завершение этой статьи о том, как рисуется карта в игре, хочу отметить, что даже спустя почти четверть века «Фараон» сохраняет популярность среди поклонников стратегий. Игра остаётся классическим образцом в жанре ситибилдеров и примером того, как выдающийся дизайн и отменный визуальный стиль продолжают восхищать и вдохновлять игроков даже спустя годы и годы после своего выпуска. И даже запуск провального ремейка от Triskell Interactive не смог снизить интерес к старому доброму Фараону со стороны сообщества. Честно, я очень ждал ремейк, активно участвуя в обсуждениях с разработчиками, но когда понял, что игра движется в сторону всё большего упрощения, как-то подрастерял запал. А когда недавно от Трискеллов прошли слухи, что они занимаются портом на мобилки и f2p-режимом, я совсем расстроился.
Если хотите посмотреть, как это всё работает в коде, и сдуть пыль веков с легаси-кода 25-летней выдержки — подключайтесь к репозиторию github.com/dalerank/Ozymandias. Игра ещё не восстановлена на 100%, но всё к этому идёт.
Ещё я сделал обновляемый билд на dalerank.itch.io/ozymandias — кому неохота компилить игру под свою ОС, можно взять уже готовую сборку. Ресурсы, конечно же, вам нужны от оригинала, мы ведь не пираты.
← Все статьи