{{notification.text}}

MirGames

В процессе создания игры мы постоянно сталкиваемся с определенными проблемами, которые как то надо решать…

Иногда трудности решения проблемы влияют и на сюжет самой игры, и на игровые моменты. В этом разделе будут публиковаться статьи, посвященные созданию Ad Infinitum. Возможно, вы сейчас как раз делаете какую нибудь игру и наши решения Вам помогут ;)

Мы отнюдь не утверждаем, что представленные нами решения являются наилудшими, мы просто делимся своим опытом

Создание тайловой карты методом блендинга

При создании карты мы рассматривали несколько различных вариантов:

  1. Первый образец карты представлял собой нечто схожее с Warcraft'ом. Конечно схожее, ведь сами тайлы были взяты из второй части игры.
    Пример первого образца карты
  2. Вторая попытка — рисовать тайлами размера 256x256, которые включали бы в себя целые фрагменты местности, например, поворот реки или ее развилка. Плюсом такого метода является художественная привлекательность результата, конечно, если руки растут откуда положено. Явных минусов, увы, целых три:
    • Во-первых, непомерная работа для художника.
    • Во-вторых, тайлы занимают много места для хранения как в памяти, так и на жестком диске.
    • И в-третьих — это ограниченные возможности в формирования местности, т.е. невозможность с помощью крупных модулей создавать, например, реки и озера произвольной формы. Для решения такой проблемы был бы только один выход — рисовать дополнительные тайлы, что не есть хорошо...
    Пример второго образца карты
И, наконец, мы вновь решили вернуться к тайлам, но рисовать на этот раз собственными силами. Тут проблем нет, кроме, разве что, переходных тайлов. Многие скажут, что хороший художник сможет сделать отличные переходные типы. С этим трудно спорить, только те самые пресловутые «хорошие художники» не согласятся ничего делать нахаляву :).

Оно, с другой стороны, и понятно, только вот нас такой вариант не устроил. Поэтому рисование переходных типов мы отложили на неопределенное время, решив пойти по альтернативному пути, т.е. генерировать переходные тайлы «на ходу». Строго говоря, это даже не генерация в строгом смысле этого слова, а отображения базовых тайлы таким образом, чтобы создавался эффект плавного перехода между тайлами.
Карты с переходными тайлами

Итак, рассмотрим конкретный пример.

Это то, что получается при заполнении матрицы карты тайлами. Условно говоря, водная часть имеет индекс 0, песок — 1. Для наглядности отображается сетка.
А это то, что должно получиться в результате

Не буду объяснять — почему, но для достижения желаемого результата нам нужно вывести карту по слоям.

Вначале рисуем воду, делая ее прозрачнее в вершинах, примыкающих к другому типу поверхности, в даном случае к песку.

Затем рисуем песок, так же делая прозрачнее вершины, примыкающие к воде.


Только как расчитать уровни прозрачности? А вот как. Надо пробежать по всей карте и расчитать уровень прозрачности каждой вершины относительно каждого типа поверхности.

Рассмотрим на нашем примере готовую карту прозрачности вершин тайлов относительно воды. Вариант с водой:

Карта прозрачности слоя воды. Чем выше число, тем выше уровень прозрачности.

Почему в левой верхней точке уровень прозрачности равен нулю (т.е. изображение непрозрачно) понятно — вокруг вершины нет ни одного инородного тайла. Та же картина со всем верхним рядом вершин.

Теперь переходим ко второму ряду. Вторая слева вершина — 64. Почему? Считаем среднее арифметическое окружающих тайлов по следующему правилу: если тайл заполнен водой, то его значение — 0, если он является любым типом поверхности, кроме воды — его значение 255. Т.о. складываем — (0+0+0+255)/4.

Округляем результат в большую сторону и получаем — 64. Переходим чуть правее и считаем — (0+0+255+255)/4 = 128. Вокруг центральной вершины вообще нет воды, следовательно уровень ее прозрачности (только относительно воды) равен (255+255+255+255)/4=255.

Если по полученой таблице (которую, к слову говоря, лучше расчитать заблаговременно, при загрузке карты, но ни в коем случае при отрисовке) отрисовать воду, получиться что-то вроде этого:

Результат отрисовки слоя воды

