Die Vorlage Verse – Prozedurales Bauen ist ein Beispielprojekt aus dem Unreal Editor für Fortnite (UEFN) Re: Stelle dir London vor (Inselcode: 1442-4257-4418), das das in Verse geschriebene prozedurale Bausystem vorstellt.
Das prozedurale Bausystem ist vollständig in Verse und nicht in Fortnite implementiert und erstellt. Mit diesem System hast du mehr Kontrolle darüber, wie Meshs in deinen Projekten zusammengestellt werden.
Über Fortnite kannst du dein Gameplay mit voreingestellten Gebäudeteilen (Boden, Decke und Wand) gestalten. Das prozedurale Bausystem erweitert deine Möglichkeiten, so dass du Voxel mit Kategorien erstellen kannst, die zugrunde liegende Systeme festlegen, in denen du schnell zusätzliche Voxel platzieren kannst, wenn das System sein Mesh platziert.
Navigiere im Projektbrowser von UEFN zu Funktionsbeispiele > Spielbeispiele > Verse – Prozedurales Bauen, um auf diese Vorlage zuzugreifen.

Das prozedurale Bausystem funktioniert in zwei Phasen.
-
In der ersten Phase bearbeitest du ein Voxel-Raster, ein 3D-Raster, bei dem jede Zelle eine Box ist, indem du Voxel hinzufügst oder entfernst.
-
Die zweite Phase wird jedes Mal ausgeführt, wenn das Voxel-Raster geändert wird. Das Ändern des Voxel-Rasters nimmt die Voxel-Informationen und führt einen Prozess aus, der die richtigen Meshs an den richtigen Stellen erzeugt.
Du kannst die Voxel-Kategorien Gebäude und Park hinzufügen und entfernen. Jede Kategorie hat andere Regeln, wie Meshs gespawnt werden sollen. Diese Regeln beziehen sich auf das Diagramm oder den Baum der Operationen, die ausgeführt werden, um von platzierten Voxeln zu tatsächlichen Meshs zu gelangen
Verwenden der Vorlage
Jedes Level in deinem Projekt hat eine Top-Level-Klasse, und zwar die root_device class
. Wenn ein Spieler dem Spiel beitritt, erstellt die Klasse root_device
ein global_player_data
für ihn, das seine UI-Informationen festlegt.
Jede Bauzone hat ein build_zone
-Gerät, das die Abmessungen des Standorts in Voxel-Einheiten definiert. Die Position dieses Geräts definiert den Ursprung des Standorts. Das Baugebiet verwendet ein build_system
-Objekt, um Baumechaniken zu handhaben, und ein spawner_device
, um Gebäude-Prop-Meshs zu spawnen. Das Baugebiet enthält auch ein voxel_grid
, ein mesh_grid
und ein wfc_system
.
Jedes Baugebiet enthält Folgendes:
-
build_zone
(Gerät) – definiert die Abmessungen des Standorts in Voxel-Einheiten. -
build_system
– behandelt Baumechaniken. -
spawner_device
(Gerät) – Spawnt Gebäude-Prop-Meshs. -
spawner_asset_references
(Gerät) – verweist auf alle gespawnten Props.
Wenn ein Spieler eine build_zone
betritt, die durch ein Volume- Gerät ausgelöst wird, wird player_data
erstellt, um den Input und die Voxel-Bearbeitung dieses Spielers zu verarbeiten.
Bauen mit Voxeln
Diese Vorlage führt einen vector3i [type]()
ein, um Voxel durch ihre X
-, Y
- und Z
[integer]()
-Koordinaten darzustellen.
Unten siehst du ein Beispiel-Script mit vector3i
.
# 3-dimensionaler Vektor mit `int`-Komponenten
vector3i<public> := struct<computes><concrete>:
@editable
X<public>:int = 0
@editable
Y<public>:int = 0
@editable
Z<public>:int = 0
Voxel-Raster
Ein Voxel-Raster ist ein 3D-Raster von Zellen für jede Bauzone, das Informationen über die Art des vorhandenen Gebäude-Voxels speichert. Dies ist in der Klasse voxel_grid
als 1D-Array mit optionalen Verweisen auf voxel_cell [objects]()
implementiert, wie unten gezeigt.
# Haupt-Array, das 3D-Raster von Voxeln darstellt
var Cells<public> : []?voxel_cell = array{}
Standardmäßig enthält ein Voxel-Zellenobjekt nur ein [enum]()
für den Voxel-Typ, wie unten gezeigt.
# Kategorie von Voxel in unserem Bauraster
build_category<public> := enum:
Building
Park
NewBuildingType
# Struktur, die für jedes belegte Voxel gespeichert wird
voxel_cell<public> := struct<computes>:
Kategorie<public>:build_category
Du kannst weitere Gebäude-Voxel-Kategorien hinzufügen, indem du die Enum wie oben gezeigt erweiterst.
Der folgende Code wandelt eine 3D-Voxelkoordinate in einen 1D-[index]()
um.
# Ruft den 1D-Array-Index von einer 3D-Position im Raster ab
GetVoxelIndex<public>(X:int, Y:int, Z:int)<transacts>:int=
return (X * (Size.Y*Size.Z)) + (Y * Size.Z) + Z
SetVoxel
und ClearVoxel
sind Schlüsselfunktionen, die das Voxel-Raster verändern.
Raycasting
Nachdem das Voxel-Raster festgelegt wurde, führt das System eine Strahlenkollisionsprüfung durch, um die Voxel-Fläche bzw. die Seite des Voxels, die der Strahl trifft, zu prüfen. Jedes Voxel hat sechs Flächen, ähnlich wie ein Würfel. Beim Raycasting musst du sowohl wissen, welches Voxel als auch welche Fläche du triffst, um das Highlight zu zeichnen und ein neues Voxel gegen diese Fläche zu spawnen.

