Спускаясь к смертным

Или добавляем картам объема

Alexey “Averrin” Nabrodov
8 min readDec 20, 2017

Кто не понимает, о чем тут речь, вот предыстория: 1, 2, 3.

Вид с высоты птичьего полета

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

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

Мировой механизм

Один из самых первых простотипов, который похож на правду

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

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

И эти проблемы правда имеют место быть. Однако есть некоторые “но”:

  • Визуальные редактор удобен, особенно так как есть возможность ставить на паузу в процессе выполнения. Тогда редактор превращается в инспектор, что дико полезно.
  • У структуры и правда появляются некторые ограничение, однако, как оказалось, все мало-мальски серьезное все равно реализует свое подобие движка над Unity. А так как это полноценный C#, то тут есть даже довольно удобные библиотеки Dependency Injection.
  • “Шаг в сторону” оказалось достаточно размытым понятием, так как движок настолько развит, что, кажется, мне нужно еще долго с ним работать, чтобы наконец захотеть сделать что-то непредусмотренное.
  • Да, есть скрытые данные. Тут нет “но”, это проблема и приходится с этим жить.

Вторая причина заключается в том, что Unity поддерживает написание кода на C#, а генератор написан на C++. Это совершенно разные языки и заставить работать их вместе, оказалось совсем нетривиальной задачей, о чем ниже.

Ну и третья, очевидно спорная. Дело в том, что у меня есть в организме некоторая доля нон-конформизма, которая не дает мне использовать то, что уже очень популярно и, в какой-то мере, даже стало массовым. Мне кажется, что нет никакой заслуги сделать что-то обычное на движке, скриншотами которого и так заполнен весь интернет. Разве что ленивый не делал процедурную генерацию ландшафта на Unity. И я до сих пор так считаю. Но и здесь есть парочка “но”. Во-первых, я и правда потратил всего пару дней и вот уже готов написать статью о том, как у меня получилось сделать что-то, что уже можно показывать. И во-вторых, популярность движка влечет за собой приличную документацию, множество полезных обсуждений на форумах и тысячи готовых ассетов, которые можно применить в своем проекте. Хотя в этом и есть проблема, ведь на скриншотах будет изображено то, что собственно я и не делал. Воду, деревья, облака и тд, все я взял из сторонних наборов. В таких случаях особенно остро чувствуется, как сложно программистам объяснять что именно они делают.

Планетарное ядро

Облачно. Того гляди дождь пойдет

Как я, наверное, уже говорил, на Unity я хочу сделать только визуализацию результатов генерации, а всю логику сохранить в C++ коде. И в очередной раз сыграла правильная архитектура, которая позволила мне с минимальными изменениями поменять интерфейс программы. Хотя “минимальные” — тоже относительно.

Дело в том, что библиотека SFML, которую я использовал для от рисовка, шла со своими структурами данных, под которые была написана и та модификация библиотеки диаграмм Вороного, которую я включил в код генератора. Но оказалось, что эти структуры можно без особых проблем выделить из состава библиотеки и включить в код как самостоятельные классы. Редкость, но приятно.

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

Астральная связь

Нельзя просто так взять, и… ну вы знаете. Здесь я все-таки нашел, где вляпаться в то, что не пишут в туториалах по игровому движку. Пару статей мне удалось найти, но они все были для примитивных случаев, а у нас, согласитесь, проект достаточно серьезный.

Итак, как из динамической библиотеки на C++ получить сложный и готовый к использованию класс. Короткий ответ: никак. Длинный, впрочем, мало отличается. Полагаю, что можно адскую магию провернуть и просунуть как-то структуру со вложенностью, но так как даже банально строку прокинуть — то еще приключение, то и морочиться с более сложными вещами я счел нецелесообразным. Можно же пожертвовать производительностью и сериализовать все в строку. Привет, JSON.

Да, я понимаю, что это очень плохое решение. Но быстрое и простое. Поэтому остановимся на этом, написав в c++ генератор json данных. Из сложного надо отметить, что данные по большей части представляют собой дерево, но ни генерировать его, ни работать с ним не слишком удобно, поэтому пришлось изобрести костыль с тем, что оба вида кластеров и регионы лежат списками, а ссылки происходят по идентификаторам (“удачно” равным позиции в списке).

Чудовищность протокола подкрепляется сложностями работы с библиотеками, так как если Unity подгрузила библиотеку, то все, ее нельзя заменить не перезагрузив редактор. Чтобы обойти это ограничение пришлось написать магию динамической подгрузки и выгрузки библиотек, которая к тому же работает только в Windows. Ура=(

Да будет объем!

Вот вам пример, как оно могло быть красиво, если бы я не был ленивым и глупым=)

