Prozedurale Muster, die Sie mit Tilemaps verwenden können, Teil 1

Viele Entwickler haben die prozedurale Generierung genutzt, um ihrem Spiel eine gewisse Vielfalt zu verleihen. Erwähnenswert sind hier Spiele wie Minecraft oder in jüngerer Zeit Enter the Gungeon und Descenders. In diesem Beitrag werden einige der Algorithmen erläutert, die Sie mit Tilemap, einer in Unity 2017.2 eingeführten 2D-Funktion, und RuleTile verwenden können.
Mit prozedural erstellten Karten können Sie sicherstellen, dass keine zwei Spiele Ihres Spiels gleich sind. Sie können verschiedene Eingaben wie die Zeit oder die aktuelle Stufe des Spielers verwenden, um sicherzustellen, dass sich der Inhalt auch nach der Erstellung des Spiels dynamisch ändert.
Wir werden uns einige der gebräuchlichsten Methoden zur Erstellung einer prozeduralen Welt und einige von mir erstellte Varianten ansehen. Hier ist ein Beispiel dafür, was Sie nach der Lektüre dieses Artikels vielleicht schaffen können. Drei Algorithmen arbeiten zusammen, um eine Karte zu erstellen, wobei eine Tilemap und eine RuleTile verwendet werden:

Wenn wir eine Karte mit einem der Algorithmen erzeugen, erhalten wir ein int-Array, das alle neuen Daten enthält. Diese Daten können wir dann weiter bearbeiten oder in eine Kachelkarte übertragen.
Gut zu wissen, bevor Sie weiter lesen:
Die Art und Weise, wie wir zwischen Kacheln und Nicht-Kacheln unterscheiden, ist die Verwendung von Binärwerten. 1 ist ein und 0 ist aus.
Wir werden alle unsere Karten in einem 2D-Ganzzahl-Array speichern, das am Ende jeder Funktion an den Benutzer zurückgegeben wird (außer beim Rendern).
Ich werde die Array-Funktion GetUpperBound() verwenden, um die Höhe und Breite jeder Karte zu ermitteln, so dass wir weniger Variablen in jeder Funktion und einen saubereren Code haben.
Ich verwende oft Mathf.FloorToInt(), weil das Tilemap-Koordinatensystem unten links beginnt und die Verwendung von Mathf.FloorToInt() es uns ermöglicht, die Zahlen auf eine ganze Zahl zu runden.
Der gesamte Code in diesem Blogbeitrag ist in C# geschrieben.
GenerateArray erzeugt ein neues int-Array mit der angegebenen Größe. Wir können auch angeben, ob das Feld voll oder leer sein soll (1 oder 0). Hier ist der Code:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Diese Funktion wird verwendet, um unsere Karte auf die Tilemap zu übertragen. Wir durchlaufen die Breite und Höhe der Karte und platzieren nur dann Kacheln, wenn das Feld an der zu prüfenden Stelle eine 1 enthält.
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Diese Funktion wird nur zur Aktualisierung der Karte verwendet und nicht zum erneuten Rendern. Auf diese Weise können wir weniger Ressourcen verbrauchen, da wir nicht jede einzelne Kachel und ihre Kacheldaten neu zeichnen müssen.
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Perlin-Rauschen kann auf verschiedene Weise verwendet werden. Die erste Möglichkeit, die wir nutzen können, besteht darin, eine oberste Ebene für unsere Karte zu erstellen. Dies ist so einfach wie das Erzeugen eines neuen Punktes unter Verwendung unserer aktuellen X-Position und eines Seed.
Diese Erzeugung ist die einfachste Form der Implementierung von Perlin-Rauschen in die Pegelerzeugung. Wir können die Unity-Funktion für Perlin Noise verwenden, um uns zu helfen, so dass keine ausgefallene Programmierung erforderlich ist. Mit der Funktion Mathf.FloorToInt() stellen wir außerdem sicher, dass wir ganze Zahlen für unsere Kachelkarte haben.
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
So sieht es auf einer Kachelkarte gerendert aus:

