Verwenden von Kachelmasken, um den Typ einer Kachel basierend auf den umgebenden Kacheln festzulegen

Mit Kachelmasken, die häufig in Tile-basierten Spielen verwendet werden, können Sie eine Kachel in Abhängigkeit von ihren Nachbarn ändern. So können Sie Gelände mischen, Kacheln ersetzen und mehr. In diesem Tutorial zeige ich Ihnen eine skalierbare, wiederverwendbare Methode, mit der Sie feststellen können, ob die unmittelbaren Nachbarn einer Kachel mit einem beliebigen der von Ihnen festgelegten Muster übereinstimmen.

Hinweis: Obwohl dieses Tutorial mit C # und Unity geschrieben wurde, sollten Sie in der Lage sein, in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte anzuwenden.


Was ist eine Fliesenmaske??

Bedenken Sie Folgendes: Sie machen ein Terraria-ähnliches Aussehen und möchten, dass sich Schmutzblöcke in Schlammblöcke verwandeln, wenn sie sich neben Wasser befinden. Nehmen wir an, Ihre Welt hat viel mehr Wasser als Schmutz. Daher ist der billigste Weg, dies zu tun, indem Sie jeden Schmutzblock überprüfen, um zu sehen, ob er sich neben Wasser befindet, und nicht umgekehrt. Dies ist ein ziemlich guter Zeitpunkt, um a zu verwenden Fliesenmaske.

Fliesenmasken sind so alt wie die Berge und basieren auf einer einfachen Idee. In einem 2D-Gitter von Objekten kann eine Kachel acht andere Kacheln direkt neben sich haben. Wir nennen das das Nahbereich dieser zentralen Kachel (Abbildung 1).


Abbildung 1. Der lokale Bereich einer Kachel.

Um zu entscheiden, was mit Ihrem Kachelstein zu tun ist, vergleichen Sie den lokalen Bereich mit einer Reihe von Regeln. Sie könnten beispielsweise direkt über den Schmutzblock schauen und sehen, ob dort Wasser ist (Abbildung 2). Die Regeln, die Sie zur Bewertung Ihres lokalen Bereichs verwenden, sind Ihre Fliesenmaske.


Abbildung 2. Kachelmaske zum Erkennen eines Schmutzblocks als Schlammblock. Die grauen Kacheln können beliebige andere Kacheln sein.

Natürlich sollten Sie auch in den anderen Richtungen nach Wasser suchen, sodass die meisten Kachelmasken mehr als eine räumliche Anordnung haben (Abbildung 3)..


Abbildung 3. Die vier typischen Orientierungen einer Fliesenmaske: 0 °, 90 °, 180 °, 270 °.

Und manchmal werden Sie feststellen, dass sogar wirklich einfache Kachelmaskenanordnungen gespiegelt und nicht überlagert werden können (Abbildung 4)..


Abbildung 4. Ein Beispiel für Chiralität in einer Kachelmaske. Die obere Reihe kann nicht gedreht werden, um der unteren Reihe zu entsprechen.

Lagerstrukturen einer Kachelmaske

Ihre Elemente speichern

Jede Zelle einer Kachelmaske enthält ein Element. Das Element in dieser Zelle wird mit dem entsprechenden Element im lokalen Bereich verglichen, um festzustellen, ob sie übereinstimmen.

Ich benutze Listen als Elemente. Mithilfe von Listen kann ich willkürlich komplexe Vergleiche durchführen. Mit Linq von C # können Listen auf einfache Weise zu größeren Listen zusammengefügt werden. Dadurch verringert sich die Anzahl der Änderungen, die ich möglicherweise vergessen kann.

Zum Beispiel kann eine Liste von Wandfliesen mit einer Liste von Bodenfliesen und einer Liste von Öffnungsfliesen (Türen und Fenster) kombiniert werden, um eine Liste von Strukturfliesen zu erstellen, oder Listen von Möbelfliesen aus einzelnen Räumen können kombiniert werden eine liste aller möbel. Andere Spiele enthalten möglicherweise Listen von Kacheln, die brennbar sind oder von der Schwerkraft beeinflusst werden.