Strahlenkollisionsprüfungen werden meistens in der Klasse ray_caster
behandelt. Dazu wird zunächst bestimmt, in welchem Voxel der Strahl beginnt, indem die Kameraposition in den lokalen Raum des Rasters transformiert wird. Dies wird dann durch Voxel-Dimensionen geteilt, wie unten gezeigt.
CurrentVoxel := vector3i:
X := Floor[InitialPosition.X / GridSize.X]
Y := Floor[InitialPosition.Y / GridSize.Y]
Z := Floor[InitialPosition.Z / GridSize.Z]
Die [Next]()
-Funktion wird wiederholt aufgerufen, um zu bestimmen, welches Voxel der Strahl als nächstes durchlaufen wird, wobei jedes Mal geprüft wird, ob das Voxel als fest angesehen wird.
Bausystem-Inputs
Die Funktion SelectModeTick_Trace
in player_data
wird für jeden Frame ausgeführt und regelt den Großteil der Voxel-Bearbeitung und Cursor-Aktualisierungslogik. Zwei [Input Trigger]()
-Geräte werden verwendet, um zu wissen, wann die Schaltflächen Feuer und Zielen gedrückt werden, und um die Logikvariablen PlaceItem
und DeleteItem
zu setzen.
Diese Funktion erfordert zusätzliche Logik, da Park-Voxel nur als Oberfläche betrachtet werden, was bedeutet, dass sie keine Strahlen blockieren und nur auf festen Voxeln (Boden oder Gebäude) existieren können. Du kannst die Funktion CategoryIsSurfaceOnly
aktualisieren, wenn du eine neue Kategorie hinzufügst, die nur die Oberfläche sein soll.
Der Turbo-Bau wird auch unterstützt, indem die Feuer-Taste gedrückt gehalten wird, um schnell mehrere Voxel in einer Ebene zu platzieren. Diese Funktion prüft auch, ob sich ein Spieler innerhalb eines Voxels befindet, bevor es zur Funktion CheckPlayerOverlapsVoxel
hinzugefügt wird.
Props spawnen
Dieses Beispielprojekt nutzt das Spawnen von Props zur [runtime]()
. Das creative_prop_asset
wird derzeit nicht automatisch in den Verse-Manifest-Dateien übernommen. Daher musst du ein Proxy-Objekt (eine Instanz von piece_type
in der Klasse piece_type_dir
) verwenden, um bestimmte Props in Verse zu referenzieren.
Das Gerät spawner_asset_references
verwendet dann ein @editable
-Feld für jedes Prop und eine Zuordnungstabelle vom Proxy zum tatsächlichen Asset. Um ein neues Mesh hinzuzufügen, musst du zuerst ein BuildingProp
dafür erstellen, einen neuen Proxy hinzufügen, eine Eigenschaft zum Gerät hinzufügen und dann die Zuordnungstabelle aktualisieren (wie unten gezeigt). Zum Schluss kompilierst und aktualisierst du die neue Eigenschaft auf dem Gerät, die auf das neue Prop verweist.
Building1_corner:piece_type := piece_type{}
@editable
BP_Building1_corner : creative_prop_asset = DefaultCreativePropAsset
PT.Building1_corner => BP_Building1_corner
Prozedurale Generierung in Verse
Dieses Beispiel implementiert zwei Arten der prozeduralen Generierung in Verse: Formgrammatik und Wellenfunktionskollaps. Die Formgrammatik wird auf 3D-Gebäude angewendet, während der Wellenfunktionskollaps für (flache) 2D-Bereiche verwendet wird.

