Verfahrensmuster, die mit Kachelkarten verwendet werden können

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:

  1. Wir unterscheiden, was eine Kachel ist und was keine Binärwerte verwendet. 1 ist eine Kachel, 0 ist ihre Abwesenheit.
  2. 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).
  3. 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.
  4. 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.
  5. 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) { //Clear the map (ensures we dont overlap) tilemap.ClearAllTiles(); //Loop through the width of the map for (int x = 0; x < map.GetUpperBound(0) ; x++) { //Loop through the height of the map for (int y = 0; y < map.GetUpperBound(1); y++) { // 1 = tile, 0 = no tile if (map[x, y] == 1) { tilemap.SetTile(new Vector3Int(x, y, 0), 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) //Takes in our map and tilemap, setting null tiles where needed { for (int x = 0; x < map.GetUpperBound(0); x++) { for (int y = 0; y < map.GetUpperBound(1); y++) { //We are only going to update the map, rather than rendering again //This is because it uses less resources to update tiles to null //As opposed to re-drawing every single tile (and collision data) if (map[x, y] == 0) { tilemap.SetTile(new Vector3Int(x, y, 0), null); } } } } 

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; //Used to reduced the position of the Perlin point float reduction = 0.5f; //Create the Perlin for (int x = 0; x < map.GetUpperBound(0); x++) { newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, seed) - reduction) * map.GetUpperBound(1)); //Make sure the noise starts near the halfway point of the height newPoint += (map.GetUpperBound(1) / 2); for (int y = newPoint; y >= 0; y--) { map[x, y] = 1; } } return map; } 

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) { //Smooth the noise and store it in the int array if (interval > 1) { int newPoint, points; //Used to reduced the position of the Perlin point float reduction = 0.5f; //Used in the smoothing process Vector2Int currentPos, lastPos; //The corresponding points of the smoothing. One list for x and one for y List<int> noiseX = new List<int>(); List<int> noiseY = new List<int>(); //Generate the noise for (int x = 0; x < map.GetUpperBound(0); x += interval) { newPoint = Mathf.FloorToInt((Mathf.PerlinNoise(x, (seed * reduction))) * map.GetUpperBound(1)); noiseY.Add(newPoint); noiseX.Add(x); } points = noiseY.Count; 

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.

 //Start at 1 so we have a previous position already for (int i = 1; i < points; i++) { //Get the current position currentPos = new Vector2Int(noiseX[i], noiseY[i]); //Also get the last position lastPos = new Vector2Int(noiseX[i - 1], noiseY[i - 1]); //Find the difference between the two Vector2 diff = currentPos - lastPos; //Set up what the height change value will be float heightChange = diff.y / interval; //Determine the current height float currHeight = lastPos.y; //Work our way through from the last x to the current x for (int x = lastPos.x; x < currentPos.x; x++) { for (int y = Mathf.FloorToInt(currHeight); y > 0; y--) { map[x, y] = 1; } currHeight += heightChange; } } } 

Das Glätten wird wie folgt durchgeführt:

  1. Wir bekommen die aktuelle und letzte Position
  2. Wir erhalten den Unterschied zwischen zwei Punkten. Die wichtigste Information, die wir benötigen, ist der Unterschied entlang der y-Achse
  3. 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.
  4. Als nächstes beginnen wir, Positionen zu setzen und gehen bis auf Null
  5. 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
  6. 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 { //Defaults to a normal Perlin gen map = PerlinNoise(map, seed); } return map; 

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) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Set our starting height int lastHeight = Random.Range(0, map.GetUpperBound(1)); //Cycle through our width for (int x = 0; x < map.GetUpperBound(0); x++) { //Flip a coin int nextMove = rand.Next(2); //If heads, and we aren't near the bottom, minus some height if (nextMove == 0 && lastHeight > 2) { lastHeight--; } //If tails, and we aren't near the top, add some height else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) - 2) { lastHeight++; } //Circle through from the lastheight to the bottom for (int y = lastHeight; y >= 0; y--) { map[x, y] = 1; } } //Return the map return map; } 


Random Walk Top mit Anti-Aliasing

Eine 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) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Determine the start position int lastHeight = Random.Range(0, map.GetUpperBound(1)); //Used to determine which direction to go int nextMove = 0; //Used to keep track of the current sections width int sectionWidth = 0; //Work through the array width for (int x = 0; x <= map.GetUpperBound(0); x++) { //Determine the next move nextMove = rand.Next(2); //Only change the height if we have used the current height more than the minimum required section width if (nextMove == 0 && lastHeight > 0 && sectionWidth > minSectionWidth) { lastHeight--; sectionWidth = 0; } else if (nextMove == 1 && lastHeight < map.GetUpperBound(1) && sectionWidth > minSectionWidth) { lastHeight++; sectionWidth = 0; } //Increment the section width sectionWidth++; //Work our way from the height down to 0 for (int y = lastHeight; y >= 0; y--) { map[x, y] = 1; } } //Return the modified map return map; } 

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; //Keep the edges as walls } else { //Generate a new point using Perlin noise, then round it to a value of either 0 or 1 newPoint = Mathf.RoundToInt(Mathf.PerlinNoise(x * modifier, y * modifier)); map[x, y] = newPoint; } } } return map; } 

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) { //Seed our random System.Random rand = new System.Random(seed.GetHashCode()); //Define our start x position int floorX = rand.Next(1, map.GetUpperBound(0) - 1); //Define our start y position int floorY = rand.Next(1, map.GetUpperBound(1) - 1); //Determine our required floorAmount int reqFloorAmount = ((map.GetUpperBound(1) * map.GetUpperBound(0)) * requiredFloorPercent) / 100; //Used for our while loop, when this reaches our reqFloorAmount we will stop tunneling int floorCount = 0; //Set our start position to not be a tile (0 = no tile, 1 = tile) map[floorX, floorY] = 0; //Increase our floor count floorCount++; 

Die Funktion beginnt mit:

  1. Finde die Startposition
  2. Berechnen Sie die Anzahl der zu löschenden Bodenfliesen.
  3. Löschen Sie die Kachel in der Startposition
  4. Fügen Sie der Anzahl der Kacheln eine hinzu.

Dann fahren wir mit der while . Er wird eine Höhle schaffen:

 while (floorCount < reqFloorAmount) { //Determine our next direction int randDir = rand.Next(4); switch (randDir) { //Up case 0: //Ensure that the edges are still tiles if ((floorY + 1) < map.GetUpperBound(1) - 1) { //Move the y up one floorY++; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase floor count floorCount++; } } break; //Down case 1: //Ensure that the edges are still tiles if ((floorY - 1) > 1) { //Move the y down one floorY--; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; //Right case 2: //Ensure that the edges are still tiles if ((floorX + 1) < map.GetUpperBound(0) - 1) { //Move the x to the right floorX++; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; //Left case 3: //Ensure that the edges are still tiles if ((floorX - 1) > 1) { //Move the x to the left floorX--; //Check if that piece is currently still a tile if (map[floorX, floorY] == 1) { //Change it to not a tile map[floorX, floorY] = 0; //Increase the floor count floorCount++; } } break; } } //Return the updated map return map; } 

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) { //This value goes from its minus counterpart to its positive value, in this case with a width value of 1, the width of the tunnel is 3 int tunnelWidth = 1; //Set the start X position to the center of the tunnel int x = map.GetUpperBound(0) / 2; //Set up our random with the seed System.Random rand = new System.Random(Time.time.GetHashCode()); //Create the first part of the tunnel for (int i = -tunnelWidth; i <= tunnelWidth; i++) { map[x + i, 0] = 0; } 

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.

  //Cycle through the array for (int y = 1; y < map.GetUpperBound(1); y++) { //Check if we can change the roughness if (rand.Next(0, 100) > roughness) { //Get the amount we will change for the width int widthChange = Random.Range(-maxPathWidth, maxPathWidth); //Add it to our tunnel width value tunnelWidth += widthChange; //Check to see we arent making the path too small if (tunnelWidth < minPathWidth) { tunnelWidth = minPathWidth; } //Check that the path width isnt over our maximum if (tunnelWidth > maxPathWidth) { tunnelWidth = maxPathWidth; } } //Check if we can change the curve if (rand.Next(0, 100) > curvyness) { //Get the amount we will change for the x position int xChange = Random.Range(-maxPathChange, maxPathChange); //Add it to our x value x += xChange; //Check we arent too close to the left side of the map if (x < maxPathWidth) { x = maxPathWidth; } //Check we arent too close to the right side of the map if (x > (map.GetUpperBound(0) - maxPathWidth)) { x = map.GetUpperBound(0) - maxPathWidth; } } //Work through the width of the tunnel for (int i = -tunnelWidth; i <= tunnelWidth; i++) { map[x + i, y] = 0; } } return map; } 

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:

  1. 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.
  2. 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) { //Seed our random number generator System.Random rand = new System.Random(seed.GetHashCode()); //Initialise the map 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 we have the edges set to be walls, ensure the cell is set to on (1) if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1) - 1)) { map[x, y] = 1; } else { //Randomly generate the grid map[x, y] = (rand.Next(0, 100) < fillPercent) ? 1 : 0; } } } return map; } 

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) { /* Moore Neighbourhood looks like this ('T' is our tile, 'N' is our neighbours) * * NNN * NTN * NNN * */ 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)) { //We don't want to count the tile we are checking the surroundings of if(neighbourX != x || neighbourY != y) { tileCount += map[neighbourX, neighbourY]; } } } } return tileCount; } 

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))) { //Set the edge to be a wall if we have edgesAreWalls to be true map[x, y] = 1; } //The default moore rule requires more than 4 neighbours else if (surroundingTiles > 4) { map[x, y] = 1; } else if (surroundingTiles < 4) { map[x, y] = 0; } } } } //Return the modified map return map; } 

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) { /* von Neumann Neighbourhood looks like this ('T' is our Tile, 'N' is our Neighbour) * * N * NTN * N * */ int tileCount = 0; //Keep the edges as walls if(edgesAreWalls && (x - 1 == 0 || x + 1 == map.GetUpperBound(0) || y - 1 == 0 || y + 1 == map.GetUpperBound(1))) { tileCount++; } //Ensure we aren't touching the left side of the map if(x - 1 > 0) { tileCount += map[x - 1, y]; } //Ensure we aren't touching the bottom of the map if(y - 1 > 0) { tileCount += map[x, y - 1]; } //Ensure we aren't touching the right side of the map if(x + 1 < map.GetUpperBound(0)) { tileCount += map[x + 1, y]; } //Ensure we aren't touching the top of the map if(y + 1 < map.GetUpperBound(1)) { tileCount += map[x, y + 1]; } return tileCount; } 

, . , 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++) { //Get the surrounding tiles int surroundingTiles = GetVNSurroundingTiles(map, x, y, edgesAreWalls); if (edgesAreWalls && (x == 0 || x == map.GetUpperBound(0) - 1 || y == 0 || y == map.GetUpperBound(1))) { //Keep our edges as walls map[x, y] = 1; } //von Neuemann Neighbourhood requires only 3 or more surrounding tiles to be changed to a tile else if (surroundingTiles > 2) { map[x, y] = 1; } else if (surroundingTiles < 2) { map[x, y] = 0; } } } } //Return the modified map return map; } 

, , :


, , .

Fazit


, - . , .

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


All Articles