То же самое повторяем со всеми типами поверхности, последовательно отрисовывая их друг на друга.

В результате и получится желанный рисунок =)

Осталось сказать только пару слов об оптимизации:

  1. Расчитывать уровни прозрачности при загрузке карты, а не паралельно с отрисовкой.
  2. Я храню уровни прозрачности вершин неправильно, т.е. для каждой вершины каждого тайла уровень расчитывается и хранится отдельно. Между тем, правая-нижняя вершина тайла [0,0] и левая-верхняя тайла [1,1] это, по сути, одна и та же точка. Вовсе незачем расчитывать ее четыре раза (именно столько тайлов ее окружают).
  3. Если тайл полностью прозрачен (уровни прозрачности всех вершин равны 255), не отрисовывайте его вообще.
  4. Если тайл полностью непрозрачен — рисуйте его, не задавая никаких эффектов, т.е. как заведомо непрозрачный.
  5. Старайтесь группировать тайлы, чтобы сократить количество выводимых текстур к минимуму. Например, если есть группа полностью непрозрачных тайлов размером 4x4, имеет смысл отрисовать все это в виде одного тайла, а не 16. Это потребует, правда, подготовить соответствующего размера текстуры.
Сага о форматах

Регулярно посещая многочисленные форумы, посвященные «игроделанию», время от времени натыкаешься на вопросы, типа: "в каком формате хранить графику?", «как хранить кадры анимации» и т.п.

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

Итак, Вы делаете некоторый игровой проект, и Вам необхоимо решить, как же покомпактнее хранить многочисленные спрайты/заставки… Можно, конечно, поступить просто и не париться — запихать все в 24-битные BMP (даже если цветов дай Бог 256), и пусть так себе и лежат. Пользователь скачивает дистрибутив, распаковывает, и получает «нечто», без намеков на оптимизацию.

В некоторых любительских проектах (не буду называть названий) так проблему и решили. Но это пассивный путь.

Разве не приятно, когда игра, в дополнение ко всему остальному, может похвастаться еще и небольшим размером дистрибутива? Вот мы и перечислим способы, с помощью которых можно максимально «пожать» графику.

Заставки, тайлы и т.п.

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

Подобные изображения можно, в принципе, легко сжать Jpeg'ом. Потери качества при этом будут незаметны для глаза, зато можно получить иногда приличное уменьшение размера.

Для примера, я взял тайл в формате TGA 24bit, занимающий 196Kb (после сжатия ZLib — 40Kb) и сжал. В результате он стал занимать 15Kb. Это — один тайл (точнее — один тип поверхности), а всего их с десяток. Так что все полноцветные непрозрачные изображения доверьте Jpeg'у :)

Спрайты, шрифты, курсоры и т.п.

Эти типы изображений уже имеют встроеный альфа-канал, либо требуют его генерации по ключевому цвету.

Сжатие с потерями, которое осуществляет Jpeg в этом случае неприемлимо, т.к. неизбежно размоются границы областей с «полезными пикселами» и областей, заполненных ключевым цветом. В случае с генерируемой прозрачностью по ключевому цвету нет необходимости в хранении альфа-канала, так что больше всего подойдет BMP 8bit (или даже 4bit для шрифтов). Чем меньше цветов при индексировании изображения будет указано, тем лучше изображение будет сжиматься архиватором :).

Самая интересная ситуация с изображениями, имеющими встроеный альфа-канал. В принципе, распространенных стандартных форматов, поддерживающих альфу, не так и много. PNG да TGA. Но ни один из них не может адаптироваться под наши запросы ;).

А я предлагаю вообще разделить само изображение, и альфа-канал. Изображение отдельно индексируется и сохраняется в BMP 8bit (для спрайтов этого более чем достаточно). Альфа-канал тоже сохранятеся отдельно как BMP 4bit (в большинстве случаев этого хватает). Кстати, теням тоже можно уготовить участь 4-битного формата :).

Разделение изображения и альфа-канала дает и еще один бонус. Он позволяет хранить один альфа-канал для всех изображений, у которых альфа-канал одинаков.