Oben siehst du ein Beispiel für generierte Gebäude-Props.

Oben siehst du ein Beispiel für generierte Park-Props.
Für beide Techniken musst du einen Satz modularer Gebäudetyp-Props erstellen, die Verse dann zur Laufzeit spawnt. Dieser Code ist deterministisch und löscht und spawnt Props nur, wenn es erforderlich ist.
Formgrammatik
Alle Voxel für eine Kategorie werden in größere konvexe Boxen transformiert, um die sogenannte Formgrammatik anzuwenden.

Die Formgrammatik besteht aus einfachen Regeln, wobei jede Regel eine Box entgegennimmt und eine oder mehrere Unterboxen für nachfolgende Regeln generiert.

Beispielsweise könnte eine Regel eine hohe Box in ein 1-Voxel hohes Bodenstück zerschneiden, während die Ecken und Wände unterschiedlichen Regeln zugewiesen sind. Durch eine Sonderregel kann ein Prop in der gleichen Größe und Position wie die Box gespawnt werden.
Jede Regel wird als separate Verse-Klasse definiert, die von der Klasse vo_base
(Volumenoperator) abgeleitet ist. Diese werden in einem Regelsatzbaum innerhalb einer von rs_base
(Regelsatz) abgeleiteten Klasse zusammengestellt.
Dieser Ansatz vereinfacht die Erstellung neuer Regeln, ermöglicht das Experimentieren mit verschiedenen Ideen, weist jedem Gebäudetyp eigene Stile zu und ermöglicht die Zuweisung von unterschiedlichen Stilen zu jedem Gebäudetyp. Die Anwendung unterschiedlicher Regeln auf denselben Satz von Voxeln führt zu unterschiedlichen Ergebnissen.
Unten siehst du ein einfaches Beispiel für einen ` vo_sizecheck`-Volumenoperator.
# Prüfe, ob alle Abmessungen einer Box >= einer bestimmten Größe sind
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)
Sie überschreibt die in der Basisklasse definierte Funktion Operate
, prüft die Größe der Eingangsbox und entscheidet, welche der folgenden Regeln (vo_pass
oder vo_fail
) aufgerufen werden sollen.
Regelsätze mit vielen Volumenoperatoren lassen sich in Verse ganz einfach einrichten. Du kannst die Funktion setupRules
überschreiben und deine [operators]()
und deren [parameters]()
deklarieren. Der Startpunkt oder Stammoperator wird VO_root
wie unten gezeigt zugewiesen.
# Regeln für den Stil 'Building1'
rs_building1<public> := class(rs_base):
RuleSetName<override>:string = "Building1"
SetupRules<override>(PT:piece_type_dir):void=
[...]
# Regeln für eine Etage des Gebäudes
FloorRules := vo_cornerwallsplit:
VO_CornerLength1 := vo_corner:
VO_Corner := vo_prop:
Prop := PT.Building1_corner
VO_Face := vo_prop:
Prop := PT.Building1_face
VO_Covered := vo_prop:
Prop := PT.Building1_box
VO_Wall := vo_wall:
VO_Width1 := vo_facecoveragecheck_1voxel:
VO_Clear := vo_prop:
Prop := PT.Building1_face
VO_Covered := vo_prop:
Prop := PT.Building1_box
VO_Centre := RoofTileCheck
VO_TooSmall := vo_tilefill:
VO_Tile := FourWay
set VO_Root = vo_floorsplit:
VO_FloorHeight1 := FloorRules
Diese Methode erleichtert die Erstellung neuer Operatoren und Regelsätze für verschiedene Gebäudekategorien. Regelsätze werden in InitRuleSets
zugewiesen und in SelectRuleSet
für eine bestimmte Kategorie ausgewählt, die beide in build_system
sind.
Wellenfunktionskollaps
Wellenfunktionskollaps (WFC) ist eine Technik zur zufälligen Generierung eines Bereichs auf der Grundlage von Regeln, die bestimmen, wie Teile zusammenpassen können.
In dieser Implementierung kannst du einen Satz von Kacheln verwenden und dann angeben, welche davon benachbart sein können. Auf jede Kante werden Beschriftungen angewendet, und es können nur Kacheln mit entsprechenden Beschriftungen platziert werden.
Im Folgenden siehst du ein Beispiel für ein Kantenlabel aus der Klasse wfc_mode_factor
.
WaterEL:wfc_edge_label := wfc_edge_label:
Name:="Water"
Symmetric := true
Im Folgenden findest du ein Beispiel für die Mesh-Definition für das obige Beispiel.
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

