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, werden 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.
vector3i<public> := struct<computes><concrete>:
@editable
X<public>:int = 0
@editable
Y<public>:int = 0
@editable
Z<public>:int = 0Voxel-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.
# Main array representing the 3D grid of voxels
var Cells<public> : []?voxel_cell = array{}Standardmäßig enthält ein Voxel-Zellenobjekt nur ein [enum]() für den Voxel-Typ, wie unten gezeigt.
# 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_categoryDu 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.
# 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 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 PlacePiece und DeletePiece 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.
Spawnen von Props
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.
# 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)Er ü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.
# 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
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 Labels angewendet, und es können nur Kacheln mit entsprechenden Labels 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 Labels 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_baseabgeleitet 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) zumGetParkModelinwfc_model_factoryhinzu.Füge eine neue
wfc_mesh-Instanz für jedes neue Mesh/Prop hinzu, das das Label für jede Kante definiert.Rufe
Model.AddMeshfü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_factoryhinzu, um das neue Modell zu erstellen.Füge ein neues
wfc_model-Mitglied zubuild_systemhinzu (wiePark_WFCModel).Füge eine neue Funktion wie
AddParkWFCTilehinzu, die Kacheln mit dem neuen Modell hinzufügt.Modifiziere
SelectModeTick_Trace, um die neue Funktion für die neue Kategorie aufzurufen.
Du kannst 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 Level-Generierungssystem 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, sodass es O(N2) sein kann. Beispiel:Verse* set OptionalArray := for(I := 0 .. ArraySize-1): falseÜbergib große Arrays nicht nach 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
.Lengthauf ein Array erstellt tatsächlich eine Kopie des Arrays zu diesem Zeitpunkt, sodass es schneller sein kann, die Größe großer Arrays selbst im Auge zu behalten.
Es ist auch sehr hilfreich, das Profiling-Makro zu verwenden, um besser zu verstehen, welche Teile des Codes die meiste Zeit in Anspruch nehmen.