Die Fliesenmaske selbst lagern

Die intuitive Methode zum Speichern einer Kachelmaske ist ein 2D-Array. Der Zugriff auf 2D-Arrays kann jedoch langsam sein, und Sie müssen viel tun. Wir wissen, dass ein lokaler Bereich und eine Kachelmaske immer aus neun Elementen bestehen. Daher können wir diese Zeitstrafe vermeiden, indem Sie ein einfaches 1D-Array verwenden und den lokalen Bereich von oben nach unten und von links nach rechts lesen (Abbildung 4)..


Abbildung 5. Speichern einer 2D-Kachelmaske in einer 1D-Struktur.

Die räumlichen Beziehungen zwischen Elementen bleiben erhalten, wenn Sie sie jemals brauchen (ich habe sie bisher nicht gebraucht), und Arrays können so ziemlich alles speichern. Tilemasks in diesem Format können auch leicht durch Offset-Lese- / Schreibvorgänge gedreht werden.

Speichern von Rotationsinformationen

In einigen Fällen möchten Sie möglicherweise die zentrale Kachel entsprechend ihrer Umgebung drehen, z. B. wenn Sie eine Wand neben anderen Wänden platzieren. Ich mag die Verwendung von gezackten Arrays, um die vier Rotationen einer Kachelmaske an derselben Stelle zu halten, und den äußeren Array-Index verwenden, um die aktuelle Rotation anzuzeigen (mehr dazu im Code)..


Code-Anforderungen

Mit den erläuterten Grundlagen können wir beschreiben, was der Code tun muss:

  1. Definieren und speichern Sie Fliesenmasken.
  2. Drehen Sie die Kachelmasken automatisch, anstatt dass wir vier gedrehte Kopien derselben Definition manuell verwalten müssen.
  3. Schnappen Sie sich den lokalen Bereich einer Fliese.
  4. Bewerten Sie den lokalen Bereich anhand einer Kachelmaske.

Der folgende Code ist in C # für Unity geschrieben, die Konzepte sollten jedoch ziemlich portabel sein. Das Beispiel ist ein Beispiel aus meiner eigenen Arbeit bei der prozeduralen Konvertierung von Roguelike-Text-Only-Maps in 3D (in diesem Fall wird ein gerader Abschnitt der Wand platziert)..


Definieren, drehen und lagern Sie die Kachelmasken

Ich automatisiere das alles mit einem Methodenaufruf an a DefineTilemask Methode. Hier ist ein Beispiel für die Verwendung mit der Methodendeklaration unten.

 öffentliche statische Readonly-Liste any = neue Liste() ; öffentliche statische Readonly-Liste ignoriert = neue Liste() ", '_'; public static readonly List wall = neue Liste() '#', 'D', 'W'; öffentliche statische Liste[] [] outerWallStraight = MapHelper.DefineTilemask (beliebige, ignorierte, beliebige, wall, any, wall, any, any, any);

Ich definiere die Tilemask in ihrer nichtrotierten Form. Die Liste wurde aufgerufen ignoriert speichert Zeichen, die in meinem Programm nichts beschreiben: Leerzeichen, die übersprungen werden; und Unterstriche, die ich zum Anzeigen eines ungültigen Array-Indexes verwende. Eine Kachel bei (0,0) (obere linke Ecke) eines 2D-Arrays hat zum Beispiel nichts in Richtung Norden oder Westen. Daher wird der lokale Bereich dort unterstrichen. irgendeinist eine leere Liste, die immer als positive Übereinstimmung gewertet wird.

 öffentliche statische Liste[] [] DefineTilemask (Liste nW, Liste n, Liste nE, Liste w, Liste Mitte, Liste e, Liste sW, Liste s, Liste sE) Liste[] template = neue Liste[9] nW, n, nE, w, Mitte, e, sW, s, sE; neue Liste zurückgeben[4] [] RotateLocalRange (Vorlage, 0), RotateLocalRange (Vorlage, 1), RotateLocalRange (Vorlage, 2), RotateLocalRange (Vorlage, 3);  öffentliche statische Liste[] RotateLocalRange (Liste[] localRange, int rotations) Liste[] gedrehtList = neue Liste[9] localRange [0], localRange [1], localRange [2], localRange [3], localRange [4], localRange [5], localRange [6], localRange [7], localRange [8]; für (int i = 0; i < rotations; i++)  List[] tempList = neue Liste[9] gedrehte Liste [6], gedrehte Liste [3], gedrehte Liste [0], gedrehte Liste [7], gedrehte Liste [4], gedrehte Liste [1], gedrehte Liste [8], gedrehte Liste [5], gedrehte Liste [2]; gedrehtList = TempList;  return gedrehtList; 

