11/06/2020

Стриминг уровней в UE4

Стриминг уровней — это загрузка уровней и их контента "на лету", что дает возможность создавать бесшовный (seamless) геймплей и левел-дизайн, не прерывая его на экраны загрузки. Позволяет заранее загружать уровни в память, чтобы быстро их отображать и прятать, создавая иллюзию непрерывности повествования. Как, например, в God of War.

Persistent уровень — основной уровень, который загружается на старте и в структуре которого будут содержаться все подуровни.

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

Always loaded — тип загрузки подуровня, название говорит само за себя — уровень загружается вместе с persistent и не выгружается, пока существует Persistent.

Blueprint — тип загрузки подуровня, название также говорящее — уровень загружается и выгружается из логики в blueprints.

Основных инструментов стриминга уровней в UE4 четыре:

  • Streaming Volumes
  • Load Streaming Level
  • Create Level Instance
  • World Composition

Их мы и рассмотрим отдельно и подробно. К данной статье в качестве доп.материалов идет мой UE4 проект на Git Lab c PDF презентацией. Все скриншоты сделаны из него.

Инструмент Streaming Volumes

Позволяет загружать подуровни, настраивая триггеры типа (и названия) Level Streaming Volumes. Сразу сделаю ремарку — данный способ не загружает и выгружает подуровни, а меняет для них свойство Hidden, что отключает видимость и коллизии, но при этом все подуровни — загружаются с загрузки persistent уровня. По этому я буду применять термин Выключает вместо Выгружает, и для Загрузки соответственно — Включение. Этот способ — самый простой в подготовке и реализации.

Подготовка

  1. Создать Persistent уровень.
  2. Создать подуровни с необходимым контентом, учитывая переходы между подуровнями в фиксированных местах.
  3. Добавить их в persistent (либо Drag'n'Drop из Content Browser, либо Add Existing в меню Levels окна Levels).
  4. Подуровни должны быть Always Loaded (тип загрузки).
  5. Расставить и настроить Level Streaming Volumes именно в persistent уровне.
Чтобы открыть окно Levels, нужно найти его в меню Window

Настройка

Теперь нужно добавить в Persistent уровень Level Streaming Volume акторы и настроить их. Их можно добавлять в уровень из окна Modes, вкладки Volumes.

Чтобы видеть в World Outliner в каком уровне или подуровне сейчас находится актор, можно поставить отображение Level во всплывающем меню аутлайнера.

Теперь в каждом Streaming Volume необходимо настроить, какой уровень он включает и выключает. В моём примере первый триггер, пока игрок стоит в нем, должен включать подуровни Level_A и Level_B.

Второй — Level_B и Level_C.

И третий — Level_C и Level_D.

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

Как только Player Character войдет в Streaming Volume — тот в свою очередь отобразит уровни, указанные в его настройках. Если с Level Streaming Volume-ом нет пересечений у Player Character (в том числе, его Camera Component) — он выключает указанные в нем уровни. В результате, когда я прихожу Player Character-ом красную комнату (Streaming Trigger 3), выключаются Level_A и Level_B с зеленой и фиолетовой комнатами.

Выводы

Плюсы: 

  • Без блюпринтов вообще, только настройка.
  • Можно запекать свет (весь контент может быть Static).
  • Корректно работают по сети out of box.

Минусы:

  • Триггеры срабатывают на Camera Component.
  • Контент в подуровнях необходимо заранее расставлять с учетом структуры подуровней.
  • Уровни загружены изначально и у них меняется только свойство hidden (уровень невидим, коллизии выключены, но он — загружен в память системы при загрузке persistent уровня). 

Функция Load Streaming Level

Данный метод позволяет загружать подуровни по имени (подуровня) при вызове функции Load Streaming Level. И в данном случае именно загружать. Уровни не загружены при старте persistent, и на их загрузку по триггеру и блюпринт-логике необходимы ресурсы вашей системы, следовательно, если уровень большой или в нем много контента — могут быть просадки FPS, будьте внимательны и правильно распределяйте контент по подуровням. 

