Часть вторая


В части первой этой статьи я рассказал, почему для игры M.E.R.C. мы выбрали процедурную генерацию уровней, и описал требования к ней. Также я описал процесс генерирования структуры процедурного уровня и соединения множества фрагментов для создания целостного уровня. Во второй части статьи мы обсудим решение проблем освещения и NavMesh в Unity и создание NPC на основании темпа.

Освещение


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

Чтобы обеспечить соответствие координат фрагментов уровня с запечённым освещением и тенями, мы запекли каждую сцену в одной теме с одинаковыми настройками освещения. Мы обнаружили что, использование настройки «ambient source» у «skybox» приводило к немного разному освещению каждого фрагмента и при соединении фрагментов были чётко видны швы. Очевидно, что это нас не устраивало, поэтому мы поэкспериментировали с разными параметрами освещения, чтобы найти оптимальный вариант. Выяснилось, что наилучшие результаты получаются при изменении «ambient source» на «gradient». Соединения получались почти идеальными и швы были незаметны. На изображении ниже видна разница.

image

При генерировании процедурного уровня для M.E.R.C. у нас была одна «родительская» сцена, загружаемая перед всеми фрагментами. В этой сцене был один источник направленного освещения, используемый для освещения в реальном времени, запекаемого с теми же самыми параметрами, что и у фрагментов. Эта сцена также содержала все глобальные объекты и параметры, необходимые для окончательного уровня. При загрузке фрагментов мы делаем эту родительскую сцену «активной» сценой в менеджере сцен Unity, чтобы её параметры использовались для всей сцены.

NavMesh


Мы используем систему NavMesh Unity для прокладывания маршрутов всех наёмников и NPC. Когда мы разрабатывали эту процедурную систему, движок Unity ещё не поддерживал динамическое склеивание нескольких NavMesh в процессе выполнения игры. В Unity эта функция добавилась в новой бете с версии 5.6, но на момент написания статьи она ещё не готова. Поэтому нам пришлось решать проблему загрузкой нескольких случайных фрагментов уровней и применения их вместе с одним NavMesh. По отзывам самих разработчиков Unity, единственным способом добиться этого было создание единой большой области NavMesh и разрезание её на каждый фрагмент уровня. Поэтому в родительской сцене мы создали один большой плоский NavMesh, покрывающий всю игровую область. Затем мы использовали компонент «NavMeshObstacle» Unity и заставили его вырезать все стены и префабы. Затем, когда они загружаются и размещаются на NavMesh, они вырезаются из него, оставляя только основной маршрут для движения.

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

image

После загрузки каждого фрагмента и прокладывания путей мы хотели добавить фрагментам вариативности, чтобы дублируемые фрагменты не выглядели одинаковыми. Для этого мы создали процедурные объёмы препятствий (Procedural Obstacle Volumes), генерирующие случайные объекты оформления и укрытия в каждом фрагменте уровня.

Procedural Obstacle Volumes


Procedural Obstacle Volume — это объём пространства, в котором генерируются выбранные случайным образом префабы объектов укрытий. Они позволяют нам добавить вариативности фрагментам уровней, даже при загрузке дублирующихся фрагментов.

Сначала мы создали добавляемый к каждому префабу компонент «ProceduralObstacle», который можно загрузить через систему. Компонент отслеживает размер границ префаба, метки и другие настройки. При каждом обновлении префаба в редакторе он автоматически обновляется в глобальном манифесте ScriptableObject. Этот манифест затем используется для проверки префабов при загрузке в процедурный объём.

image

Затем мы помещаем игровые объекты Procedural Obstacle Volume внутрь фрагментов уровней. Эти объёмы имеют настройки, позволяющие на основании меток и направлений подбирать префабы, которые можно сгенерировать. Например, нам может потребоваться, чтобы область была заполнена только префабами, предназначенными для открытых пространств, которые разрушаемы и являются укрытиями только в определённом направлении. При загрузке уровней эти объёмы обрабатываются и случайным образом подбирают соответствующие префабы. Вся эта обработка основана на начальном числе (seed), генерирующем уровень (поэтому процесс детерминирован и пригоден для совместной игры по сети). Затем мы используем стандартный алгоритм упаковки в контейнеры (bin packing) для как можно более плотного размещения префабов в объёме с небольшим буфером вокруг, чтобы наёмники могли прокладывать путь между ними.

image

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

image

Теперь, когда мы закончили с генерированием маршрутов на уровнях, препятствиями, настройками освещения и NavMesh, нам нужно населить уровни врагами!

NPC и кривые темпа


При создании каждого фрагмента уровня для M.E.R.C. мы стратегически расположили точки создания NPC внутри каждого фрагмента. Однако мы не хотели, чтобы во время игры создавались все враги, это была бы настоящая мясорубка. Вместо этого при загрузке процедурного уровня мы определяем, какую точку включить на основании кривой темпа (Tempo Curve).

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

image

При загрузке процедурной миссии она имеет базовую сложность миссии. Мы случайным образом выбираем кривую темпа из списка и оцениваем точки кривой друг относительно друга. Это значит, что где бы точки ни находились на графике, наш код находит самую нижнюю и считает её нулём или уровнем «Не создавать врагов». Потом он находит самую высокую точку и считает её «Самым сложным» уровнем. Затем он распределяет промежуточные модификаторы сложности с одинаковым приращением между самой низкой и самой высокой точками следующим образом:

  • Не создавать врагов (самая низкая точка)
  • Самый простой (создавать самых лёгких NPC)
  • Простой (создавать лёгких NPC)
  • Базовый (соответствует сложности миссии)
  • Сложный (создавать серьёзных NPC)
  • Самый сложный (самая высокая точка)

Например, если основной маршрут состоит из 9 фрагментов, то точки кривой темпа, показанной выше, распределяются от начала до конца уровня. Первому фрагменту присваивается модификатор «Не создавать врагов», четвёртой точке (или седьмому фрагменту) — модификатор «Самый сложный», а последней точке (или последнему фрагменту) — «Не создавать врагов». Все остальные фрагменты распределяются по точкам кривой и определяют темп создания NPC в этих фрагментах. Результатом является «пульс» прохождения уровня в отношении сложности NPC. Представленный выше график медленно растёт и постепенно доходит до крупной битвы перед концом основного пути. Почти в каждом фрагменте этого примера создаются NPC, за исключением первого и последнего фрагмента, поэтому на уровне будут постоянно присутствовать враги.

NPC в M.E.R.C. создаются с помощью системы очков. У нас есть полная таблица очков, основанная на текущем уровне отряда и рейтинге сложности миссии. Из неё мы определяем, какое количество очков нужно потратить при создании NPC. Если модификатор сложности равен «Базовому», то создаются NPC, соответствующие уровню отряда для выбранной сложности миссии. Если модификатор равен «Простому» или «Самому простому», то часть очков при создании NPC вычитается. Аналогично, «Сложный» и «Самый сложный» добавляют очки на создание врагов. В точках создания эти очки используются для определения количества и уровня создаваемых NPC.

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

Итог


M.E.R.C. — это игра, рассчитанная на повторные прохождения. Поэтому она сильно выигрывает от использования системы процедурной генерации уровней.

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

Сейчас M.E.R.C. находится на этапе раннего доступа в Steam, и мы будем рады вашим отзывам и поддержке.

Пример загрузки процедурного уровня M.E.R.C., вид сверху:

image
Поделиться с друзьями
-->

Комментарии (0)