Wir können auch diese Funktion nehmen und sie glätten. Legen Sie Intervalle fest, um die Perlin-Höhe aufzuzeichnen, und glätten Sie dann zwischen den Punkten. Diese Funktion ist etwas komplizierter, da wir für unsere Intervalle Listen von ganzen Zahlen berücksichtigen müssen.
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Im ersten Teil dieser Funktion prüfen wir zunächst, ob das Intervall größer als eins ist. Ist dies der Fall, erzeugen wir das Rauschen. Wir tun dies in Abständen, um eine Glättung zu ermöglichen. Im nächsten Schritt werden die Punkte geglättet.
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Die Glättung erfolgt in den folgenden Schritten:
- Ermittlung der aktuellen und der letzten Position
- Ermitteln Sie die Differenz zwischen den beiden Positionen. Die wichtigste Information, die wir benötigen, ist die Differenz auf der y-Achse
- Als Nächstes bestimmen wir, um wie viel wir den Treffer ändern sollten, indem wir die y-Differenz durch die Intervallvariable teilen.
Jetzt können wir mit der Einstellung der Positionen beginnen. Wir arbeiten uns bis auf Null herunter
Wenn wir 0 auf der y-Achse erreichen, addieren wir die Höhenänderung zur aktuellen Höhe und wiederholen den Vorgang für die nächste x-Position
Sobald wir alle Positionen zwischen der letzten und der aktuellen Position abgearbeitet haben, gehen wir zum nächsten Punkt über
Wenn das Intervall kleiner als eins ist, verwenden wir einfach die vorherige Funktion, um die Arbeit für uns zu erledigen.
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Schauen wir mal, wie es gerendert aussieht:

Dieser Algorithmus funktioniert, indem eine Münze geworfen wird. Wir erhalten dann eines von zwei Ergebnissen. Wenn das Ergebnis Kopf ist, rücken wir einen Block nach oben, wenn das Ergebnis Zahl ist, rücken wir einen Block nach unten. Dies schafft eine gewisse Höhe für unsere Ebene, indem wir uns immer entweder nach oben oder nach unten bewegen. Der einzige Nachteil dieses Algorithmus ist, dass er sehr blockig aussieht. Schauen wir uns einmal an, wie das funktioniert.
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Diese Generation bietet eine gleichmäßigere Höhe im Vergleich zur Perlin-Rauschgeneration.
Diese Generation bietet eine gleichmäßigere Höhe im Vergleich zur Perlin-Rauschgeneration.
Diese Random-Walk-Variante ermöglicht einen viel glatteren Abschluss als die vorherige Version. Wir können dies tun, indem wir zwei neue Variablen zu unserer Funktion hinzufügen:
- Die erste Variable wird verwendet, um festzustellen, wie lange wir unsere aktuelle Höhe gehalten haben. Dies ist eine ganze Zahl und wird zurückgesetzt, wenn wir die Höhe ändern.
- Die zweite Variable ist eine Eingabe für die Funktion und wird als Mindestschnittbreite für die Höhe verwendet. Dies macht mehr Sinn, wenn Sie sich die Funktion
Jetzt wissen wir, was wir hinzufügen müssen. Schauen wir uns die Funktion an:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Wie Sie im folgenden Bild sehen können, ermöglicht die Glättung des Random-Walk-Algorithmus einige schöne flache Stücke innerhalb des Levels.

Ich hoffe, dies hat Sie dazu inspiriert, in Ihren Projekten eine Form der prozeduralen Generierung zu verwenden. Wenn Sie mehr über die prozedurale Generierung von Karten erfahren möchten, besuchen Sie das Procedural Generation Wiki oder Roguebasin.com, die beide großartige Ressourcen sind.
Im nächsten Beitrag dieser Reihe werden wir sehen, wie wir mit Hilfe der prozeduralen Generierung Höhlensysteme erstellen können.
Wenn ihr etwas Cooles mit prozeduraler Generierung macht, könnt ihr mir auf Twitter eine Nachricht schicken oder unten einen Kommentar hinterlassen!
Möchten Sie mehr darüber erfahren und eine Live-Demo erhalten? Ich spreche auch über prozedurale Muster zur Verwendung mit Tilemaps auf der Unite Berlin, im Mini-Theater der Expo-Halle am 20. Juni. Ich werde nach dem Vortrag in der Nähe sein, wenn Sie sich persönlich mit mir unterhalten möchten!