Подготовка

  1. Создать Persistent уровень.
  2. Создать подуровни с необходимым контентом, учитывая переходы между подуровнями, в фиксированных местах.
  3. Добавить их в persistent.
  4. Подуровни должны быть типа загрузки Blueprint.
  5. Создать свой blueprint-триггер с логикой, завязанной на Load Streaming Level.
  6. Расставить и настроить свои кастомные blueprint-триггеры. Первый blueprint-триггер должен быть в persistent, чтобы начать цепочку загрузок уровней. Остальные могут располагаться в подуровнях или в persistent, зависит от удобства.

Настройка

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

Я создал актора с именем BP_CustomStreamingTrigger, он унаследован от обычного actor класса. Перечислю, что я в него добавил.

BP Custom Streaming Trigger — переменные

Я добавил три Instance Editable переменные, чтобы их можно было изменять непосредственно в сцене для выбранного актера:

Extent — 3D-Vector, который я использую в Construction Script, чтобы управлять размером триггера (компонент Box) в уровне при настройке.
LevelsToLoadNames / LevelsToUnloadNamesArray (список) переменных типа Name, этот список заполняется точными именами уровней, которые необходимо загрузить / выгрузить, когда Player Character попадает в триггер.

А также четыре переменных для работы логики:

CurrentLevelToLoad / CurrentLevelToUnload — переменные типа Name, хранят имя текущего уровня на загрузку / выгрузку.
CurrentLoadlevelID / CurrentUnloadlevelID — переменные типа Int, хранят индекс уровня на загрузку/выгрузку.

BP Custom Streaming Trigger — логика

В Event Graph-е происходит не очень много.

Сразу замечу, что логика не завернута в функции по двум причинам. 

Первая: это быстрое решение, чтобы показать результат, ну и — всегда есть куда рефакторить. 

Вторая: базовая функция движка Load Stream Level (как и Unload Stream Level) — является Скрытой (или Отложенной), это означает, что после того как функция была вызвана и до того как она завершит свою работу — повторные вызовы execution flow через неё не пройдут, и всё, что стоит на выполнении после неё — не выполнится. А когда она выполнится — исполнение пойдет от первого вызова (все последующие — не запомнятся).

В данной реализации по соприкосновении с blueprint-триггером (Begin Overlap) любого актора срабатывает сперва загрузка первого из списка на загрузку уровня, а следом — выгрузка первого из списка на выгрузку, соответственно. Если делать рефакторинг, то точно стоит определить, кто может взаимодействовать с этим триггером (либо через коллижн пресет, либо через проверку актора на выходе события Begin Overlap), имейте это ввиду.

После первых запусков загрузки / выгрузки уровней пойдет выполнение следующих, если в списках таковые есть.

В результате, когда я нахожусь Player Character-ом в зеленой комнате (BP_CustomStreamingTrigger_AB), загружены Level_A и Level_B с зеленой и фиолетовой комнатами, и выгружены Level_C и Level_D с желтой и красной.

Выводы

Плюсы: 

  • Свой кастомный код, достаточно простой, и своя настройка оверапов.
  • Можно запекать свет (весь контент может быть Static).
  • Корректно работают по сети out of box.

Минусы:

  • Контент в подуровнях необходимо заранее расставлять с учетом структуры подуровней.
  • Требуется простейшее знание Blueprints.

Функция Create Level Instance

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

Подготовка

  1. Создать Persistent уровень.
  2. Создать подуровни-заготовки с необходимым контентом, выставлять точки выходов из подуровней, а точка входа — либо в нулях, либо — сложная система определения смещения уровня (в примере — точки входа в нулях).
  3. Подуровни должны быть Blueprint (тип загрузки).
  4. Создать свой Blueprint-триггер с логикой, завязанной на Create Instance.
  5. Создать свой Blueprint, который будет служить точкой выхода из уровня.
  6. Создать логику глобального менеджера стриминга уровней (можно реализовать её в кастомном GameMode).
  7. Расставить и настроить все свои кастомные Blueprint акторы по всем подуровням-заготовкам.