Осталось самое простое: нарисовать. Просто берешь и… рисуешь? Что-то не так все просто, как казалось. Тут мы сталкиваемся с той самой проблемой, где оказывается, что мы сами все генерируем на ходу. То есть если совсем просто, то нам надо найти способ создавать объемную фигуру, имея вершины поверхности и давать ей какой-то цвет.

Прошло часов 20 написания и стирания кода…

Нуууу… В целом как-то так, да. Но нет.

Собрать объемную фигуру-регион оказалось не очень сложно, я модифицировал код готовой библиотеки-ассета для “процедурной генерации мешей”. Но уже на стадии назначения цветов (а это делается материалами), уже пришлось задуматься об архитектуре приложения. Дело в том, что Unity подразумевает передачу зависимостей в класс путем связывания сущностей в редакторе. Это не только периодически ломается, чудовищно неудобно, но и в целом ничего не гарантирует.

Поэтому пришлось попутно разобраться с местным DI. Внедрение зависимостей в целом — штука простая и интуитивно понятная, но это в обычных условиях. Однако, когда дело касается такой вещи как prefab (местные “заготовки” объектов), то тут все плохо. Это тот случай, когда проще объяснить по-простому, чем разобраться, как же эта зараза работает. Это “шаблон”, то есть уже готовый объект, с которого можно “снять копию” и поменять кое-какие параметры. То есть вместо того, чтобы каждому дереву забивать его модель, материалы, коллайдеры и тп, мы создаем префаб, в котором все настраиваем, а затем уже “клонируем” его, меняя лишь позицию в мире. Вот именно подобных заготовок и пришлось наделать для всех типов регионов. Да и регионы быстро начали отличаться, ведь, например, деревья имеет смысл размещать только на лесных регионах. А траву еще и на степи… Это я к чему? Очень просто сделать что-то чтобы понять, что оно работает, но делать хорошо — это долго даже если “все уже готово”. Правда, не могу не признать, о миллионе вещей мне вообще думать не пришлось. Свет, камера, отрисовка моделей и тому подобное. Все необходимое заводится с двух-трех кликов, главное знать, что оно вообще есть.

А король-то голый!

Король перестал быть плоским, и оказалось, что в таком виде лучше бы ему оставаться в 2д. Если покрасить дно моря в синий, это не станет морем. Да и лес от более темного зеленого цвета сильно не выигрывает рядом с лугом. Вот так горы, хотя все еще невразумительные, стали внезапно более убедительными чем леса.

Для начала я занялся водой. Благо, мозг тратить не пришлось, только деньги. Зато посмотрите, какая красота!

Пришлось разве что настроить немного и корректировать уровень моря в зависимости от получившейся карты.

Что у нас дальше? Деревья. Ну и трава заодно. Не буду врать, и тут мне на помощь пришли высокооплачиваемые моделлеры низкополигональных моделек. Жаль в логике размещения они мне не помогли, а ведь это оказалось совсем не просто. Допустим, мы решаем сделать тупо: берем центральную точку региона (напомню, она не равна геометрическому центру, но после серии прогонов алгоритма Ллойда, она довольно близка к нему), рисуем круг, включающий в себя весь регион. Да, задевает и соседние, но в целом это совсем не страшно. Теперь берем случайные точки в нем и ставим туда деревья. Просто? Нет. Для начала, на какой высоте? Высота центральной точки —усредненная. Головой я понимаю, что для любой из точек можно посчитать высоту. Но это надо или опять лезть в c++, либо вспоминать геометрию с тригонометрией. Не наш путь. Делаем дешево и сердито: поднимаем деревья на высоту и рисуем вниз луч до столкновения с землей. И опускаем. Звучит просто, если кучу нюансов упускать. Но в целом работает. Случайным образом покрутим и вуаля, лесок. И травка тем же образом. Модельки мы тоже случайные выбираем, благо, в наборе есть много подходящих.

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

ТОРМОЗААААА!!!!

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

Вот так на самом деле выглядит “равнина”

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

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

Но основную проблему с тормозами решило уменьшение количество фигур-регионов. Очевидная идея, у меня же есть кластеры, как раз на этот случай! Ну да, спустя пару часов возни с переработкой структуры данных и вуаля, все лесочки и поля лежат рядом. Спасибо Unity, оказалось достаточно просто их объединить. И чтобы ты сгорела в аду, Unity, за то, что я три часа пытался понять, почему при объединении карта уездает на 715 пикселей. Лишь на следующий день мне пришло в голову глянуть корневой префаб кластера и обнаружить там вбитые координаты. Это к слову о моей любви к визуальным редакторам.

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

Так что ждите следующих статей, может они и появятся.

--

--

Responses (3)