Шаблон Verse: Procedural Building — это пример проекта из Unreal Editor для Fortnite (UEFN) Re: Изображение Лондона (Код острова: 1442-4257-4418), в котором продемонстрирована система Procedural Building, написанная на Verse.
Система Procedural Building полностью реализована и создана на Verse, а не в Fortnite. Она предоставляет расширенное управление созданием сеток в проектах.
В Fortnite вы можете создать игру, используя заготовки для постройки (пол, потолок и стены). Система Procedural Building предоставляет вам больше возможностей: с ней вы можете создавать воксели с категориями, настраивающими базовые системы, в которых можно быстро размещать дополнительные воксели по мере размещения сетки системой.
Чтобы получить доступ к этому шаблону в Каталоге проектов UEFN, выберите Демонстрация возможностей > Примеры игр > Verse: Procedural Building.
Система Procedural Building состоит из двух этапов.
На первом этапе вы редактируете решётку вокселей — трёхмерную решётку, в которой каждая ячейка является кубоидом, — добавляя или удаляя воксели.
Вторая фаза запускается при каждом изменении решётки вокселей. При изменении решётки вокселей извлекаются данные о вокселях и запускается процесс, который создаёт нужные сетки в нужных местах.
Вы можете добавлять и удалять категории вокселей Постройка и Парк. В каждой категории есть свои правила создания сеток. Эти правила относятся к графу или дереву операций, которые будут выполняться для перехода от размещённых вокселей к фактическим сеткам.
Использование шаблона
Каждый уровень в проекте будет иметь один класс верхнего уровня root_device class. В момент присоединения игрока к игре класс root_device создаёт для него global_player_data, где задаются данные интерфейса для этого игрока.
В каждой зоне постройки расположено устройство build_zone, которое задаёт размеры участка в вокселях. Положение этого устройства определяет точку начала координат участка. В зоне постройки для реализации механики строительства используется объект build_system, а для создания сеток объектов окружения — spawner_device. В ней также есть voxel_grid, mesh_grid и wfc_system.
В каждой зоне постройки будут находиться следующие элементы:
build_zone(устройство): определяет размеры участка в вокселях.build_system: обрабатывает механику строительства.spawner_device(устройство): создаёт сетки объектов окружения.spawner_asset_references(устройство): ссылки на все созданные объекты окружения.
Когда игрок входит в зону build_zone, которая активируется устройством Область, для обработки входных данных этого игрока и редактирования вокселей создаётся массив player_data.
Строительство с помощью вокселей
В этом шаблоне вводится тип vector3i [type]() для представления вокселей с помощью координат X, Y и Z типа [integer]().
Ниже приведён пример сценария с использованием vector3i.
vector3i<public> := struct<computes><concrete>:
@editable
X<public>:int = 0
@editable
Y<public>:int = 0
@editable
Z<public>:int = 0Решётки вокселей
Решётка вокселей — это трёхмерная решётка ячеек для каждой зоны постройки, в которой хранится информация о типе постройки с помощью вокселей. Это реализовано в классе voxel_grid в виде одномерного массива необязательных ссылок на объекты voxel_cell [objects](), как показано ниже.
# Main array representing the 3D grid of voxels
var Cells<public> : []?voxel_cell = array{}По умолчанию объект ячейки вокселя содержит только [enum]() для типа вокселя, как показано ниже.
# Category of voxel in our build grid
build_category<public> := enum:
Building
Park
NewBuildingType
# Structure stored for each occupied voxel
voxel_cell<public> := struct<computes>:
Category<public>:build_categoryВы можете добавить новые категории вокселей постройки, расширив enum, как показано выше.
Код ниже позволяет преобразовать координату трёхмерного вокселя в одномерный [index]().
# Gets the 1D array index from a 3D location in grid
GetVoxelIndex<public>(X:int, Y:int, Z:int)<transacts>:int=
return (X * (Size.Y*Size.Z)) + (Y * Size.Z) + ZSetVoxel и ClearVoxel — это ключевые функции, которые изменяют решётку вокселей.
Рейкастинг
После настройки решётки вокселей система запустит проверку коллизии лучей, чтобы проверить грань вокселя или ту сторону вокселя, на которую попадает луч. Каждый воксель имеет шесть граней, как игровые кости. При рейкастинге вам нужно определить, в какой воксель и на какую грань попал луч, чтобы отрисовать световой эффект и создать новый воксель на этой грани.
Проверки коллизии лучей в основном выполняются в классе ray_caster. Для этого путём преобразования положения камеры в локальное пространство решётки сначала определяется, в каком вокселе начинается луч. Затем он разделяется по размерам вокселей, как показано ниже.
CurrentVoxel := vector3i:
X := Floor[InitialPosition.X / GridSize.X]
Y := Floor[InitialPosition.Y / GridSize.Y]
Z := Floor[InitialPosition.Z / GridSize.Z]
Функция [Next]() вызывается повторно, чтобы определить, через какой воксель должен пройти следующий луч, при этом каждый раз проверяется, является ли воксель сплошным.
Входные данные системы постройки
Функция SelectModeTick_Trace в player_data выполняется для каждого кадра и отвечает за большую часть логики редактирования вокселей и обновления курсора. Два устройства [Input Trigger]() используются для определения момента, когда нажаты кнопки Стрелять и Прицеливаться, а также для задания логических переменных PlacePiece и DeletePiece.
Для этой функции требуется дополнительная логика, поскольку воксели Парк учитываются только на поверхности, то есть они не блокируют лучи и могут существовать только поверх сплошных вокселей (поверхности или построек). Вы можете обновить функцию CategoryIsSurfaceOnly при добавлении новой категории, которая должна быть только поверхностью.
Также можно использовать функцию ускоренного строительства, для чего нужно удерживать кнопку Стрелять, чтобы быстро разместить несколько вокселей на плоскости. Эта функция также проверяет, находится ли игрок внутри вокселя, прежде чем добавлять его в функцию CheckPlayerOverlapsVoxel.
Создание объектов окружения
В проекте в этом примере объекты окружения создаются в [runtime](). В настоящее время creative_prop_asset не отражается автоматически в файлах манифеста Verse. Поэтому для ссылки на отдельные объекты окружения в Verse необходимо использовать прокси-объект (экземпляр piece_type в классе piece_type_dir).
Далее, устройство spawner_asset_references использует поле @editable для каждого объекта окружения и таблицу сопоставления прокси-объекта непосредственно с ресурсом. Чтобы добавить новую сетку, нужно сначала создать для неё объект BuildingProp, добавить новый прокси-объект, добавить свойство в устройство, а затем обновить таблицу сопоставления (как показано ниже). В конце перекомпилируем и обновим новое свойство устройства, чтобы оно указывало на новый объект окружения.
Building1_corner:piece_type := piece_type{}
@editable
BP_Building1_corner : creative_prop_asset = DefaultCreativePropAsset
PT.Building1_corner => BP_Building1_corner
Процедурная генерация в Verse
В этом примере реализуются два типа процедурной генерации в Verse: Грамматика формы и Коллапс волновой функции. Грамматика формы применяется к трёхмерным постройкам, а коллапс волновой функции — к двухмерным (плоским) областям.
Выше приведён пример сгенерированных объектов-зданий.
Выше приведён пример сгенерированных объектов для парка.
Для использования обоих методов необходимо создать набор модульных объектов окружения типа здания, которые Verse затем создаст в среде выполнения. Этот код является детерминированным, он удаляет и создаёт объекты окружения только по мере необходимости.
Грамматика формы
Все воксели для категории преобразуются в более крупные выпуклые кубоиды, чтобы можно было применить так называемую грамматику формы.
Грамматика формы состоит из простых правил, где каждое правило берёт кубоид и создаёт один или несколько вложенных кубоидов для последующих правил.
Например, одно правило может сделать из высокого кубоида высоко расположенный элемент, состоящий из одного вокселя, тогда как углы и стены будут назначены для других правил. Особое правило может создать объект окружения в том же размере и в месте, что и кубоид.
Каждое правило определяется как отдельный класс Verse, наследующийся от класса (оператора области) vo_base. Они объединяются в дерево с набором правил внутри класса, производного от (набора правил) rs_base.
Такой подход упрощает создание новых правил, позволяет экспериментировать с различными идеями, назначать/способствовать назначению отдельных стилей для каждого типа постройки. Применение разных правил к одному и тому же набору вокселей выдаёт разные результаты.
Ниже приведён простой пример оператора области ` vo_sizecheck`.
# Check all dimensions of a box are >= a certain size
vo_sizecheck := class(vo_base):
Size:vector3i = vector3i{X:=0, Y:=0, Z:=0}
VO_Pass:vo_base = vo_base{}
VO_Fail:vo_base = vo_base{}
Operate<override>(Box:voxel_box, Rot:prop_yaw, BuildSystem:build_system):void=
if(Box.Size.X >= Size.X and Box.Size.Y >= Size.Y and Box.Size.Z >= Size.Z):
VO_Pass.Operate(Box, Rot, BuildSystem)
else:
VO_Fail.Operate(Box, Rot, BuildSystem)Он переопределяет функцию Operate, определённую в базовом классе, проверяет размер входного кубоида и определяет, какое из следующих правил вызвать (vo_pass или vo_fail).
В Verse легко настраивать наборы правил, содержащие множество операторов области. Можно переопределить функцию setupRules и объявить [operators]() и их [parameters](). Начальная точка, или корневой оператор, назначается для VO_root, как показано ниже.
# Rules for 'Building1' style
rs_building1<public> := class(rs_base):
RuleSetName<override>:string = "Building1"
SetupRules<override>(PT:piece_type_dir):void=
[...]
# Rules for one floor of the building
FloorRules := vo_cornerwallsplit:
VO_CornerLength1 := vo_corner:
VO_Corner := vo_prop:
Prop := PT.Building1_corner
Этот метод позволяет легко создавать новые операторы и наборы правил для различных категорий построек. Наборы правил размещаются в InitRuleSets и выбираются для определённой категории в SelectRuleSet (оба набора находятся в build_system).
Коллапс волновой функции
Коллапс волновой функции (WFC) — это метод случайной генерации области на основе правил, определяющих способы совмещения частей.
При использовании этого метода можно выбрать набор плиток, а затем указать, какие из них будут расположены рядом. К каждому ребру применяются метки, и размещать можно только плитки с совпадающими метками.
Ниже приведён пример метки ребра из класса wfc_mode_factory.
WaterEL:wfc_edge_label := wfc_edge_label:
Name:="Water"
Symmetric := true
Ниже представлен пример определения сетки для приведённого выше примера.
Park_Grass_Water_InCorn:wfc_mesh := wfc_mesh:
Props := array:
PT.Park_Grass_Water_Incorn
Name := "Park_Grass_Water_Incorn"
Edges := array:
WaterEL
WaterEL
WaterToGrassEL
GrassToWaterEL
Вы можете указывать метки по часовой стрелке, начиная с направления +Y.
В этом примере нижнее ребро создано в направлении «от воды к траве», ведь рёбра создавались по часовой стрелке. С помощью этой системы можно легко добавлять новые метки и сетки, либо новые модели WFC в игру.
Алгоритм WPC выбирает место на решётке, случайным образом выбирает или скрывает какой-либо из возможных вариантов, затем переносит последствия этого выбора на возможные варианты в других местах. Этот процесс будет продолжаться до тех пор, пока не будет сгенерирована вся область.
Класс wfc_system содержит текущее состояние всех плиток. Специальный оператор области vo_wfc считывает состояние и создаёт правильные сетки.
Возможные расширения
Ниже приведено несколько способов преобразования этого примера проекта в новые игры.
Добавьте новую категорию вокселей / грамматику формы
Расширьте перечисление
build_categoryОбновите все выражения
case, которые должны обрабатывать новую категориюСоздайте новый набор правил, производный от
rs_baseОбновите
SelectRuleSet, чтобы использовать новый набор правил для новой категории
Добавьте новые сетки в модель WFC парка
Создайте объект-здание для каждой новой сетки (как описано в разделе «Создание объектов окружения» выше).
При необходимости добавьте новую метку
wfc_edge_labelкGetParkModelвwfc_model_factory.Добавьте новый экземпляр
wfc_meshдля каждой новой сетки или объекта окружения, определив метку для каждого ребра.Вызовите
Model.AddMeshдля каждой новой сетки.
Добавьте новую модель WFC
Добавьте новую категорию вокселей для новой модели.
Добавьте новую функцию в
wfc_model_factoryдля создания новой модели.Добавьте новую составляющую
wfc_modelвbuild_system(например,Park_WFCModel).Добавьте новую функцию, например
AddParkWFCTile, которая будет добавлять плитки, используя новую модель.Внесите изменения в
SelectModeTick_Traceдля вызова новой функции для новой категории.
Вы также можете генерировать воксели процедурно. В примере проекта есть кнопки, которые добавляют случайные области для постройки или парка в зону строительства. В нём используются функции ClearRegion и AddRegion из build_system; этот проект можно использовать как отправную точку для системы случайной генерации уровней.
Производительность Verse
Код Verse в этом примере проекта выполняется достаточно быстро для обработки обновлений в реальном времени. Поскольку работа с большими массивами может привести к проблемам с производительностью, важно учитывать следующее:
Использование цикла
forдля возврата массива выполняется быстрее, чем его создание поэлементно, поскольку каждое добавление копирует массив, чтобы он мог быть O(N2). Пример:Verse* set OptionalArray := for(I := 0 .. ArraySize-1): falseНе передавайте большие массивы по значению — лучше поместите их в объект и вызовите методы или передайте объект.
Многомерные массивы обрабатываются медленно, поскольку первый оператор
[]передаёт копию следующему оператору[].Вызов
.Lengthдля массива фактически создаёт копию массива, поэтому самостоятельно отслеживать размер больших массивов будет быстрее.
Также очень полезно использовать макрос profile, чтобы лучше понять, какие именно части кода выполняются дольше всего.