В последнее время я влюблен в процедурную генерацию. А точнее, используя его из редактора Unity для внесения изменений в сцену моей игры.

В моей игре The Godkiller у меня будут сотни уровней. И я хочу, чтобы они выглядели красиво, если не ААА качества. Но в моей команде нет артиста, кроме меня. И у меня уже есть несколько других работ, например, написание кода. Я всего лишь один парень, который может время от времени нанимать подрядчика для завершения своей игры. Это типичная ситуация для разработчиков инди-игр.

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

Генерация в режиме редактирования по сравнению с режимом воспроизведения

Почти весь мой код procgen выполняется в редакторе Unity, а не во время игры (либо из редактора player, либо из сборки). Всякий раз, когда я могу сделать procgen «только для редактора», я это делаю. Потому что это означает, что вычисления не будут нагружать процессор во время игры.

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

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

Много инструментов в наборе инструментов

Процедурная генерация охватывает огромное пространство. Некоторые типичные сценарии в играх:

  • Настройка значений карты высот для создания интересного ландшафта.
  • Добавление новых областей подземелья в стиле Rogue по мере их исследования игроком.
  • Создание деревьев, растений, водоемов и других природных объектов.
  • Создание текстур, которые не выглядят повторяющимися при покрытии больших поверхностей.

Мои методы в основном основаны на дополнении простой геометрии чем-то более интересным. Технически они были достигнуты следующими способами:

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

Это определенно не полный список всех методов, которые вы можете использовать для procgen в Unity. Теоретически можно использовать любой способ, которым вы могли бы программно манипулировать тем, что показано на сцене. Это включает в себя использование шейдеров, манипулирование вершинами, эффекты постобработки, настройку UV-координат и многое другое.

Совет № 1 — вы хотите изменить код режима

Я говорю «Режим редактирования», чтобы было ясно: я не имею в виду код, который запускается, пока игра играет в редакторе.

Как только вы начнете думать о том, что код работает либо в режиме воспроизведения, либо в режиме редактирования, вы поймете, что хотите, чтобы в режиме редактирования выполнялась как можно большая его часть. В вашей игре будет меньше работы. А другие функции режима редактирования, такие как световое картирование и запекание, могут работать после запуска вашего кода procgen.

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

Таким образом, вы можете делать такие вещи, как:

  • Выделяйте много памяти динамически, и не беспокойтесь о скачках частоты кадров, вызванных сборкой мусора из этой памяти. Используйте List.Add() сколько душе угодно!
  • Избегайте использования бухгалтерских переменных, которые могут усложнить ваш код. Идем дальше и вызываем GameObject.GetComponent() без сохранения результата для будущего использования.

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

При разработке игр старая поговорка о том, что нельзя оптимизировать преждевременно, бесполезна. Почему? Определение «преждевременный» более неуловимо для игр. В Unity создание пулов экземпляров GameObjects для таких вещей, как пули, не является «преждевременным», если вы заранее знаете, как эта задача обычно важна для предотвращения замедлений.

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

Совет № 2 — Защитите свой код режима редактирования

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

  • Код режима редактирования может быть неэффективным. Особенно, если вы пишете это с бесцеремонным отношением «кого волнует производительность», которое я рекомендовал в предыдущем разделе. Проблемы со сбором мусора особенно коварны.
  • Код режима редактирования может ссылаться на активы или код, которых нет в сборке плеера. В зависимости от того, как вы настроите свой проект, некоторые вещи будут скомпилированы.
  • Некоторые API-интерфейсы Unity следует вызывать только в режиме редактирования, или вы можете использовать их по-разному в режиме воспроизведения или режиме редактирования. Пример см. в разделах Destroy() и DestroyImmediate().
  • Как правило, вы хотите, чтобы у вас в голове было ясно, когда код предназначен для запуска — в игре или в редакторе, чтобы вы могли принимать правильные решения по этому поводу.

