Zufälliger zweidimensionaler Höhlengenerator

Vorwort


Wenn Sie zu faul sind, um sich um Ihre Zeit zu kümmern und ein Level für Ihr Spiel zu erstellen, sind Sie bei uns genau richtig.

In diesem Artikel erfahren Sie ausführlich, wie Sie am Beispiel von Hochland und Höhlen eine von vielen anderen Generierungsmethoden anwenden können. Wir werden den Aldous-Broder- Algorithmus betrachten und wie man die erzeugte Höhle schöner macht.

Am Ende des Lesens des Artikels sollten Sie ungefähr Folgendes erhalten:

Zusammenfassung


Theorie


Berg


Um ehrlich zu sein, kann die Höhle von Grund auf neu erstellt werden, aber wird sie irgendwie hässlich sein? In der Rolle der "Plattform" für die Platzierung von Minen habe ich eine Bergkette gewählt.
Dieser Berg wird ganz einfach erzeugt: Lassen Sie uns ein zweidimensionales Array und eine variable Höhe haben , die anfänglich der halben Länge des Arrays in der zweiten Dimension entspricht; Wir gehen einfach die Spalten durch und füllen etwas mit allen Zeilen in der Spalte auf einen variablen Höhenwert aus, wobei wir ihn mit einer zufälligen Chance nach oben oder unten ändern.

Höhle


Um die Dungeons selbst zu generieren, habe ich - wie mir schien - einen hervorragenden Algorithmus gewählt. In einfachen Worten kann dies wie folgt erklärt werden: Selbst wenn wir zwei (vielleicht zehn) Variablen X und Y und ein zweidimensionales Array von 50 mal 50 haben, geben wir diesen Variablen Zufallswerte innerhalb unseres Arrays, zum Beispiel X = 26 und Y = 28 . Danach führen wir die gleichen Aktionen mehrmals aus: Wir erhalten eine Zufallszahl von Null bis

AnzahlderVariablen2

in unserem Fall bis zu vier ; und dann ändern wir uns abhängig von der Anzahl der abgebrochenen Personen
unsere Variablen:

switch (Random.Range(0, 4)) { case 0: X += 1; break; case 1: X -= 1; break; case 2: Y += 1; break; case 3: Y -= 1; break; } 

Dann prüfen wir natürlich, ob eine Variable außerhalb der Grenzen unseres Feldes liegt:

  X = X < 0 ? 0 : (X >= 50 ? 49 : X); Y = Y < 0 ? 0 : (Y >= 50 ? 49 : Y); 

Nach all diesen Überprüfungen tun wir etwas in den neuen X- und Y- Werten für unser Array (zum Beispiel: Fügen Sie dem Element einen hinzu) .

 array[X, Y] += 1; 

Vorbereitung


Zeichnen wir zur Vereinfachung der Implementierung und Visualisierung unserer Methoden die resultierenden Objekte? Ich bin so froh, dass es dir nichts ausmacht! Wir werden dies mit Texture2D tun.

Zum Arbeiten benötigen wir nur zwei Skripte:
Bei ground_libray dreht sich der Artikel. Hier erzeugen und reinigen und zeichnen wir
ground_generator ist das, was unser ground_libray verwenden wird
Lassen Sie den ersten statisch sein und erben Sie nichts:

 public static class ground_libray 

Und die zweite ist normal, nur brauchen wir die Update- Methode nicht.

Lassen Sie uns außerdem mit der SpriteRenderer- Komponente ein Spielobjekt auf der Bühne erstellen

Praktischer Teil


Woraus besteht es?


Um mit Daten zu arbeiten, verwenden wir ein zweidimensionales Array. Sie können eine Reihe verschiedener Typen verwenden, von Byte oder Int bis hin zu Farbe , aber ich glaube, dass dies am besten möglich ist:

Neuer Typ
Wir schreiben dieses Ding in ground_libray .

 [System.Serializable] public class block { public float[] color = new float[3]; public block(Color col) { color = new float[3] { col.r, col.g, col.b }; } } 


Ich werde dies durch die Tatsache erklären, dass wir damit sowohl unser Array speichern als auch es bei Bedarf ändern können.

Massiv


Bevor wir mit der Erzeugung des Berges beginnen, bestimmen wir den Ort, an dem wir ihn lagern werden .

Im Skript ground_generator habe ich Folgendes geschrieben:

  public int ground_size = 128; ground_libray.block[,] ground; Texture2D myT; 

ground_size - die Größe unseres Feldes ( dh das Array besteht aus 16384 Elementen).
ground_libray.block [,] ground - das ist unser Feld für die Erzeugung.
Texture2D myT ist das, worauf wir zurückgreifen werden.

