Die prozedurale Generierung wird verwendet, um die Variabilität von Spielen zu erhöhen. Bekannte Projekte sind
Minecraft ,
Enter the Gungeon und
Descenders . In diesem Beitrag werde ich einige der Algorithmen erläutern, die bei der Arbeit mit dem
Tilemap- System, das in Unity 2017.2 als 2D-Funktion angezeigt wurde, und mit
RuleTile verwendet werden können .
Mit der prozeduralen Erstellung von Karten wird jedes vorbeiziehende Spiel einzigartig. Sie können verschiedene Eingabedaten wie die Zeit oder die aktuelle Stufe des Spielers verwenden, um den Inhalt auch nach der Zusammenstellung des Spiels dynamisch zu ändern.
Worum geht es in diesem Beitrag?
Wir werden uns einige der gebräuchlichsten Methoden zum Erstellen von prozeduralen Welten sowie einige von mir erstellte Variationen ansehen. Hier ist ein Beispiel dafür, was Sie nach dem Lesen des Artikels erstellen können. Drei Algorithmen arbeiten zusammen, um mithilfe von
Tilemap und
RuleTile eine Karte zu erstellen:
Beim Generieren einer Karte mit einem beliebigen Algorithmus erhalten wir ein
int
Array, das alle neuen Daten enthält. Sie können diese Daten weiterhin ändern oder in eine Kachelzuordnung rendern.
Bevor Sie weiterlesen, wäre es schön, Folgendes zu wissen:
- Wir unterscheiden, was eine Kachel ist und was keine Binärwerte verwendet. 1 ist eine Kachel, 0 ist ihre Abwesenheit.
- Wir speichern alle Karten in einem zweidimensionalen Ganzzahl-Array, das am Ende jeder Funktion an den Benutzer zurückgegeben wird (mit Ausnahme derjenigen, in der das Rendern ausgeführt wird).
- Ich werde die Array-Funktion GetUpperBound () verwenden, um die Höhe und Breite jeder Karte zu ermitteln, damit die Funktion weniger Variablen empfängt und der Code sauberer ist.
- Ich verwende häufig Mathf.FloorToInt () , da das Tilemap-Koordinatensystem unten links beginnt und Sie mit Mathf.FloorToInt () Zahlen auf eine Ganzzahl runden können.
- Der gesamte Code in diesem Beitrag ist in C # geschrieben.
Array-Generierung
GenerateArray erstellt ein neues
int
Array mit der angegebenen Größe. Wir können auch angeben, ob das Array gefüllt oder leer sein soll (1 oder 0). Hier ist der Code:
public static int[,] GenerateArray(int width, int height, bool empty) { int[,] map = new int[width, height]; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { if (empty) { map[x, y] = 0; } else { map[x, y] = 1; } } } return map; }
Karten-Rendering
Diese Funktion wird verwendet, um eine Karte auf einer Kachelkarte zu rendern. Wir durchlaufen die Breite und Höhe der Karte und platzieren Kacheln nur, wenn das Array am getesteten Punkt den Wert 1 hat.
public static void RenderMap(int[,] map, Tilemap tilemap, TileBase tile) {
Kartenaktualisierung
Diese Funktion wird nur zum Aktualisieren der Karte und nicht zum erneuten Rendern verwendet. Dank dessen können wir weniger Ressourcen verbrauchen, ohne jede Kachel und ihre Kacheldaten neu zu zeichnen.
public static void UpdateMap(int[,] map, Tilemap tilemap)
Noise Perlin
Perlin-Rauschen kann für verschiedene Zwecke verwendet werden. Erstens können wir damit die oberste Ebene unserer Karte erstellen. Holen Sie sich dazu einfach einen neuen Punkt mit der aktuellen Position x und dem Startwert.
Einfache Lösung
Diese Erzeugungsmethode verwendet die einfachste Form der Realisierung von Perlin-Rauschen bei der Erzeugung von Pegeln. Wir können die Unity-Funktion für Perlin-Rauschen verwenden, damit wir den Code nicht selbst schreiben. Wir werden auch nur Ganzzahlen für die
Kachelzuordnung verwenden , indem
wir die Funktion
Mathf.FloorToInt () verwenden.
public static int[,] PerlinNoise(int[,] map, float seed) { int newPoint;
So sieht es nach dem Rendern auf einer Kachelkarte aus:
Glätten
Sie können diese Funktion auch übernehmen und glätten. Stellen Sie Intervalle für die Festlegung der Perlin-Höhen ein und führen Sie dann eine Glättung zwischen diesen Punkten durch. Diese Funktion wird sich als etwas komplizierter herausstellen, da Sie für Intervalle Listen mit ganzzahligen Werten berücksichtigen müssen.
public static int[,] PerlinNoiseSmooth(int[,] map, float seed, int interval) {
Im ersten Teil dieser Funktion prüfen wir zunächst, ob das Intervall größer als eins ist. Wenn ja, dann erzeugen Sie Rauschen. Die Erzeugung erfolgt in Intervallen, damit eine Glättung angewendet werden kann. Der nächste Teil der Funktion besteht darin, Punkte zu glätten.
Das Glätten wird wie folgt durchgeführt:
- Wir bekommen die aktuelle und letzte Position
- Wir erhalten den Unterschied zwischen zwei Punkten. Die wichtigste Information, die wir benötigen, ist der Unterschied entlang der y-Achse
- Dann bestimmen wir, wie viel die Änderung vorgenommen werden muss, um zum Punkt zu gelangen. Dies erfolgt durch Teilen der Differenz in y durch die Intervallvariable.
- Als nächstes beginnen wir, Positionen zu setzen und gehen bis auf Null
- Wenn wir auf der y-Achse 0 erreichen, addieren Sie die Höhenänderung zur aktuellen Höhe und wiederholen Sie den Vorgang für die nächste x-Position
- Wenn wir mit jeder Position zwischen der letzten und der aktuellen Position fertig sind, gehen wir zum nächsten Punkt
Wenn das Intervall kleiner als eins ist, verwenden wir einfach die vorherige Funktion, die die ganze Arbeit für uns erledigt.
else {
Werfen wir einen Blick auf das Rendering:
Zufälliger Spaziergang
Random Walk Top
Dieser Algorithmus führt einen Münzwurf durch. Wir können eines von zwei Ergebnissen erzielen. Wenn das Ergebnis "Adler" ist, bewegen wir uns einen Block nach oben, wenn das Ergebnis "Schwänze" ist, bewegen wir den Block nach unten. Dies schafft Höhen, indem es sich ständig nach oben oder unten bewegt. Der einzige Nachteil eines solchen Algorithmus ist seine sehr auffällige Blockierung. Werfen wir einen Blick darauf, wie es funktioniert.
public static int[,] RandomWalkTop(int[,] map, float seed) {
Random Walk Top mit Anti-AliasingEine solche Erzeugung führt zu gleichmäßigeren Höhen als die Perlin-Geräuschentwicklung.
Diese Variante von Random Walk liefert im Vergleich zur vorherigen Version ein viel flüssigeres Ergebnis. Wir können es implementieren, indem wir der Funktion zwei weitere Variablen hinzufügen:
- Die erste Variable wird verwendet, um zu bestimmen, wie lange es dauert, die aktuelle Höhe beizubehalten. Es ist eine Ganzzahl und wird zurückgesetzt, wenn sich die Höhe ändert
- Die zweite Variable wird in die Funktion eingegeben und als minimale Abschnittsbreite für die Höhe verwendet. Es wird klarer, wenn wir uns die Funktion ansehen.
Jetzt wissen wir, was wir hinzufügen müssen. Werfen wir einen Blick auf die Funktion:
public static int[,] RandomWalkTopSmoothed(int[,] map, float seed, int minSectionWidth) {
Wie Sie im unten gezeigten GIF sehen können, können Sie durch Glätten des Random-Walk-Algorithmus schöne flache Segmente auf der Ebene erhalten.
Fazit
Ich hoffe, dieser Artikel inspiriert Sie dazu, die prozedurale Generierung in Ihren Projekten zu verwenden. Wenn Sie mehr über prozedural generierte Karten erfahren möchten,
besuchen Sie die hervorragenden Ressourcen des
Prozedurgenerierungs- Wikis oder von
Roguebasin.com .
Im zweiten Teil des Artikels werden wir die prozedurale Generierung verwenden, um Höhlensysteme zu erstellen.
Teil 2
Alles, was wir in diesem Teil diskutieren werden, finden Sie in
diesem Projekt . Sie können Assets herunterladen und Ihre eigenen Verfahrensalgorithmen ausprobieren.
Noise Perlin
Im vorherigen Teil haben wir uns Möglichkeiten angesehen,
Perlin-Rauschen anzuwenden, um oberste Ebenen zu erstellen. Glücklicherweise kann Perlins Lärm auch verwendet werden, um eine Höhle zu schaffen. Dies wird durch Berechnung des neuen Perlin-Rauschwerts realisiert, der die Parameter der aktuellen Position multipliziert mit dem Modifikator empfängt. Der Modifikator ist ein Wert von 0 bis 1. Je höher der Modifikatorwert, desto chaotischer ist die Perlin-Erzeugung. Dann runden wir diesen Wert auf die Ganzzahl (0 oder 1), die wir im Map-Array speichern. Sehen Sie, wie dies implementiert wird:
public static int[,] PerlinNoiseCave(int[,] map, float modifier, bool edgesAreWalls) { int newPoint; for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { if (edgesAreWalls && (x == 0 || y == 0 || x == map.GetUpperBound(0) - 1 || y == map.GetUpperBound(1) - 1)) { map[x, y] = 1;
Wir verwenden den Modifikator anstelle von Seed, da die Ergebnisse der Perlin-Generation besser aussehen, wenn sie mit einer Zahl von 0 bis 0,5 multipliziert werden. Je niedriger der Wert, desto blockiger wird das Ergebnis. Schauen Sie sich die Beispielergebnisse an. Gif beginnt mit einem Modifikatorwert von 0,01 und erreicht schrittweise einen Wert von 0,25.
Aus diesem GIF ist ersichtlich, dass die Perlin-Erzeugung mit jedem Inkrement einfach das Muster erhöht.
Zufälliger Spaziergang
Im vorherigen Teil haben wir gesehen, dass Sie mithilfe eines Münzwurfs bestimmen können, wo sich die Plattform nach oben oder unten bewegt. In diesem Teil werden wir die gleiche Idee verwenden, aber
mit zwei zusätzlichen Optionen für Links- und Rechtsverschiebung. Diese Variation des Random Walk-Algorithmus ermöglicht es uns, Höhlen zu erstellen. Dazu wählen wir eine zufällige Richtung, verschieben dann unsere Position und löschen die Kachel. Wir setzen diesen Prozess fort, bis wir die erforderliche Anzahl von Kacheln erreicht haben, die zerstört werden müssen. Bisher verwenden wir nur 4 Richtungen: oben, unten, links, rechts.
public static int[,] RandomWalkCave(int[,] map, float seed, int requiredFloorPercent) {
Die Funktion beginnt mit:
- Finde die Startposition
- Berechnen Sie die Anzahl der zu löschenden Bodenfliesen.
- Löschen Sie die Kachel in der Startposition
- Fügen Sie der Anzahl der Kacheln eine hinzu.
Dann fahren wir mit der
while
. Er wird eine Höhle schaffen:
while (floorCount < reqFloorAmount) {
Was machen wir hier?
Zunächst wählen wir mit Hilfe einer Zufallszahl, in welche Richtung wir uns bewegen möchten. Dann überprüfen wir die neue Richtung mit der
switch case
. In dieser Aussage prüfen wir, ob es sich bei der Position um eine Wand handelt. Wenn nicht, löschen Sie das Element mit der Kachel aus dem Array. Wir machen so weiter, bis wir die gewünschte Grundfläche erreicht haben. Das Ergebnis ist unten dargestellt:
Ich habe auch meine eigene Version dieser Funktion erstellt, die auch diagonale Richtungen enthält. Der Funktionscode ist ziemlich lang. Wenn Sie ihn also ansehen möchten, laden Sie das Projekt über den Link am Anfang dieses Teils des Artikels herunter.
Richtungstunnel
Ein Richtungstunnel beginnt an einem Rand der Karte und erreicht den gegenüberliegenden Rand. Wir können die Krümmung und Rauheit des Tunnels steuern, indem wir sie an die Eingabefunktion übergeben. Wir können auch die minimale und maximale Länge der Teile des Tunnels einstellen. Werfen wir einen Blick auf die Implementierung:
public static int[,] DirectionalTunnel(int[,] map, int minPathWidth, int maxPathWidth, int maxPathChange, int roughness, int curvyness) {
Was ist los?
Zuerst setzen wir den Breitenwert. Der Breitenwert wechselt vom Wert mit einem Minus zu einem Positiv. Dank dessen erhalten wir die Größe, die wir benötigen. In diesem Fall verwenden wir den Wert 1, was wiederum eine Gesamtbreite von 3 ergibt, da wir die Werte -1, 0, 1 verwenden.
Als nächstes setzen wir die Anfangsposition in x, dafür nehmen wir die Mitte der Breite der Karte. Danach können wir im ersten Teil der Karte einen Tunnel legen.
Kommen wir nun zum Rest der Karte.
Wir generieren eine Zufallszahl zum Vergleich mit dem Rauheitswert. Wenn dieser Wert höher als dieser Wert ist, kann die Pfadbreite geändert werden. Wir überprüfen auch den Wert, um die Breite nicht zu klein zu machen. Im nächsten Teil des Codes machen wir uns auf den Weg durch die Karte. In jeder Phase tritt Folgendes auf:
- Wir erzeugen eine neue Zufallszahl im Vergleich zum Krümmungswert. Wie im vorherigen Test ändern wir den Mittelpunkt des Pfades, wenn er größer als der Wert ist. Wir führen auch eine Überprüfung durch, um nicht über die Karte hinauszugehen.
- Schließlich legen wir einen Tunnel in den neu geschaffenen Teil.
Die Ergebnisse dieser Implementierung sehen folgendermaßen aus:
Zelluläre Automaten
Zelluläre Automaten verwenden benachbarte Zellen, um zu bestimmen, ob die aktuelle Zelle eingeschaltet (1) oder ausgeschaltet (0) ist. Die Basis zur Bestimmung benachbarter Zellen wird basierend auf einem zufällig erzeugten Gitter von Zellen erstellt. Wir werden dieses
Quellraster mit der Funktion C #
Random.Next generieren .
Da wir einige verschiedene Implementierungen von zellularen Automaten haben, habe ich eine separate Funktion geschrieben, um dieses Grundraster zu generieren. Die Funktion sieht folgendermaßen aus:
public static int[,] GenerateCellularAutomata(int width, int height, float seed, int fillPercent, bool edgesAreWalls) {
In dieser Funktion können Sie auch festlegen, ob unser Gitter Wände benötigt. Im Übrigen ist es ganz einfach. Wir überprüfen eine Zufallszahl mit prozentualer Füllung, um festzustellen, ob die aktuelle Zelle aktiviert ist. Schauen Sie sich das Ergebnis an:
Die Nachbarschaft von Moore
Die Moore-Nachbarschaft wird verwendet, um die anfängliche Erzeugung von zellularen Automaten zu glätten. Die Nachbarschaft von Moore sieht so aus:
Für die Nachbarschaft gelten folgende Regeln:
- Wir überprüfen den Nachbarn in jede Richtung.
- Wenn der Nachbar eine aktive Kachel ist, addieren Sie eine zur Anzahl der umgebenden Kacheln.
- Wenn der Nachbar eine inaktive Kachel ist, tun wir nichts.
- Wenn eine Zelle mehr als 4 umgebende Kacheln hat, aktivieren Sie die Zelle.
- Wenn die Zelle genau 4 umgebende Kacheln hat, machen wir nichts damit.
- Wiederholen Sie diesen Vorgang, bis wir jede Kartenkachel überprüft haben.
Die Nachbarschaftsprüfungsfunktion von Moore lautet wie folgt:
static int GetMooreSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { int tileCount = 0; for(int neighbourX = x - 1; neighbourX <= x + 1; neighbourX++) { for(int neighbourY = y - 1; neighbourY <= y + 1; neighbourY++) { if (neighbourX >= 0 && neighbourX < map.GetUpperBound(0) && neighbourY >= 0 && neighbourY < map.GetUpperBound(1)) {
Nach dem Überprüfen der Kachel verwenden wir diese Informationen in der Glättungsfunktion. Hier kann wie bei der Erzeugung von zellularen Automaten angegeben werden, ob die Kanten der Karte Wände sein sollen.
public static int[,] SmoothMooreCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount) { for (int i = 0; i < smoothCount; i++) { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { int surroundingTiles = GetMooreSurroundingTiles(map, x, y, edgesAreWalls); if (edgesAreWalls && (x == 0 || x == (map.GetUpperBound(0) - 1) || y == 0 || y == (map.GetUpperBound(1) - 1))) {
Hierbei ist zu beachten, dass die Funktion über eine
for
Schleife verfügt, die die angegebene Anzahl von Glättungen durchführt. Dadurch wird eine schönere Karte erhalten.
Wir können diesen Algorithmus jederzeit ändern, indem wir Räume verbinden, wenn sich beispielsweise nur zwei Blöcke zwischen ihnen befinden.
Von Neumann Nachbarschaft
Das Viertel von Neumann ist ein weiterer beliebter Weg, um zellulare Automaten zu implementieren. Für eine solche Generation verwenden wir eine einfachere Nachbarschaft als für die Moore-Generation. Die Nachbarschaft sieht so aus:
Für die Nachbarschaft gelten folgende Regeln:
- Wir überprüfen die unmittelbaren Nachbarn der Kachel, ohne die diagonalen zu berücksichtigen.
- Wenn die Zelle aktiv ist, addieren Sie eine zur Menge.
- Wenn die Zelle inaktiv ist, tun Sie nichts.
- Wenn die Zelle mehr als 2 Nachbarn hat, aktivieren wir die aktuelle Zelle.
- Wenn die Zelle weniger als 2 Nachbarn hat, wird die aktuelle Zelle inaktiv.
- Wenn es genau 2 Nachbarn gibt, ändern Sie die aktuelle Zelle nicht.
Das zweite Ergebnis verwendet dieselben Prinzipien wie das erste, erweitert jedoch den Bereich der Nachbarschaft.
:
static int GetVNSurroundingTiles(int[,] map, int x, int y, bool edgesAreWalls) { int tileCount = 0;
, . ,
for
.
public static int[,] SmoothVNCellularAutomata(int[,] map, bool edgesAreWalls, int smoothCount) { for (int i = 0; i < smoothCount; i++) { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) {
, , :
, , .
Fazit
, - . ,
.