Du kannst Beschriftungen im Uhrzeigersinn angeben, beginnend mit der Richtung +Y.

In diesem Beispiel ist die untere Kante „water-to-grass“, da jede Kante im Uhrzeigersinn betrachtet wird. Mit diesem System ist es ganz einfach, neue Labels und Meshs oder neue WFC-Modelle zu deinem Gameplay hinzuzufügen.
Der WFC-Algorithmus wählt eine Stelle auf dem Raster aus, wählt zufällig die möglichen Optionen aus und überträgt dann die Folgen dieser Auswahl auf die möglichen Optionen an anderen Orten. Dieser Prozess wird fortgesetzt, bis der gesamte Bereich generiert ist.
Die Klasse wfc_system
enthält den aktuellen Zustand aller Kacheln. Der spezielle Volumenoperator vo_wfc
liest den Zustand und spawnt die richtigen Meshs.
Mögliche Erweiterungen
Im Folgenden findest du einige Möglichkeiten, wie dieses Beispielprojekt in neue Erlebnisse umgewandelt werden kann.
Füge eine neue Voxel-Kategorie/Formgrammatik hinzu
-
Erweitere die Enum
build_category
-
Aktualisiere alle
case
-Anweisungen, die die neue Kategorie verarbeiten müssen -
Erstelle einen neuen Regelsatz, der von
rs_base
abgeleitet ist -
Aktualisiere
SelectRuleSet
, um den neuen Regelsatz für die neue Kategorie zu verwenden
Füge neue Meshs zum Park-WFC-Modell hinzu
-
Erstelle ein Gebäude-Prop für jedes neue Mesh (wie oben unter Props spawnen beschrieben).
-
Füge ein neues
wfc_edge_label
(falls gewünscht) zumGetParkModel
inwfc_model_factor
hinzu. -
Füge eine neue
wfc_mesh
-Instanz für jedes neue Mesh/Prop hinzu, das die Beschriftung für jede Kante definiert. -
Rufe
Model.AddMesh
für jedes neue Mesh auf.
Füge ein neues WFC-Modell hinzu
-
Füge eine neue Voxel-Kategorie für das neue Modell hinzu.
-
Füge eine neue Funktion zu
wfc_model_factor
hinzu, um das neue Modell zu erstellen. -
Füge ein neues
wfc_model
-Mitglied zubuild_system
hinzu (wiePark_WPCModel
). -
Füge eine neue Funktion wie
AddParkWCCtile
hinzu, die Kacheln nach dem neuen Modell hinzufügt. -
Modifiziere
SelectModeTick_Trace
, um die neue Funktion für die neue Kategorie aufzurufen.

Sie können Voxel auch prozedural erzeugen. Im Beispielprojekt gibt es Schaltflächen, die zufällige Gebäude- oder Parkbereiche zur Bauzone hinzufügen. Dies verwendet die Funktionen ClearRegion
und AddRegion
von build_system
und könnte als Ausgangspunkt für ein zufälliges Levelgenerierungssystem verwendet werden.
Verse-Performance
Der Verse-Code in diesem Beispielprojekt stellt sicher, dass der Code schnell genug ist, um Aktualisierungen in Echtzeit zu verarbeiten. Da der Umgang mit großen Arrays zu Leistungsproblemen führen kann, ist es wichtig, die folgenden Hinweise zu beachten:
- Das Verwenden einer
for
-Schleife zum Zurückgeben eines Arrays ist schneller, als es Element für Element aufzubauen, da jede Addition das Array kopiert, so dass es O(N2) sein kann. Zum Beispiel:
* set OptionalArray := for(I := 0 .. ArraySize-1):
false
-
Übergib große Arrays nicht per Wert, sondern füge sie stattdessen in ein Objekt ein und rufe Methoden auf oder übergib das Objekt.
-
Mehrdimensionale Arrays sind langsam, da der erste
[]
-Operator eine Kopie an den nächsten[]
-Operator übergibt. -
Der Aufruf von
.Length
bei einem Array erstellt tatsächlich eine Kopie des Arrays zu diesem Zeitpunkt, so dass es schneller sein kann, die Größe großer Arrays selbst im Auge zu behalten.
Es ist auch sehr hilfreich, das profile
-Makro zu verwenden, um besser zu verstehen, welche Teile des Codes die meiste Zeit in Anspruch nehmen.