Es lohnt sich, die Implementierung dieses Codes zu erklären. Im DefineTilemask, Ich gebe neun Listen als Argumente an. Diese Listen werden in einem temporären 1D-Array abgelegt und dann in + 90 ° -Schritten gedreht, indem in ein anderes Array in einer anderen Reihenfolge geschrieben wird. Die gedrehten Kachelmasken werden dann in einem gezackten Array gespeichert, dessen Struktur ich zum Übertragen von Rotationsinformationen verwende. Wenn die Tilemask am äußeren Index 0 übereinstimmt, wird die Kachel ohne Drehung platziert. Eine Übereinstimmung am äußeren Index 1 gibt der Kachel eine Drehung um + 90 ° und so weiter.


Schnappen Sie sich die Reichweite eines Kachels

Dieser ist unkompliziert. Er liest den lokalen Bereich der aktuellen Kachel in ein 1D-Zeichenfeld und ersetzt ungültige Indizes durch Unterstriche.

 / * Usage: char [] localRange = GetLocalRange (Blaupause, Zeile, Spalte); Blaupause ist das 2D-Array, das das Gebäude definiert. Zeile und Spalte sind die Arrayindizes der aktuell ausgewerteten Kachel. * / public static char [] GetLocalRange (char [,] thisArray, int Zeile, int Spalte) char [] localRange = new char [9]; int localRangeCounter = 0; // Iteratoren zählen von -1, um den abgelesenen Wert nach links zu verschieben, und setzen den angeforderten Index in die Mitte. für (int i = -1; i < 2; i++)  for (int j = -1; j < 2; j++)  int tempRow = row + i; int tempColumn = column + j; if (IsIndexValid (thisArray, tempRow, tempColumn) == true)  localRange[localRangeCounter] = thisArray[tempRow, tempColumn];  else  localRange[localRangeCounter] = '_';  localRangeCounter++;   return localRange;  public static bool IsIndexValid (char[,] thisArray, int row, int column)  // Must check the number of rows at this point, or else an OutOfRange exception gets thrown when checking number of columns. if (row < thisArray.GetLowerBound (0) || row > (thisArray.GetUpperBound (0))) gibt false zurück; if (spalte < thisArray.GetLowerBound (1) || column > (thisArray.GetUpperBound (1))) gibt false zurück; sonst kehre zurück; 

Bewerten Sie einen lokalen Bereich mit einer Kachelmaske

Und hier passiert die Magie! TrySpawningTile wird der lokale Bereich, eine Kachelmaske, das zu erzeugende Wandstück, wenn die Kachelmaske übereinstimmt, und die Zeile und Spalte der zu bewertenden Kachel angegeben.