Настройка

Давайте разберем те кастомные Blueprint-акторы, что я упомянул в подготовке. В первую очередь, в данном методе необходимы два актора: 

BP_Connection — будет указывать координаты и направление для загрузки инстанса (копии) подуровня-заготовки.
BP_CreateLevelInstance — аналогичный актору BP_CustomStreamingTrigger из предыдущего примера, но со своей логикой.

И немножко логики появилось в Level Blueprint нашего persistent и кастомном Game Mode.

BP Connection

Начнем с самого простого — актор BP_Connection.

В нём нет никакой собственной логики, только два компонента: StaticMesh и Arrow. Он просто является более удобной (на мой взгляд) альтернативой такому актеру как Target Point. Единственное, в нём есть Bool переменная Available, которая нужна, чтобы на занятом какой-то комнатой BP_Connection не появилось новой комнаты. По умолчанию она True.

BP Create Level Instance — переменные

Дальше смотрим на BP_CreateLevelInstance.

BP Create Level Instance — логика

У него в Construction script (как и у BP_CustomStreamingTrigger) — логика для настройки размера триггера в сцене. 

В Event Graph есть проверка на Character и после — вызов кастомной функции ManageLevelInstance.
Ещё у этого актера есть функция CustomLevelReset, но о ней чуть позже.
В ManageLevelInstance же происходит следующее:
В начале кастомный Do Once (вы можете использовать обычный макрос Do Once, если вам так удобнее) и сохранение референса Game Mode в переменную.

Дальше — цикл, который проходит по всем указанным в уровне с blueprint-триггером коннекторам (BP_Connection).

Тело цикла делает следующее:

Сперва проверяет — доступен ли для выставления комнаты коннектор и если доступен, то для начала — выключить эту доступность.

Далее — выбирает рандомно из доступных имен уровней тот, из которого будет делать инстанс и загружать его.

И вызывает функцию Create Instance, на вход которой нужен объект уровня (берем его по имени), и уникальное (это важно) имя инстанса. И чтобы создать уникальное имя инстанса, и чтобы оно было всегда новым, я реализовал увеличение индекса в Game Mode (следующий скриншот из Game Mode), что сделало этот счетчик для уникальных имен инстансов — глобальным (для игровой сессии).

И в финале тела цикла — я добавляю ссылку на созданный инстанс в список (Array) всех загруженных уровней (чтобы была возможность их всех разом выгрузить), в качестве координат (Transform) передаю местоположение и направление текущего Коннектора, для которого выполняется данное тело цикла и после — загружаю и делаю видимым инстанс уровня.

Что осталось сделать? Правильно выставить комнаты в подуровнях — вход в комнату должен быть в нулевых координатах мира. На скриншоте фиолетовая комната с одним выходом — в persistent уровне и она стартовая. Красная же комната — это подуровень Level_D, ассеты которой так выставлены, чтобы вход в комнату был в нулевых координатах мира.

Чтобы стало понятнее — если включить отображение всех уровней будет такое:

И настройка blueprint-триггера BP_CreateLevelInstance с указанием в нем всех выходных BP_Connection выглядит примерно так:

И последний штрих — я добавил возможность "Сбросить состояние подземелья".

В Event Graph блюпринта persistent уровня я на клавишу «1» вызываю функцию CustomReset, которая принадлежит GameMode.

В этой функции в цикле я прохожусь по всем BP_CreateLevelInstance, у них беру список всех ими созданных инстансов подуровней, все их выгружаю и чищу этот список. И в завершении — беру BP_CreateLevelInstance из persistent уровня которому добавил тег FirstOne, и для него вызываю ManageLevelInstatnce функцию (по сути — запускаю загрузку первой комнаты после стартовой).

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

Выводы

Плюсы: 

  • Свой кастомный код, но более сложный, своя настройка оверапов.
  • Множество копий заготовленных подуровней.
  • Рандомная генерация на лету — "огонь ваще".

