Часть первая
Процедурная генерация уровней — отличный способ добавить в игру больше контента и неожиданных сценариев. Для сюжетных миссий M.E.R.C. мы хотели создать большой набор сделанных вручную уровней, но осознавали, что нашей небольшой инди-команде не хватит времени или ресурсов на изготовление контента для такой большой игры. Кроме того, мы стремились добавить случайность и повысить реиграбельность игры. Процедурная генерация уровней позволила нам создать большой, бесконечно изменчивый мир, который мы не смогли бы получить, строя отдельные уровни вручную. Использование процедурной генерации позволяет добавить больше контента и улучшить игровой процесс.
Что такое M.E.R.C.? M.E.R.C. — это тактический симулятор отряда в реальном времени с видом сверху. Игрок одновременно управляет отрядом из четырёх наёмников в антиутопическом мире Неотопии, отдаёт приказы и активирует особые умения. Каждый наёмник отряда имеет собственные особые боевые, технические и хакерские навыки, которые необходимо использовать в миссиях. Визуально M.E.R.C. напоминает стиль «Бегущего по лезвию»: тёмные дождливые трущобы и крыши города со множеством извилистых улиц и неоновым освещением. Сюжет заключается в войне могущественных корпораций за контроль над Неотопией. Отряд нанимают для выполнения различных заданий корпораций, таких как похищение учёных конкурентов или убийство сотрудников-перебежчиков. Каждая полученная миссия влияет на отношения с разными корпорациями и в результате изменяет игровой мир. Учитывая всё это, давайте рассмотрим требования к процедурной генерации уровней.
Требования к уровням
Уровни представляют собой лабиринт из городских трущоб и крыш, но чтобы в них было интересно играть, они должны иметь особую структуру. Для создания больших уровней миссий мы решили собирать их из небольших фрагментов. Благодаря этому мы смогли бы изготовить вручную интересные и «многоразовые» фрагменты, добавить процедурную случайность и придать каждому уровню ощущение сделанного вручную. Для этого мы определили, что процедурный уровень должен:
- содержать один основной маршрут
- содержать 1-3 тупиковых пути для целей и собирания добычи
- содержать 1-3 коротких пути, используемых как альтернативные маршруты
- генерировать случайные объекты оформления в каждом фрагменте уровня
- генерировать врагов на основании «графика темпа»
- генерировать разные типы и уровни врагов на основании сложности миссии
- работать с системой NavMesh Unity
- быть детерминированным и генерировать абсолютно одинаковые уровни для совместной игры по сети
Эти требования подобраны с учётом нашего геймплея, мира и планируемой длительности миссий. В других играх будут собственные требований к системе процедурных уровней. Мы хотели, чтобы миссии можно было проходить всего за десять минут и в два раза медленнее при тщательном исследовании уровня. Это означало, что важным параметром стала небольшая вариативность в длинах путей.
Самым сложным было соблюсти все условия, создав при этом целостные и интересные уровни. Как нам удалось этого достичь? Если представить готовый уровень с его структурой, тайниками, темпом и целями миссий, то сложно понять, как создать всё это процедурно. Я решил, что проще будет разбить уровень на слои. Надо воспринимать процедурный уровень как создаваемый в несколько проходов, каждый из которых добавляет уровню новый слой сложности. Логично, что можно начать процесс с базового уровня. В нашем случае это структура (маршрут) уровня.
Генерирование маршрута уровня
Первая проблема системы процедурных уровней заключается в генерировании интересного основного маршрута, содержащего тупики и короткие пути. Мы нашли решение в докладе Зака Эйкмана (Zach Aikman) с Unite 2014 о генерировании уровней игры Galak-Z студией 17-BIT (полный доклад можно посмотреть здесь).
Доклад Зака очень интересен, и я советую его посмотреть. Расскажу о нём вкратце: для генерирования основного 2D-маршрута мы использовали модифицированный алгоритм кривой Гильберта. Он создаёт интересный и уникальный извилистый маршрут, идеально подходящий в качестве основы для наших уровней. Разработчики Galak-Z использовали его для маршрута в двухмерном виде сбоку, мы с его помощью создавали двухмерный вид сверху. Представьте, что на иллюстрации ниже показана схема улиц нашей игры в виде сверху.
(иллюстрация из доклада о Galak-Z)
После генерирования основного маршрута нужно оценить все возможные ячейки карты для создания коротких путей, и случайным образом выбрать часть из них. Короткий путь не позволяет сократить перемещение по всей карте, а просто срезает часть основного маршрута, например, чтобы избежать некоторых врагов. Мы добавили в код ограничения, чтобы короткие пути не были слишком длинными и частыми. При добавлении этих коротких путей во фрагменты уровней мы часто блокировали их дверями, которые можно «хакнуть», или другими препятствиями, требовавшими каких-нибудь действий игрока. Это добавляет больше вариативности в основной маршрут уровня и предоставляет ещё один способ проверить навыки игрока.
Затем мы случайным образом добавляем побочные пути с тупиками в местах, где нет ячеек карты. Они создают альтернативные пути, прерывающие игровой процесс, и идеально подходят для размещения добычи, скрытых тайников и особых целей миссий, например, поиск и устранение персонажа. В зависимости от целей миссии мы случайным образом генерируем от одного до трёх тупиковых пути для каждого уровня. Все особые цели миссии расположены в конце тупиков.
Задумка заключается в том, что отряд прибывает на транспортном судне в стартовую ячейку карты, с которой начинается основной маршрут. Затем игрок, управляя наёмниками, проводит их через уровень и выполняет цели, достигает конца основного маршрута и эвакуируется. На каждом уровне нужно обязательно выполнить цели миссии. Цели миссии расположены за пределами основного маршрута, что вынуждает игрока исследовать уровень. Другие тупики исследовать не обязательно, в них находятся тайники и дополнительная добыча. Это даёт игроку выбор: прорваться через уровень и быстро пройти миссию, или потратить больше времени на исследования.
После завершения генерирования полного маршрута с короткими путями и тупиками мы преобразуем его в список загружаемых фрагментов уровней. Каждый фрагмент уровня — это сцена Unity, поэтому каждой сцене мы дали название по шаблону, определяющему её конфигурацию. Сгенерировав уровень, мы преобразуем каждую ячейку карты в название сцены, соответствующее шаблону. Шаблон содержит тему фрагмента, соединения и обозначение вариации. Например, по шаблону <тема>_<соединения основного маршрута>_<соединение короткого пути>_<соединение тупика>_<вариация> фрагмент уровня может иметь название сцены «slums_03_-1_-1_A».
Тема — это визуальный стиль фрагментов уровней. Все фрагменты уровней одной темы должны бесшовно соединяться с любым другим фрагментом такой же темы. Например, все фрагменты уровней темы «slums» (трущобы) должны логически и графически сочетаться друг с другом в точках соединений. Мы также создали тему «rooftops» (крыши домов), в которой вместо соединённых друг с другом улиц крыши зданий соединены скатами и настилами. Обычно все фрагменты одной темы имеют одинаковый размер. Наши фрагменты в среднем имеют размер 40x40 единиц.
В соответствии со структурой кривых Гильберта и сгенерированных нами путей каждый фрагмент уровня будет иметь два соединения основного маршрута, ноль или одно соединение короткого пути и ноль или одно соединение тупика. Фрагмент уровня никогда не содержит одновременно соединения короткого пути и тупика, потому что они никогда не используются. Каждое соединение соответствует грани фрагмента уровня, и каждая грань помечена цифрой от нуля до трёх, как показано на рисунке ниже (для пометки несуществующего соединения используется "-1", например, на рисунке нет соединения короткого пути).
Например, фрагмент уровня с обозначением соединения основного маршрута «03» имеет соединения внизу (0) и справа (3). Пометка соединения основного пути всегда имеет порядок по увеличению (т.е. «03», а не «30»). Важно помнить, что при сборке фрагментов уровней соединения необязательно должны выстраиваться абсолютно ровно. Добавив некоторым соединениям неровности, мы увеличим вариативность и уменьшим плавность соединения фрагментов. Однако соединения всё равно должны быть выравнены друг относительно друга, чтобы пути обязательно состыковывались.
Вариации позволяют дизайнеру уровней создавать разные версии одного фрагмента уровня. Например, посмотрите на две вариации «A» и «B» соединения основного маршрута «01» без короткого пути. При генерировании уровня система случайно выбирает одну из вариаций каждого фрагмента уровня, обеспечивая большее визуальное разнообразие.
При загрузке каждого фрагмента уровня мы перемещаем его на нужное смещение в мире на основании его положения на маршруте. Это значит, что фрагменты не могут содержать сеток (meshes), помеченных для статического батчинга в Unity (потому что при их перемещении система нарушится). Однако динамический батчинг Unity отлично работает в системе. Ниже представлены примеры случайно сгенерированных структур уровней (красные области — короткие пути через здания, синие — вариации зданий). Это пока только проверка концепции без окончательного оформления.
Буду рад более подробно обсудить эту тему и с удовольствием приму любые предложения. Со мной можно связаться в Твиттере.
Во второй части статьи «Процедурная генерация уровней для M.E.R.C. в Unity» мы обсудим решённые нами проблемы освещения и NavMesh, а также процесс генерирования персонажей в миссии на основании темпа. Ждите продолжения!
Автор: PatientZero