Wie wird es funktionieren?
Das Prinzip der Arbeit mit uns wird wie folgt sein - wir werden einige ground_libray- Methoden von ground_generator aufrufen und dem ersten unser Bodenfeld geben.

Erstellen wir die erste Methode im Skript ground_libray:

Bergbau
  public static float mount_noise = 0.02f; public static void generate_mount(ref block[,] b) { int h_now = b.GetLength(1) / 2; for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < h_now; y++) { b[x, y] = new block(new Color(0.7f, 0.4f, 0)); h_now += Random.value > (1.0f - mount_noise) ? (Random.value > 0.5 ? 1 : -1) : 0; } } 

Und sofort werden wir versuchen zu verstehen, was hier passiert: Wie gesagt, wir gehen einfach die Spalten unseres Arrays b durch und ändern gleichzeitig die Höhenvariable h_now , die ursprünglich gleich der Hälfte 128 (64) war . Aber es gibt noch etwas Neues - mount_noise . Diese Variable ist für die Möglichkeit verantwortlich , h_now zu ändern, denn wenn Sie die Höhe sehr oft ändern, sieht der Berg wie ein Kamm aus .

Farbe
Ich habe sofort eine leicht bräunliche Farbe eingestellt, lass es zumindest einige sein - in Zukunft werden wir sie nicht brauchen.

Gehen wir nun zu ground_generator und schreiben dies in die Start- Methode:

  ground = new ground_libray.block [ground_size, ground_size]; ground_libray.generate_mount(ref ground); 

Wir initialisieren den variablen Boden, sobald dies erforderlich ist .
Senden Sie es anschließend ohne Erklärung an ground_libray .
Also haben wir den Berg erzeugt.

Warum kann ich meinen Berg nicht sehen?


Zeichnen wir jetzt, was wir haben!

Zum Zeichnen schreiben wir die folgende Methode in unser ground_libray :

Zeichnen
  public static void paint(block[,] b, ref Texture2D t) { t = new Texture2D(b.GetLength(0), b.GetLength(1)); t.filterMode = FilterMode.Point; for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < b.GetLength(1); y++) { if (b[x, y] == null) { t.SetPixel(x, y, new Color(0, 0, 0, 0)); continue; } t.SetPixel(x, y, new Color( b[x, y].color[0], b[x, y].color[1], b[x, y].color[2] ) ); } t.Apply(); } 

Hier geben wir niemandem mehr unser Feld, wir geben nur eine Kopie davon (obwohl wir aufgrund der Wortklasse etwas mehr als nur eine Kopie gegeben haben) . Wir werden dieser Methode auch unsere Texture2D geben.

Die ersten beiden Zeilen: Wir erstellen unsere Textur in der Größe des Feldes und entfernen die Filterung .

Danach gehen wir unser gesamtes Array-Feld durch und haben nichts erstellt (die Klasse muss initialisiert werden) - wir zeichnen ein leeres Feld, andernfalls, wenn es nicht leer ist - zeichnen wir das, was wir gespeichert haben, in das Element.

Und wenn wir fertig sind, gehen wir natürlich zu ground_generator und fügen Folgendes hinzu:

  ground = new ground_libray.block [ground_size, ground_size]; ground_libray.generate_mount(ref ground); //   ground_libray.paint(ground, ref myT); GetComponent<SpriteRenderer>().sprite = Sprite.Create(myT, new Rect(0, 0, ground_size, ground_size), Vector3.zero ); 

Aber egal wie viel wir auf unsere Textur zeichnen, im Spiel können wir es nur sehen, indem wir diese Leinwand auf etwas legen:

SpriteRenderer akzeptiert Texture2D nirgendwo , aber nichts hindert uns daran, aus dieser Textur ein Sprite zu erstellen - Sprite.Create ( Textur , Rechteck mit den Koordinaten der unteren linken Ecke und der oberen rechten Ecke , der Koordinate der Achse ).

Diese Linien werden als die neuesten bezeichnet, wir werden den Rest über der Malmethode hinzufügen!

Meins


Jetzt müssen wir unsere Felder mit zufälligen Höhlen füllen. Für solche Aktionen erstellen wir auch eine separate Methode in ground_libray . Ich möchte sofort die Parameter der Methode erklären:

 ref block[,] b -     . int thick -    int size -         Color outLine -   