Минусы:

  • Запечь свет не получится, если только это не глухие помещения (например, подземелья), т.к. вы будете крутить уровень. Или — не использовать rotation, только location для выставления подуровня.
  • Либо все подуровни выставлять в нулях, либо продумывать сложную систему смещений уровня.
  • Гораздо больше всего нужно предусматривать в своем коде, в т.ч. проверки, чтобы подуровни не появлялись там, где уже есть загруженные подуровни.
  • По сети — всё очень плохо и сломано out of box (чинить репликацию надо в “плюсах”).

Бесшовный открытый мир с World Composition

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

Подготовка

  1. Создать Persistent уровень.
  2. Включить для него в World Settings опцию Enable World Composition.
  3. Создать первый подуровень (в окне LevelsNew Level).
  4. В первом подуровне создать первый ландшафт (в окне ModesLandscape) .
  5. На его основе создавать все последующие ландшафты, которые редактируются совместно (ПКМ на ландшафте — подменю LandscapeAdd Adjacent Landscape Level — выбрать направление).
  6. Создать свой слой и установить дистанцию от камеры для стриминга уровня.
  7. Назначить всем подуровням созданный слой.

Настройка

Подробнее разберем настройки, необходимые, чтобы весь этот World Composition настроить. Сперва — Enable World Composition, включить его можно тут:

Создать первый террейн можно в ModesLandscape:

Чтобы увидеть композицию ваших террейнов, нужно открыть окно World Composition по кнопке Summon world composition окна Levels:

Чтобы добавить элемент террейна, нужно в World Composition сперва сделать Load для террейна, который вы хотите взять для исходный, а дальше — Add Adjacent Landscape Level и выбор направления, куда добавить относительно этого террейна новый участок.

Редактор предложит назвать и сохранить новый уровень с террейном. И вот добавленный террейн, который можно редактировать как один с остальными (бесшовно).

И результатом будет отличный террейн, состоящий из нескольких частей, которые автоматически будут подгружаться, когда Player Character дойдет до дистанции загрузки. 

На первом скриншоте Player Character вне дальности загрузки:

А тут — в дистанции загрузки:

Выводы

Плюсы: 

  • Без блюпринтов вообще, только настройка.
  • Можно запекать свет (весь контент может быть Static) или спокойно пользоваться динамическим освещением, это ж “опенворлд”!
  • Система сделана для удобства работы с ландшафтами, и именно с ними — идеальный вариант.
  • Корректно работают по сети out of box.

Минусы:

  • Триггеры срабатывают на Camera Component.
  • Контент в подуровнях необходимо заранее расставлять с учетом структуры подуровней.

Фишечки

Фишечка с террейнами

Можно импортировать в проект заготовленный в стороннем ПО и разделенный на сектора террейн, главное — именование секторов должно соответствовать правилу <name>X<n>_Y<n>.

Фишечка с C++

В плюсовом коде есть возможность сделать обертку функции Create Level Instance, которой на вход можно передавать путь к нужному подуровню, что дает возможность не добавлять в persistent уровень необходимые подуровни. Следовательно, можно пользоваться одним entry подуровнем с player spawner, а всё остальное подгружать на лету.

Но тут нужно будет ещё чинить репликацию в C++, так же как с Create Instance функцией.

Фишечка с освещением

Уровень может быть использован как Light Scenario (сценарий освещения), что позволяет запечь несколько видов освещения для одного и того же пространства. Но есть нюансы со sky light и sky sphere.

Заключение

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

Текст: Антон Токарев

Недавние статьи

Сценарий вертикального среза

16/09/2020
Алексей Савченко на конкретном примере показывает и рассказывает, как сделать очень важный для разработки игры документ. Образец документа — прилагается.

ON AIR: обратная сторона американских отелей

15/09/2020
Один из победителей гран-при всенародного конкурса разработчиков в прошлом году — хоррор от первого лица ON AIR — решил снова испытать удачу уже на UEDC-2020.

Dash? Dash… Dash!

14/09/2020
Программист многопользовательского экшена Flea Madness рассказывает о реализации одной из задуманных фич — даш-атаки. Эволюция подходов, варианты решения, примеры кода — все, как вы любите.