Die Methode, die den tatsächlichen Vergleich zwischen dem lokalen Bereich und der Kachelmaske durchführt (TileMatchesTemplate) gibt eine Kachelmaskenrotation aus, sobald eine Abweichung gefunden wird. Nicht gezeigt ist eine grundlegende Logik, die festlegt, welche Fliesenmasken für welche Fliesentypen verwendet werden sollen (Sie würden beispielsweise keine Wandfliesenmaske für ein Möbelstück verwenden)..

 / * Usage: TrySpawningTile (localRange, TileIDs.outerWallStraight, outerWallWall, floorEdgingHalf, Zeile, Spalte); * / // Diese Quaternionen haben eine Rotation von -90 entlang X, weil Modelle in Unity aufgrund der unterschiedlichen Aufwärtsachse in Blender gedreht werden müssen. public static readonly Quaternion ROTATE_NONE = Quaternion.Euler (-90, 0, 0); public static readonly Quaternion ROTATE_RIGHT = Quaternion.Euler (-90, 90, 0); public static readonly Quaternion ROTATE_FLIP = Quaternion.Euler (-90, 180, 0); public static readonly Quaternion ROTATE_LEFT = Quaternion.Euler (-90, -90, 0); bool TrySpawningTile (char [] needleArray, List.)[] [] templateArray, GameObject tilePrefab, int Zeile, int Spalte) Quaternion horizontalRotation; if (TileMatchesTemplate (needleArray, templateArray, out horizontalRotation) == true) SpawnTile (tilePrefab, Zeile, Spalte, HorizontalRotation); wahr zurückgeben;  else return false;  public static bool TileMatchesTemplate (char [] needleArray, List.)[] [] tileMaskJaggedArray, out Quaternion horizontalRotation) horizontalRotation = ROTATE_NONE; für (int i = 0; i < (tileMaskJaggedArray.Length); i++)  for (int j = 0; j < 9; j++)  if (j == 4) continue; // Skip checking the centre position (no need to ascertain that a block is what it says it is). if (tileMaskJaggedArray[i][j].Count != 0)  if (tileMaskJaggedArray[i][j].Contains (needleArray[j]) == false) break;  if (j == 8) // The loop has iterated nine times without stopping, so all tiles must match.  switch (i)  case 0: horizontalRotation = ROTATE_NONE; break; case 1: horizontalRotation = ROTATE_RIGHT; break; case 2: horizontalRotation = ROTATE_FLIP; break; case 3: horizontalRotation = ROTATE_LEFT; break;  return true;    return false;  void SpawnTile (GameObject tilePrefab, int row, int column, Quaternion horizontalRotation)  Instantiate (tilePrefab, new Vector3 (column, 0, -row), horizontalRotation); 

Bewertung und Schlussfolgerung

Vorteile dieser Implementierung

  • Sehr skalierbar; Fügen Sie einfach weitere Definitionen für Kachelmasken hinzu.
  • Die Prüfungen, die eine Kachelmaske durchführt, können so komplex sein, wie Sie möchten.
  • Der Code ist ziemlich transparent und seine Geschwindigkeit kann durch einige zusätzliche Überprüfungen des lokalen Bereichs verbessert werden, bevor Sie mit Kachelmasken beginnen.

Nachteile dieser Implementierung

  • Kann möglicherweise sehr teuer sein. Im absolut schlimmsten Fall haben Sie (36 × n) + 1 Array greift zu und ruft an List.Contains () bevor Sie eine Übereinstimmung finden, wo n ist die Anzahl der Kachelmasken-Definitionen, die Sie durchsuchen. Es ist wichtig, dass Sie die Liste der Kachelmasken eingrenzen, bevor Sie mit der Suche beginnen.

Fazit

Fliesenmasken werden nicht nur in der Welt der Generation oder in der Ästhetik verwendet, sondern auch in Elementen, die das Gameplay beeinflussen. Es ist nicht schwer, sich ein Puzzle-Spiel vorzustellen, bei dem Kachelmasken verwendet werden könnten, um den Status der Tafel oder die möglichen Bewegungen von Teilen zu bestimmen, oder Bearbeitungswerkzeuge könnten ein ähnliches System verwenden, um Blöcke aneinander zu schnappen. In diesem Artikel wurde eine grundlegende Umsetzung der Idee beschrieben, und ich hoffe, Sie fanden es nützlich.