Стриминг уровней в 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 уровня. По этому я буду применять термин Выключает вместо Выгружает, и для Загрузки соответственно — Включение. Этот способ — самый простой в подготовке и реализации.
Подготовка
- Создать Persistent уровень.
- Создать подуровни с необходимым контентом, учитывая переходы между подуровнями в фиксированных местах.
- Добавить их в persistent (либо Drag'n'Drop из Content Browser, либо Add Existing в меню Levels окна Levels).
- Подуровни должны быть Always Loaded (тип загрузки).
- Расставить и настроить Level Streaming Volumes именно в persistent уровне.
Настройка
Теперь нужно добавить в 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, будьте внимательны и правильно распределяйте контент по подуровням.
Подготовка
- Создать Persistent уровень.
- Создать подуровни с необходимым контентом, учитывая переходы между подуровнями, в фиксированных местах.
- Добавить их в persistent.
- Подуровни должны быть типа загрузки Blueprint.
- Создать свой blueprint-триггер с логикой, завязанной на Load Streaming Level.
- Расставить и настроить свои кастомные blueprint-триггеры. Первый blueprint-триггер должен быть в persistent, чтобы начать цепочку загрузок уровней. Остальные могут располагаться в подуровнях или в persistent, зависит от удобства.
Настройка
В подготовке я уже сказал, что нужно расставить blueprint-триггеры, но не рассказал, что же должно быть у них внутри, какая логика. Срочно исправляю это упущение!
Я создал актора с именем BP_CustomStreamingTrigger, он унаследован от обычного actor класса. Перечислю, что я в него добавил.
BP Custom Streaming Trigger — переменные
Я добавил три Instance Editable переменные, чтобы их можно было изменять непосредственно в сцене для выбранного актера:
Extent — 3D-Vector, который я использую в Construction Script, чтобы управлять размером триггера (компонент Box) в уровне при настройке.
LevelsToLoadNames / LevelsToUnloadNames — Array (список) переменных типа 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
Данный способ позволяет загружать множество инстансов (копий) уровней из заготовок (подуровней), которые также можно вращать и подставлять в любые необходимые вам координаты. Проще говоря — можно делать рандомно-генерируемые подземелья по созданным паттернам.
Подготовка
- Создать Persistent уровень.
- Создать подуровни-заготовки с необходимым контентом, выставлять точки выходов из подуровней, а точка входа — либо в нулях, либо — сложная система определения смещения уровня (в примере — точки входа в нулях).
- Подуровни должны быть Blueprint (тип загрузки).
- Создать свой Blueprint-триггер с логикой, завязанной на Create Instance.
- Создать свой Blueprint, который будет служить точкой выхода из уровня.
- Создать логику глобального менеджера стриминга уровней (можно реализовать её в кастомном GameMode).
- Расставить и настроить все свои кастомные 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
И последний из представленных мной способов — совокупность лендскейпов (террейнов), каждый из которых является подуровнем и подгружаются они за счет настроек расстояния.
Подготовка
- Создать Persistent уровень.
- Включить для него в World Settings опцию Enable World Composition.
- Создать первый подуровень (в окне Levels — New Level).
- В первом подуровне создать первый ландшафт (в окне Modes — Landscape) .
- На его основе создавать все последующие ландшафты, которые редактируются совместно (ПКМ на ландшафте — подменю Landscape — Add Adjacent Landscape Level — выбрать направление).
- Создать свой слой и установить дистанцию от камеры для стриминга уровня.
- Назначить всем подуровням созданный слой.
Настройка
Подробнее разберем настройки, необходимые, чтобы весь этот World Composition настроить. Сперва — Enable World Composition, включить его можно тут:
Создать первый террейн можно в Modes — Landscape:
Чтобы увидеть композицию ваших террейнов, нужно открыть окно 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 и иммерсивность игрового процесса за счет того, что игрок не прерывается на экраны загрузки — стоит многих часов разработки, а все вышеперечисленные методы различаются больше именно сложностью разработки и поддержки.
Текст: Антон Токарев