Как рисуется карта в Фараоне
В свободное время я восстанавливаю старенькую, но довольно известную игру Pharaoh. Это ситибилдер, выпущенный в прошлом веке и разработанный Impressions Games. Технология рендеринга в этой игре была значительным достижением для своего времени и способствовала созданию впечатляющей атмосферы Древнего Египта, которая погружает игрока в проработанное окружение, удивляет вниманием к мелким деталям и передает богатство и разнообразие древнеегипетских пейзажей. В этой статье я опишу алгоритм отрисовки города, зданий, объектов, анимации и формат карты оригинальной игры.
Отрисовка изометрических карт
Классическая схема отрисовки карты в изометрии - это выстраивание геометрии по осям X, Y, Z, углы между которыми равны. Собственно только такой алгоритм отрисовки и можно называть изометрическим. В играх есть и другие виды аксонометрических проекций, где только два угла равные или все три отличаются. Но люди по старинке продолжают все это называть изометрией, ну и пусть хулиганят, главное чтобы игры красивые получались.
Так например игры серии Caesar/Pharaoh используют классическую схему 120-120-120.
Почему вообще разработчики первых игр использовали этот вид отрисовки, да потому что дешево и именно изометрия дает самый простой вид тайла, с соотношением сторон 2:1
Он проще всего (кроме видов сбоку и сверху) поддается обработке в пакетах создания 3D моделей. Еще одним преимуществом отрисовки объектов в таком виде, что если мы реализуем переключение вида карты, то не придется делать дополнительные текстуры видов для этих объектов с других сторон.
Есть и другие виды аксонометрических проекций, и все они в той или степени отметились в популярных играх. Большинство игр получили свой запоминающийся вид в силу технических ограничений инструментов своего времени, Джейсон Андерсон в одном из интервью рассказал, что движок первых двух Fallout имеет такое соотношение сторон тайла (5:3), потому что пакет Softimage 3D медленно работал в режиме рендера изометрии, а когда уже купили Maya, то решили не переделывать.
Не менее популярен вид диметрической проекции, когда два из трёх углов между осями будут равны, как например в серии игры Civilzation 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 (scree_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 координате, чем выше объект на экране, тем раньше его следует отрисовать. Это неплохо работает для любых объектов на сцене, но требует дополнительного прохода при отрисовке сцены. Ниже схематично показано, как это может выглядеть, если считать клетки пикселями в тайле.
Более продвинутая техника отображения, которая применялась в последующих играх серии, состоит в технологии слоев, когда каждый тип объектов рисовался на своем слое (земля, деревья, люди, здания и тд) чем крупнее объект, тем выше слой он использовал для отрисовки. Потом эти слои накладывались и получалась финальное изображение. Эта технология появилась частично в Зевсе, и полностью расцвела в Императоре, но требовала значительно большего объема памяти для реализации. Так например в императоре использовалось 8 слоев карты (земля/вода, эффекты на земле, люди, крупные объекты на земле, здания, здания, монументы, эффекты зданий, эффекты). Каждый из слоев требовал столько же памяти, как и основной слой. Если вы играли в Зевса/Императора, то могли заметить что они содержат намного меньше артефактов отображения чем игры до них. К тому же в Императоре был слой для теней, поэтому картинка выглядит более естественной.
Проблемы с отрисовкой объектов на этом не заканчиваются, чем больше места объект занимает на карте, тем больше видны артефакты отображения по краям объекта. Кроме этого процент пикселей, которые надо отсечь на этапе наложения текстур становится проблемой для производительности. В этом случае разработчики обычно режут большую текстуру на несколько более мелких, и появляется составной объект, который может не иметь правильной ромбовидной формы. Размер 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-летней выдержки - подключайтесь к репозиторию https://github.com/dalerank/Ozymandias
Игра еще не восстановлена на 100%, но все к этому идет.
Еще я сделал обновляемый билд на https://dalerank.itch.io/ozymandias, кому неохота компилить игру под свою ос, можно взять уже готовую сборку. Ресурсы конечно же вам нужны от оригинала, мы ведь не пираты.