Höhle
  public static void make_cave(ref block[,] b, int thick, int size, Color outLine) { int xNow = Random.Range(0, b.GetLength(0)); int yNow = Random.Range(0, b.GetLength(1) / 2); for (int i = 0; i < size; i++) { b[xNow, yNow] = null; make_thick(ref b, thick, new int[2] { xNow, yNow }, outLine); switch (Random.Range(0, 4)) { case 0: xNow += 1; break; case 1: xNow -= 1; break; case 2: yNow += 1; break; case 3: yNow -= 1; break; } xNow = xNow < 0 ? 0 : (xNow >= b.GetLength(0) ? b.GetLength(0) - 1 : xNow); yNow = yNow < 0 ? 0 : (yNow >= b.GetLength(1) ? b.GetLength(1) - 1 : yNow); } } 

Zunächst haben wir unsere Variablen X und Y deklariert, aber ich habe sie nur xNow bzw. yNow genannt .

Das erste, nämlich xNow , erhält einen zufälligen Wert von Null bis zur Größe des Feldes in der ersten Dimension.

Und das zweite - yNow - erhält ebenfalls einen zufälligen Wert: von Null bis zur Mitte des Feldes in der zweiten Dimension. Warum? Wir erzeugen unseren Berg aus der Mitte, die Chance, dass er bis zur "Decke" wächst, ist nicht groß . Aus diesem Grund halte ich es nicht für relevant, Höhlen in der Luft zu erzeugen.

Danach geht sofort eine Schleife, deren Anzahl von Ticks vom Größenparameter abhängt. Bei jedem Tick aktualisieren wir das Feld an den Positionen xNow und yNow und erst dann aktualisieren wir sie selbst ( Feldaktualisierungen können am Ende vorgenommen werden - Sie werden den Unterschied nicht spüren).

Es gibt auch eine make_thick- Methode, in deren Parametern wir unser Feld übergeben , die Breite des Höhlenstrichs , die aktuelle Aktualisierungsposition der Höhle und die Farbe des Strichs :

Schlaganfall
  static void make_thick (ref block[,] b, int t, int[] start, Color o) { for (int x = (start[0] - t); x < (start[0] + t); x++) { if (x < 0 || x >= b.GetLength(0)) continue; for (int y = (start[1] - t); y < (start[1] + t); y++) { if (y < 0 || y >= b.GetLength(1)) continue; if (b[x, y] == null) continue; b[x, y] = new block(o); } } } 

Die Methode verwendet die an sie übergebene Startkoordinate und malt in einem Abstand t alle Blöcke in der Farbe o neu - alles ist sehr einfach!


Fügen wir nun diese Zeile zu unserem ground_generator hinzu :

 ground_libray.make_cave(ref ground, 2, 10000, new Color(0.3f, 0.3f, 0.3f)); 

Sie können das Skript ground_generator als Komponente auf unserem Objekt installieren und überprüfen, wie es funktioniert!



Mehr über die Höhlen ...
  • Um mehr Höhlen zu erstellen , können Sie die Methode make_cave mehrmals aufrufen (verwenden Sie eine Schleife).
  • Durch Ändern des Größenparameters wird die Höhle nicht immer vergrößert, sie wird jedoch häufig größer
  • Durch Ändern des Thick- Parameters erhöhen Sie die Anzahl der Operationen erheblich:
    Wenn der Parameter 3 ist, beträgt die Anzahl der Quadrate in einem Radius von 3 36 , sodass bei einer Parametergröße von 40.000 die Anzahl der Operationen 36 * 40.000 = 1440000 beträgt


Höhlenkorrektur




Haben Sie bemerkt, dass die Höhle in dieser Ansicht nicht besonders gut aussieht? Zu viele zusätzliche Details (vielleicht denken Sie anders) .

Um Einschlüsse von # 4d4d4d loszuwerden, schreiben wir diese Methode in ground_libray :

Reiniger
  public static void clear_caves(ref block[,] b) { for (int x = 0; x < b.GetLength(0); x++) for (int y = 0; y < b.GetLength(1); y++) { if (b[x, y] == null) continue; if (solo(b, 2, 13, new int[2] { x, y })) b[x, y] = null; } } 

Es wird jedoch schwierig sein zu verstehen, was hier vor sich geht, wenn Sie nicht wissen, was die Solofunktion bewirkt:

  static bool solo (block[,] b, int rad, int min, int[] start) { int cnt = 0; for (int x = (start[0] - rad); x <= (start[0] + rad); x++) { if (x < 0 || x >= b.GetLength(0)) continue; for (int y = (start[1] - rad); y <= (start[1] + rad); y++) { if (y < 0 || y >= b.GetLength(1)) continue; if (b[x, y] == null) cnt += 1; else continue; if (cnt >= min) return true; } } return false; } 