В другом направлении все в порядке — вы можете сделать так, чтобы код режима редактирования вызывал код режима воспроизведения, хотя для этого вам нужно знать о различиях API Unity.

Две вещи, которые помогают избежать вызова кода режима редактирования из кода режима воспроизведения:

  • Используйте отдельное пространство имен для кода режима редактирования. В The Godkiller у меня есть пространства имен «Godkiller» и «GodkillerEditor». Поэтому я должен явно добавить оператор «using» для ссылки на классы из другого пространства имен, что подскажет мне, что я собираюсь сделать что-то глупое и, возможно, мне нужно провести рефакторинг.
  • Включите код режима редактирования в определения «#if UNITY_EDITOR». Однако обратите внимание, что это будет компилировать код только при сборке проигрывателя. Это полезно, чтобы убедиться, что вы случайно не отправили код режима редактирования.

Совет № 3 — не меняйте сцену в OnValidate

Обработчик OnValidate() — это простой способ определить, когда значения инспектора для GameObject изменились в редакторе.

Я написал много кода, подобного тому, что вы видите ниже, — кода, который изменит что-то в сцене в ответ на то, что я обновлю переменную, влияющую на procgen, из Инспектора.

И этот код сработал. Но я продолжал получать эти раздражающие предупреждения, появляющиеся в консоли:

«SendMessage нельзя вызывать во время пробуждения, CheckConsistency или OnValidate»

Я бы тогда кричал вслух, как сумасшедший…

«Но я не вызывал SendMessage(), глупая Unity!»

…и другие гневные вариации этой мысли. В приведенном выше случае вызов SetParent() генерирует предупреждение. Где-то в недрах недоступного для меня кода Unity они вызывают SendMessage(), что вызывает предупреждение. Более полезным предупреждающим сообщением будет:

«SetParent не следует вызывать во время Awake, CheckConsistency или OnValidate. Поведение может быть ненадежным».

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

В большинстве случаев вызов SetParent() завершается успешно. Но иногда он дает сбой, что является наихудшей ошибкой — прерывистой. И дело не только в SetParent(). Другие API будут генерировать предупреждение «SendMessage» и периодически сбоить таким же образом. Чтобы быть в безопасности, вы можете использовать это общее правило: Не обновлять сцену в OnValidate().

Так как же правильно обновить сцену в ответ на изменение в Инспекторе?

Вы можете продолжать использовать обработчик OnValidate(), но просто используйте его, чтобы сигнализировать об изменениях сцены, выполненных немного позже внутри Update(). Вот так:

Приведенный выше код создает префаб дерева и устанавливает его высоту. Это полный пример изменения сцены в ответ на смену инспектора. Обратите внимание, что если вы переместите строки 22–24 вверх в функцию OnValidate(), это сработает. Но это вызовет ошибку SendMessage().

Раздражающим (по крайней мере для меня) поведением OnValidate является то, что он также будет вызываться при загрузке скрипта, даже если никакое значение в Инспекторе не изменилось. Это заставляет OnValidate() выполняться, когда вы открываете сцену или возвращаетесь в режим редактирования из режима воспроизведения. Чтобы избежать этого, вы можете добавить немного больше отслеживания состояния, например:

Существует альтернативный способ обойти предупреждение, которое приводит к более простому коду — использование delayCall. Но это создаст другие проблемы, когда код внутри запускается после того, как экземпляры GameObjects и другие ресурсы недоступны. В частности, это происходит при переходе из режима редактирования в режим воспроизведения.

Что еще мы хотим знать?

Мне кажется, что мы только царапаем поверхность того, что можно сделать с помощью procgen в играх. У многих есть эта проблема, связанная с созданием сложных, красивых вещей без армии художников. И procgen — весьма полезный способ создавать вещи для разработчика. Так что для меня это интересная тема.

Меня определенно интересуют мысли других на эту тему, и я могу написать об этом больше.