Например, у вас есть домик, у которого горит, меняя интенсивность, свет в окошке. Альфа-канал в этом случае будет одинаковым, ведь уровень прозрачности в районе окна не будет меняться (впрочем, это если не извращаться с glow-эффектами). Вот и будем хранить альфа-канал один, а при загрузке применять его ко всем кадрам анимации.

В принципе, для дополнительного сжатия изображений можно применять и всевозможные RLE (Run length encoding) алгоритмы. В частности, этот метод поддерживается и BMP и TGA изображениями. Но эффективность его не всегда очевидна. На моей памяти было лишь два формата, в которых графика посредством использования собственной разновидности RLE, существенно выигрывала в размере.

Это формат DEF из игры Heroes of Might & Magic III (вся анимация лежит в этом формате), и формат GP из Казаков (Cossacks). В последнем случае использовался не RLE, а нечто совершенно извращенное.

На вскрытие этого формата ушла просто туча времени. :) К сожалению, распакованый результат представлял из себя разрезанные куски спрайтов, и найти способ их автоматической склейки не удалось, так что дело до создания публикабельного конвертера не дошло. Но создателям формата все равно респект :)

Структура данных тайловой карты и статичных спрайтов

Правильная организация данных не только позволит ускорить многие алгоритмы, но от нее в принципе зависит реализация многих интересных моментов.

Ниже представлена (в упрощенной форме) описываемая рекоммендуемая структура.

Структура карты

Допустим, нам в неких целях потребовалось узнать, какой объект (или объекты) находится в конкретной клетке. Скажем, хотим мы, чтобы при передвижении крусора высвечивалась надпись — название объекта, на который направлен курсор.

Будем считать, что выделенным является тот объект, на непроходимые части которого направлен курсор (см. рисунок ниже). Это необходимо только для активных объектов, например, домов, колодцев, ворот. Деревьям такая система ни к чему. Итак, мы направляем курсор на клетку C4. Теоретически, лучшим вариантом определения того, что мы «попали» в объект, будет попиксельная проверка. Сравниваем цвет пиксела на экране с цветом пиксела спрайта. Если они совпадают, значит, под нами объект. Можно еще проверять значение альфа-канала у спрайта…

Тут, правда, могут возникнуть трудности, если объектов в данной точке несколько. Именно поэтому я предпочел выбрать выделение по непроходимым клеткам. Но как определить, что под курсором объект, и что он в этом месте непроходим? Не нужно говорить, что как минимум каждый спрайт должен иметь собственную карту проходимости. Это факт. Теперь возможны два варианта:

  1. Пробегаем по базе объектов в игре и проверяем, проходим ли он в необходимой нам точке. Способ простой, но не особо продвинутый. Пробег по базе предметов (а их несколько сотен) и проверка довольно обременительны, ведь лучше оптимизировать все, что только можно. Кроме того, проверку желательно делать на каждом кадре (курсор ведь двигается). Так что этот вариант мне не нравится.
  2. Храним в каждой ячейке информацию обо всех объектах, находящихся на ней. Вот к этому я и вел весь предыдущий разговор. :). Суть состоит в том, что объект, при помещении на карту, добавляет ссылку на себя (Pointer) в список объектов на тайлах. Но не на всех тайлах, которые захватывает изображение, а только тех, на которых спрайт непроходим. На рисунке ниже они отмечены красным цветом.
Пример второго варианта

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

Есть еще один важный момент. Если Вам приходилось создавать «сложные» спрайты (например, такой домик как на рисунке — он частично проходим и сзади и спереди), Вы, вероятно, попадали в ситуацию, когда спрайт, и объект (существо), проходящее по его территории, неверно друг друга перекрывают, т.е. прорисовываются в неверной последоватнльности. Для того, чтобы все было правильно, нужно спрайтам добавить еще одну таблицу — таблицу прорисовки.

Пример таблицы прорисовки

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

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

На втором рисунке видно, что домик вылезает и на самую нижнюю строку, но там он проходим. Т.о. при нашем способе прорисовки домик перекроет все объекты, находящиеся выше строки G. В то же время, если существо вступит на последнюю строку, оно само перекроет нижнюю часть домика, что и следовало доказать. :). Если бы в центре изображения было больше свободного места, можно было бы сделать клетки перед дверью проходимыми и, соответственно, сдвинуть выше центральные точки прорисовки.

04.05.04 07:59