In den Parametern dieser Funktion müssen unser Feld , der Radius der Punktüberprüfung , die „Zerstörungsschwelle“ und die Koordinaten des zu überprüfenden Punktes vorhanden sein.
Hier ist eine detaillierte Erklärung, was diese Funktion tut:
int cnt ist der Zähler der aktuellen "Schwelle"
Als nächstes folgen zwei Zyklen, die alle Punkte um den einen herum überprüfen, dessen Koordinaten zum Starten übergeben werden . Wenn es einen leeren Punkt gibt , fügen wir einen zu cnt hinzu. Wenn wir die "Schwelle der Zerstörung" erreichen, geben wir die Wahrheit zurück - der Punkt ist überflüssig . Ansonsten berühren wir sie nicht.

Ich habe die Zerstörungsschwelle auf 13 leere Punkte festgelegt, und der Überprüfungsradius beträgt 2 (dh es werden 24 Punkte überprüft, ohne den zentralen Punkt).
Beispiel
Dieser bleibt unversehrt, da es nur 9 leere Punkte gibt.



Aber dieser hatte kein Glück - ungefähr 14 leere Punkte



Eine kurze Beschreibung des Algorithmus: Wir gehen das gesamte Feld durch und überprüfen alle Punkte, um festzustellen, ob sie benötigt werden.

Als nächstes fügen wir einfach die folgende Zeile zu unserem ground_generator hinzu :

 ground_libray.clear_caves(ref ground); 

Zusammenfassung


Wie wir sehen können, gingen die meisten unnötigen Partikel einfach weg.

Fügen Sie etwas Farbe hinzu


Unser Berg sieht sehr eintönig aus, ich finde es langweilig.

Fügen wir etwas Farbe hinzu. Fügen Sie die Methode level_paint zu ground_libray hinzu :

Über die Berge malen
  public static void level_paint(ref block[,] b, Color[] all_c) { for (int x = 0; x < b.GetLength(0); x++) { int lvl_div = -1; int counter = 0; int lvl_now = 0; for (int y = b.GetLength(1) - 1; y > 0; y--) { if (b[x, y] != null && lvl_div == -1) lvl_div = y / all_c.Length; else if (b[x, y] == null) continue; b[x, y] = new block(all_c[lvl_now]); lvl_now += counter >= lvl_div ? 1 : 0; lvl_now = (lvl_now >= all_c.Length) ? (all_c.Length - 1) : lvl_now; counter = counter >= lvl_div ? 0 : (counter += 1); } } } </ <cut />source>           .    ,       ,   .       ,      .          <b>Y </b>  ,      . </spoiler>     <b>ground_generator </b> : <source lang="cs"> ground_libray.level_paint(ref ground, new Color[3] { new Color(0.2f, 0.8f, 0), new Color(0.6f, 0.2f, 0.05f), new Color(0.2f, 0.2f, 0.2f), }); 

Ich habe nur 3 Farben gewählt: Grün , Dunkelrot und Dunkelgrau .
Natürlich können Sie sowohl die Anzahl der Farben als auch die Werte der einzelnen Farben ändern. Es stellte sich so heraus:

Zusammenfassung


Trotzdem sieht es zu streng aus, um den Farben ein wenig Zufälligkeit zu verleihen. Wir werden diese Eigenschaft in ground_libray schreiben:

Zufällige Farben
  public static float color_randomize = 0.1f; static float crnd { get { return Random.Range(1.0f - color_randomize, 1.0f + color_randomize); } } 

Und jetzt in den Methoden level_paint und make_thick , in den Zeilen, in denen wir Farben zuweisen, zum Beispiel in make_thick :

 b[x, y] = new block(o); 

Wir werden dies schreiben:

 b[x, y] = new block(o * crnd); 

Und in level_paint

 b[x, y] = new block(all_c[lvl_now] * crnd); 


Am Ende sollte alles ungefähr so ​​aussehen:

Zusammenfassung



Nachteile


Angenommen, wir haben ein Feld von 1024 mal 1024, wir müssen 24 Höhlen erzeugen, deren Kantenstärke 4 beträgt und deren Größe 80.000 beträgt.

1024 * 1024 + 24 * 64 * 80.000 = 5.368.832.000.000 Operationen.

Diese Methode eignet sich nur zum Generieren kleiner Module für die Spielwelt. Es ist unmöglich, jeweils etwas sehr Großes zu generieren.

Source: https://habr.com/ru/post/de416419/


All Articles