Turmverteidigung in Einheit schaffen, Teil 1

Das Feld


  • Erstellen eines Kachelfelds.
  • Suchpfade mit der Breitensuche.
  • Implementieren Sie die Unterstützung für leere und Endkacheln sowie Wandkacheln.
  • Bearbeiten von Inhalten im Spielemodus.
  • Optionale Anzeige von Rasterfeldern und Pfaden.

Dies ist der erste Teil einer Reihe von Tutorials zum Erstellen eines einfachen Tower Defense- Spiels. In diesem Teil werden wir überlegen, ein Spielfeld zu schaffen, einen Weg zu finden und endgültige Kacheln und Wände zu platzieren.

Das Tutorial wurde in Unity 2018.3.0f2 erstellt.


Ein Feld, das zur Verwendung in einem Tower Defense-Genre-Kachelspiel bereit ist.

Tower Defense-Spiel


Tower Defense ist ein Genre, in dem es das Ziel des Spielers ist, Massen von Feinden zu zerstören, bis sie ihren Endpunkt erreichen. Der Spieler erreicht sein Ziel, indem er Türme baut, die Feinde angreifen. Dieses Genre hat viele Variationen. Wir werden ein Spiel mit einem Kachelfeld erstellen. Gegner bewegen sich über das Spielfeld in Richtung ihres Endpunkts und der Spieler schafft Hindernisse für sie.

Ich gehe davon aus, dass Sie bereits eine Reihe von Tutorials zum Verwalten von Objekten studiert haben .

Das Feld


Das Spielfeld ist der wichtigste Teil des Spiels, daher werden wir es zuerst erstellen. Dies ist ein Spielobjekt mit einer eigenen Komponente GameBoard , die durch Festlegen der Größe in zwei Dimensionen initialisiert werden kann, für die wir den Wert von Vector2Int . Das Feld sollte mit jeder Größe funktionieren, aber wir werden die Größe woanders auswählen, also werden wir eine gemeinsame Initialize dafür erstellen.

Zusätzlich visualisieren wir das Feld mit einem Viereck, das die Erde bezeichnet. Wir werden das Feldobjekt selbst nicht zu einem Viereck machen, sondern ihm ein untergeordnetes Quad-Objekt hinzufügen. Nach der Initialisierung werden wir die XY-Skala der Erde gleich der Größe des Feldes machen. Das heißt, jede Fliese hat eine Größe von einer quadratischen Maßeinheit für den Motor.

 using UnityEngine; public class GameBoard : MonoBehaviour { [SerializeField] Transform ground = default; Vector2Int size; public void Initialize (Vector2Int size) { this.size = size; ground.localScale = new Vector3(size.x, size.y, 1f); } } 

Warum explizit den Standardwert festlegen?
Die Idee ist, dass alles, was über den Unity-Editor anpassbar ist, über serialisierte versteckte Felder zugänglich ist. Diese Felder müssen nur im Inspektor geändert werden. Leider zeigt der Unity-Editor ständig eine Compiler-Warnung an, dass der Wert niemals zugewiesen wird. Wir können diese Warnung unterdrücken, indem wir den Standardwert für das Feld explizit festlegen. Sie können auch null zuweisen, aber ich habe es so gemacht, dass explizit gezeigt wird, dass wir einfach den Standardwert verwenden, der kein echter Verweis auf Masse ist, also verwenden wir default .

Erstellen Sie ein Feldobjekt in einer neuen Szene und fügen Sie ein untergeordnetes Quad mit einem Material hinzu, das wie Erde aussieht. Da wir ein einfaches Prototypenspiel erstellen, wird ein einheitliches grünes Material ausreichen. Drehen Sie das Quad um 90 ° entlang der X-Achse, so dass es auf der XZ-Ebene liegt.




Spielfeld.

Warum positionieren Sie das Spiel nicht auf der XY-Ebene?
Obwohl das Spiel im 2D-Raum stattfinden wird, werden wir es in 3D rendern, mit 3D-Feinden und einer Kamera, die relativ zu einem bestimmten Punkt bewegt werden kann. Die XZ-Ebene ist hierfür bequemer und entspricht der Standard-Skybox-Ausrichtung für die Umgebungsbeleuchtung.

Das Spiel


Erstellen Sie als Nächstes eine Game , die für das gesamte Spiel verantwortlich ist. In diesem Stadium bedeutet dies, dass das Feld initialisiert wird. Wir machen die Größe einfach über den Inspektor anpassbar und zwingen die Komponente, das Feld beim Aufwachen zu initialisieren. Verwenden wir die Standardgröße 11 × 11.

 using UnityEngine; public class Game : MonoBehaviour { [SerializeField] Vector2Int boardSize = new Vector2Int(11, 11); [SerializeField] GameBoard board = default; void Awake () { board.Initialize(boardSize); } } 

Feldgrößen können nur positiv sein und es macht wenig Sinn, ein Feld mit einer einzigen Kachel zu erstellen. Begrenzen wir also das Minimum auf 2 × 2. Dies kann durch Hinzufügen der OnValidate Methode erfolgen, wodurch die Mindestwerte zwangsweise begrenzt werden.

  void OnValidate () { if (boardSize.x < 2) { boardSize.x = 2; } if (boardSize.y < 2) { boardSize.y = 2; } } 

Wann wird Onvalidate aufgerufen?
Wenn es vorhanden ist, ruft der Unity-Editor es nach dem Ändern für die Komponenten auf. Einschließlich beim Hinzufügen zum Spielobjekt, nach dem Laden der Szene, nach dem erneuten Kompilieren, nach dem Ändern im Editor, nach dem Abbrechen / erneuten Versuchen und nach dem Zurücksetzen der Komponente.

OnValidate ist die einzige Stelle im Code, an der Sie Komponentenkonfigurationsfeldern Werte zuweisen können.


Spielobjekt.

Wenn Sie jetzt den Spielmodus starten, erhalten wir ein Feld mit der richtigen Größe. Positionieren Sie die Kamera während des Spiels so, dass das gesamte Brett sichtbar ist, kopieren Sie die Transformationskomponente, verlassen Sie den Spielmodus und fügen Sie die Werte der Komponente ein. Bei einem 11 × 11-Feld am Ursprung können Sie die Kamera in Position (0,10,0) positionieren und um 90 ° entlang der X-Achse drehen, um eine bequeme Ansicht von oben zu erhalten. Wir lassen die Kamera in dieser festen Position, aber es ist möglich ändere es in der Zukunft.


Kamera über dem Feld.

Wie kopiere ich Komponentenwerte und füge sie ein?
Über das Dropdown-Menü, das angezeigt wird, wenn Sie auf die Schaltfläche mit dem Zahnrad in der oberen rechten Ecke der Komponente klicken.

Fertighausfliese


Das Feld besteht aus quadratischen Kacheln. Feinde können sich von Kachel zu Kachel bewegen und die Kanten überqueren, jedoch nicht diagonal. Die Bewegung erfolgt immer zum nächsten Endpunkt. Bezeichnen wir grafisch die Bewegungsrichtung entlang der Kachel mit einem Pfeil. Sie können die Pfeilstruktur hier herunterladen.


Pfeil auf einem schwarzen Hintergrund.

Platzieren Sie die Pfeilstruktur in Ihrem Projekt und aktivieren Sie die Option Alpha als Transparenz . Erstellen Sie dann ein Material für den Pfeil, das das Standardmaterial sein kann, für das der Ausschnittmodus ausgewählt ist, und wählen Sie den Pfeil als Haupttextur aus.


Pfeilmaterial.

Warum den Cutout-Render-Modus verwenden?
Sie können den Pfeil mithilfe der Standard-Unity-Rendering-Pipeline verdecken.

Um jedes Plättchen im Spiel zu kennzeichnen, verwenden wir das Spielobjekt. Jeder von ihnen hat ein eigenes Quad mit Pfeilmaterial, genau wie das Feld ein Quad aus Erde hat. Wir werden der GameTile-Komponente auch Kacheln mit einem Link zu ihrem Pfeil hinzufügen.

 using UnityEngine; public class GameTile : MonoBehaviour { [SerializeField] Transform arrow = default; } 

Erstellen Sie ein Kachelobjekt und verwandeln Sie es in ein Fertighaus. Die Fliesen sind bodenbündig. Heben Sie den Pfeil daher etwas an, um Probleme mit der Tiefe beim Rendern zu vermeiden. Verkleinern Sie auch ein wenig, damit zwischen benachbarten Pfeilen wenig Platz ist. Ein Y-Versatz von 0,001 und eine Skala von 0,8, die für alle Achsen gleich ist, reichen aus.




Fertighausfliese.

Wo ist die vorgefertigte Kachelhierarchie?
Sie können den Fertighaus-Bearbeitungsmodus öffnen, indem Sie auf das Fertighaus doppelklicken oder das Fertighaus auswählen und im Inspektor auf die Schaltfläche Fertighaus öffnen klicken. Sie können den vorgefertigten Bearbeitungsmodus verlassen, indem Sie auf die Schaltfläche mit einem Pfeil in der oberen linken Ecke der Hierarchieüberschrift klicken.

Beachten Sie, dass die Kacheln selbst keine Spielobjekte sein müssen. Sie werden nur benötigt, um den Status des Feldes zu verfolgen. Wir könnten den gleichen Ansatz wie für das Verhalten in der Tutorials-Reihe " Objektverwaltung" verwenden. Aber in den frühen Stadien einfacher Spiele oder Prototypen von Spielobjekten sind wir ziemlich glücklich. Dies kann in Zukunft geändert werden.

Wir haben Fliesen


Um Kacheln zu erstellen, muss das GameBoard einen Link zum Kachelfertigteil haben.

  [SerializeField] GameTile tilePrefab = default; 


Link zur Fertighauskachel.

Anschließend kann er seine Instanzen mithilfe einer Doppelschleife über zwei Rasterdimensionen erstellen. Obwohl die Größe als X und Y ausgedrückt wird, ordnen wir die Kacheln in der XZ-Ebene sowie im Feld selbst an. Da das Feld relativ zum Ursprung zentriert ist, müssen wir die entsprechende Größe minus eins geteilt durch zwei von den Komponenten der Kachelposition subtrahieren. Bitte beachten Sie, dass dies eine Gleitkommadivision sein muss, da dies sonst bei geraden Größen nicht funktioniert.

  public void Initialize (Vector2Int size) { this.size = size; ground.localScale = new Vector3(size.x, size.y, 1f); Vector2 offset = new Vector2( (size.x - 1) * 0.5f, (size.y - 1) * 0.5f ); for (int y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++) { GameTile tile = Instantiate(tilePrefab); tile.transform.SetParent(transform, false); tile.transform.localPosition = new Vector3( x - offset.x, 0f, y - offset.y ); } } } 


Instanzen von Kacheln erstellt.

Später benötigen wir Zugriff auf diese Kacheln, damit wir sie in einem Array verfolgen können. Wir brauchen keine Liste, da sich die Größe des Feldes nach der Initialisierung nicht ändert.

  GameTile[] tiles; public void Initialize (Vector2Int size) { … tiles = new GameTile[size.x * size.y]; for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { GameTile tile = tiles[i] = Instantiate(tilePrefab); … } } } 

Wie funktioniert diese Aufgabe?
Dies ist eine verknüpfte Aufgabe. In diesem Fall bedeutet dies, dass wir sowohl dem Array-Element als auch der lokalen Variablen einen Link zur Kachelinstanz zuweisen. Diese Vorgänge entsprechen dem unten gezeigten Code.

 GameTile t = Instantiate(tilePrefab); tiles[i] = t; GameTile tile = t; 

Suche nach einem Weg


Zu diesem Zeitpunkt hat jede Kachel einen Pfeil, aber alle zeigen in die positive Richtung der Z-Achse, die wir als Norden interpretieren werden. Der nächste Schritt besteht darin, die richtige Richtung für die Kachel zu bestimmen. Wir tun dies, indem wir den Weg finden, dem die Feinde bis zum Endpunkt folgen müssen.

Fliesen Nachbarn


Die Wege verlaufen von Fliese zu Fliese im Norden, Osten, Süden oder Westen. Um die Suche zu vereinfachen, GameTile Track-Links zu den vier Nachbarn.

  GameTile north, east, south, west; 

Die Beziehungen zwischen den Nachbarn sind symmetrisch. Wenn das Plättchen der östliche Nachbar des zweiten Plättchens ist, ist das zweite der westliche Nachbar des ersten Plättchens. Fügen Sie GameTile eine allgemeine statische Methode GameTile , um diese Beziehung zwischen zwei Kacheln zu definieren.

  public static void MakeEastWestNeighbors (GameTile east, GameTile west) { west.east = east; east.west = west; } 

Warum eine statische Methode verwenden?
Wir können es zu einer Instanzmethode mit einem einzelnen Parameter machen, und in diesem Fall nennen wir es eastTile.MakeEastWestNeighbors(westTile) oder so ähnlich. In Fällen, in denen nicht klar ist, auf welche Kacheln die Methode aufgerufen werden soll, ist es besser, statische Methoden zu verwenden. Beispiele sind die Distance und Dot Methoden der Vector3 Klasse.

Einmal verbunden, sollte es sich nie ändern. In diesem Fall haben wir einen Fehler im Code gemacht. Sie können dies überprüfen, indem Sie beide Links vergleichen, bevor Sie null Werte zuweisen, und einen Fehler anzeigen, wenn dieser falsch ist. Sie können hierfür die Debug.Assert Methode verwenden.

  public static void MakeEastWestNeighbors (GameTile east, GameTile west) { Debug.Assert( west.east == null && east.west == null, "Redefined neighbors!" ); west.east = east; east.west = west; } 

Was macht Debug.Assert?
Wenn das erste Argument false , wird ein Bedingungsfehler angezeigt, wobei das zweite Argument verwendet wird, wenn es angegeben ist. Ein solcher Aufruf ist nur in Test-Builds enthalten, nicht jedoch in Release-Builds. Daher ist dies eine gute Möglichkeit, während des Entwicklungsprozesses Überprüfungen hinzuzufügen, die sich nicht auf die endgültige Version auswirken.

Fügen Sie eine ähnliche Methode hinzu, um Beziehungen zwischen den nördlichen und südlichen Nachbarn herzustellen.

  public static void MakeNorthSouthNeighbors (GameTile north, GameTile south) { Debug.Assert( south.north == null && north.south == null, "Redefined neighbors!" ); south.north = north; north.south = south; } 

Wir können diese Beziehung herstellen, wenn wir Kacheln in GameBoard.Initialize . Wenn die X-Koordinate größer als Null ist, können wir eine Ost-West-Beziehung zwischen der aktuellen und der vorherigen Kachel herstellen. Wenn die Y-Koordinate größer als Null ist, können wir eine Nord-Süd-Beziehung zwischen der aktuellen Kachel und der Kachel aus der vorherigen Zeile erstellen.

  for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … if (x > 0) { GameTile.MakeEastWestNeighbors(tile, tiles[i - 1]); } if (y > 0) { GameTile.MakeNorthSouthNeighbors(tile, tiles[i - size.x]); } } } 

Beachten Sie, dass die Kacheln an den Feldrändern keine vier Nachbarn haben. Ein oder zwei Nachbarreferenzen bleiben null .

Entfernung und Richtung


Wir werden nicht alle Feinde zwingen, ständig nach dem Weg zu suchen. Dies muss nur einmal pro Kachel durchgeführt werden. Dann können die Feinde von dem Plättchen, auf dem sie sich befinden, anfordern, wohin sie sich bewegen möchten. Wir werden diese Informationen in GameTile speichern, indem wir einen Link zur nächsten GameTile hinzufügen. Außerdem speichern wir die Entfernung zum Endpunkt, ausgedrückt als Anzahl der Kacheln, die besucht werden müssen, bevor der Feind den Endpunkt erreicht. Für Feinde sind diese Informationen nutzlos, aber wir werden sie verwenden, um die kürzesten Wege zu finden.

  GameTile north, east, south, west, nextOnPath; int distance; 

Jedes Mal, wenn wir entscheiden, dass wir nach Pfaden suchen müssen, müssen wir die Pfaddaten initialisieren. Bis der Pfad gefunden ist, gibt es kein nächstes Plättchen und die Entfernung kann als unendlich angesehen werden. Wir können uns dies als den maximal möglichen ganzzahligen Wert von int.MaxValue . Fügen Sie eine generische ClearPath Methode hinzu, um das GameTile auf diesen Status zurückzusetzen.

  public void ClearPath () { distance = int.MaxValue; nextOnPath = null; } 

Pfade können nur durchsucht werden, wenn wir einen Endpunkt haben. Dies bedeutet, dass die Kachel zum Endpunkt werden muss. Eine solche Kachel hat einen Abstand von Null und nicht die letzte Kachel, da der Pfad darauf endet. Fügen Sie eine generische Methode hinzu, die eine Kachel in einen Endpunkt verwandelt.

  public void BecomeDestination () { distance = 0; nextOnPath = null; } 

Letztendlich sollten sich alle Kacheln in einen Pfad verwandeln, damit ihre Entfernung nicht mehr int.MaxValue . Fügen Sie eine praktische Getter-Eigenschaft hinzu, um zu überprüfen, ob die Kachel derzeit einen Pfad hat.

  public bool HasPath => distance != int.MaxValue; 

Wie funktioniert diese Eigenschaft?
Dies ist ein verkürzter Eintrag für eine Getter-Eigenschaft, die nur einen Ausdruck enthält. Dies entspricht dem unten gezeigten Code.

  public bool HasPath { get { return distance != int.MaxValue; } } 

Der Pfeiloperator => kann auch einzeln für den Getter und Setter von Eigenschaften, für die Körper von Methoden, Konstruktoren und an einigen anderen Stellen verwendet werden.

Wir wachsen einen Weg


Wenn wir ein Plättchen mit einem Pfad haben, können wir es einen Pfad zu einem seiner Nachbarn wachsen lassen. Anfänglich ist das einzige Plättchen mit dem Pfad der Endpunkt. Wir beginnen also bei Null und erhöhen ihn von hier aus, wobei wir uns in die entgegengesetzte Richtung zur Bewegung der Feinde bewegen. Das heißt, alle unmittelbaren Nachbarn des Endpunkts haben einen Abstand von 1, und alle Nachbarn dieser Kacheln haben einen Abstand von 2 usw.

Fügen Sie eine versteckte GameTile Methode hinzu, um den Pfad zu einem seiner Nachbarn zu vergrößern, der über den Parameter angegeben wird. Die Entfernung zum Nachbarn ist eins mehr als die aktuelle Kachel, und der Pfad des Nachbarn gibt die aktuelle Kachel an. Diese Methode sollte nur für Kacheln aufgerufen werden, die bereits einen Pfad haben. Überprüfen wir dies also mit assert.

  void GrowPathTo (GameTile neighbor) { Debug.Assert(HasPath, "No path!"); neighbor.distance = distance + 1; neighbor.nextOnPath = this; } 

Die Idee ist, dass wir diese Methode einmal für jeden der vier Nachbarn der Kachel aufrufen. Da einige dieser Links null , überprüfen wir dies und stoppen gegebenenfalls die Ausführung. Wenn ein Nachbar bereits einen Pfad hat, sollten wir außerdem nichts tun und auch damit aufhören.

  void GrowPathTo (GameTile neighbor) { Debug.Assert(HasPath, "No path!"); if (neighbor == null || neighbor.HasPath) { return; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; } 

Die Art GameTile Weise, wie GameTile seine Nachbarn verfolgt, ist dem Rest des Codes unbekannt. Daher ist GrowPathTo ausgeblendet. Wir werden allgemeine Methoden hinzufügen, die die Kachel GrowPathTo ihren Pfad in eine bestimmte Richtung zu erweitern, indem GrowPathTo indirekt GrowPathTo aufrufen. Der Code, der im gesamten Feld sucht, sollte jedoch verfolgen, welche Kacheln besucht wurden. Daher wird es einen Nachbarn oder null wenn die Ausführung beendet wird.

  GameTile GrowPathTo (GameTile neighbor) { if (!HasPath || neighbor == null || neighbor.HasPath) { return null; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; return neighbor; } 

Fügen Sie nun Methoden zum Wachsen von Pfaden in bestimmte Richtungen hinzu.

  public GameTile GrowPathNorth () => GrowPathTo(north); public GameTile GrowPathEast () => GrowPathTo(east); public GameTile GrowPathSouth () => GrowPathTo(south); public GameTile GrowPathWest () => GrowPathTo(west); 

Breite Suche


GameBoard muss GameBoard dass alle Kacheln die richtigen GameBoard enthalten. Wir tun dies, indem wir eine Breitensuche durchführen. Beginnen wir mit der Endpunktkachel und erweitern dann den Pfad zu den Nachbarn, dann zu den Nachbarn dieser Kacheln und so weiter. Mit jedem Schritt erhöht sich der Abstand um eins, und die Pfade wachsen nie in Richtung der Kacheln, die bereits Pfade haben. Dadurch wird sichergestellt, dass alle Kacheln auf dem kürzesten Weg zum Endpunkt zeigen.

Was ist mit einem Pfad mit A *?
Der A * -Algorithmus ist die evolutionäre Entwicklung der Breitensuche. Es ist nützlich, wenn wir nach dem einzig kürzesten Weg suchen. Wir brauchen aber die kürzesten Wege, deshalb bietet A * keine Vorteile. Beispiele für die Breitensuche und A * in einem Sechseckraster mit Animation finden Sie in der Reihe von Tutorials zu Sechseckkarten .

Um die Suche durchzuführen, müssen wir die Kacheln verfolgen, die wir dem Pfad hinzugefügt haben, von denen wir den Pfad jedoch noch nicht vergrößert haben. Diese Sammlung von Kacheln wird oft als Suchgrenze bezeichnet. Es ist wichtig, dass die Kacheln in derselben Reihenfolge verarbeitet werden, in der sie dem Rahmen hinzugefügt werden. Verwenden Sie daher die Queue . Später müssen wir die Suche mehrmals durchführen, also legen wir sie als Feld des GameBoard .

 using UnityEngine; using System.Collections.Generic; public class GameBoard : MonoBehaviour { … Queue<GameTile> searchFrontier = new Queue<GameTile>(); … } 

Damit der Status des Spielfelds immer wahr ist, müssen wir die Pfade am Ende von Initialize , aber den Code in eine separate FindPaths Methode FindPaths . Zuerst müssen Sie den Pfad aller Kacheln löschen, dann eine Kachel zum Endpunkt machen und sie dem Rand hinzufügen. Lassen Sie uns zuerst die erste Kachel auswählen. Da tiles ein Array sind, können wir die foreach ohne Angst vor Speicherverschmutzung zu haben. Wenn wir später von einem Array zu einer Liste wechseln, müssen wir auch die foreach Schleifen durch for Schleifen ersetzen.

  public void Initialize (Vector2Int size) { … FindPaths(); } void FindPaths () { foreach (GameTile tile in tiles) { tile.ClearPath(); } tiles[0].BecomeDestination(); searchFrontier.Enqueue(tiles[0]); } 

Als nächstes müssen wir ein Plättchen von der Grenze nehmen und einen Pfad zu allen Nachbarn aufbauen und sie alle zur Grenze hinzufügen. Zuerst ziehen wir nach Norden, dann nach Osten, Süden und schließlich nach Westen.

  public void FindPaths () { foreach (GameTile tile in tiles) { tile.ClearPath(); } tiles[0].BecomeDestination(); searchFrontier.Enqueue(tiles[0]); GameTile tile = searchFrontier.Dequeue(); searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); } 

Wir wiederholen diese Phase, während sich Kacheln in der Grenze befinden.

  while (searchFrontier.Count > 0) { GameTile tile = searchFrontier.Dequeue(); searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); } 

Das Wachsen eines Pfades führt uns nicht immer zu einem neuen Plättchen. Vor dem Hinzufügen zur Warteschlange müssen wir den Wert auf null prüfen, aber wir können die Prüfung auf null bis nach der Ausgabe aus der Warteschlange verschieben.

  GameTile tile = searchFrontier.Dequeue(); if (tile != null) { searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathWest()); } 

Zeigen Sie die Pfade an


Jetzt haben wir ein Feld mit den richtigen Pfaden, aber bisher sehen wir dies nicht. Sie müssen die Pfeile so konfigurieren, dass sie entlang des Pfads durch ihre Kacheln zeigen. Dies kann durch Drehen erfolgen. Da diese GameTile immer gleich sind, fügen wir dem GameTile ein statisches Quaternion Feld für jede der Richtungen hinzu.

  static Quaternion northRotation = Quaternion.Euler(90f, 0f, 0f), eastRotation = Quaternion.Euler(90f, 90f, 0f), southRotation = Quaternion.Euler(90f, 180f, 0f), westRotation = Quaternion.Euler(90f, 270f, 0f); 

ShowPath auch die allgemeine ShowPath Methode hinzu. Wenn der Abstand Null ist, ist die Kachel der Endpunkt und es gibt nichts, auf das Sie zeigen können. Deaktivieren Sie daher den Pfeil. Andernfalls aktivieren Sie den Pfeil und stellen Sie seine Drehung ein. Die gewünschte Richtung kann durch Vergleichen von nextOnPath mit seinen Nachbarn bestimmt werden.

  public void ShowPath () { if (distance == 0) { arrow.gameObject.SetActive(false); return; } arrow.gameObject.SetActive(true); arrow.localRotation = nextOnPath == north ? northRotation : nextOnPath == east ? eastRotation : nextOnPath == south ? southRotation : westRotation; } 

Rufen Sie diese Methode am Ende für alle Kacheln auf GameBoard.FindPaths.

  public void FindPaths () { … foreach (GameTile tile in tiles) { tile.ShowPath(); } } 


Wege gefunden.

Warum verwandeln wir den Pfeil nicht direkt in GrowPathTo?
Trennung der Logik und Visualisierung der Suche. Später werden wir die Visualisierung deaktivieren. Wenn die Pfeile nicht angezeigt werden, müssen wir sie nicht jedes Mal drehen, wenn wir anrufen FindPaths.

Suchpriorität ändern


Es stellt sich heraus, dass, wenn der Endpunkt die südwestliche Ecke ist, alle Pfade genau nach Westen verlaufen, bis sie den Rand des Feldes erreichen, wonach sie nach Süden abbiegen. Hier stimmt alles, denn es gibt wirklich keine kürzeren Wege zum Endpunkt, weil diagonale Bewegungen unmöglich sind. Es gibt jedoch viele andere kürzeste Wege, die schöner aussehen können.

Um besser zu verstehen, warum solche Pfade gefunden werden, verschieben Sie den Endpunkt in die Mitte der Karte. Bei einer ungeraden Feldgröße ist es nur eine Kachel in der Mitte des Arrays.

  tiles[tiles.Length / 2].BecomeDestination(); searchFrontier.Enqueue(tiles[tiles.Length / 2]); 


Endpunkt in der Mitte.

Das Ergebnis erscheint logisch, wenn Sie sich daran erinnern, wie die Suche funktioniert. Da wir Nachbarn in der Nordost-Südwest-Ordnung hinzufügen, hat der Norden die höchste Priorität. Da wir die Suche in umgekehrter Reihenfolge durchführen, bedeutet dies, dass wir zuletzt nach Süden gefahren sind. Deshalb zeigen nur wenige Pfeile nach Süden und viele nach Osten.

Sie können das Ergebnis ändern, indem Sie die Prioritäten der Anweisungen festlegen. Lassen Sie uns nach Osten und Süden tauschen. Wir müssen also die Nord-Süd- und Ost-West-Symmetrie erhalten.

  searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathWest()) 


Die Suchreihenfolge ist Nord-Süd-Ost-West.

Es sieht hübscher aus, aber es ist besser, wenn die Wege ihre Richtung ändern und sich einer diagonalen Bewegung nähern, bei der es natürlich aussieht. Wir können dies tun, indem wir die Suchprioritäten benachbarter Kacheln in einem Schachbrettmuster umkehren.

Anstatt herauszufinden, welche Art von Kachel wir während der Suche verarbeiten, fügen wir der GameTileallgemeinen Eigenschaft hinzu, die angibt, ob die aktuelle Kachel eine Alternative ist.

  public bool IsAlternative { get; set; } 

Wir werden diese Eigenschaft in setzen GameBoard.Initialize. Markieren Sie zuerst die Kacheln als Alternative, wenn ihre X-Koordinate gerade ist.

  for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … tile.IsAlternative = (x & 1) == 0; } } 

Was macht die Operation (x & 1) == 0?
— (AND). . 1, 1. 10101010 00001111 00001010.

. 0 1. 1, 2, 3, 4 1, 10, 11, 100. , .

AND , , . , .

Zweitens ändern wir das Vorzeichen des Ergebnisses, wenn ihre Y-Koordinate gerade ist. Also werden wir ein Schachmuster erstellen.

  tile.IsAlternative = (x & 1) == 0; if ((y & 1) == 0) { tile.IsAlternative = !tile.IsAlternative; } 

Da FindPathswir die gleiche Reihenfolge wie die Suche nach alternativer Kachel halten, aber es zurück zu allen anderen Fliesen zu machen. Dies erzwingt den Weg zur diagonalen Bewegung und erzeugt Zickzackbewegungen.

  if (tile != null) { if (tile.IsAlternative) { searchFrontier.Enqueue(tile.GrowPathNorth()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathWest()); } else { searchFrontier.Enqueue(tile.GrowPathWest()); searchFrontier.Enqueue(tile.GrowPathEast()); searchFrontier.Enqueue(tile.GrowPathSouth()); searchFrontier.Enqueue(tile.GrowPathNorth()); } } 


Variable Suchreihenfolge.

Fliesen wechseln


Zu diesem Zeitpunkt sind alle Kacheln leer. Eine Kachel wird als Endpunkt verwendet, aber zusätzlich zum Fehlen eines sichtbaren Pfeils sieht sie genauso aus wie alle anderen. Wir werden die Möglichkeit hinzufügen, Kacheln zu ändern, indem wir Objekte darauf platzieren.

Kachelinhalt


Kachelobjekte selbst sind einfach eine Möglichkeit, Kachelinformationen zu verfolgen. Wir ändern diese Objekte nicht direkt. Fügen Sie stattdessen separaten Inhalt hinzu und platzieren Sie ihn auf dem Feld. Im Moment können wir zwischen leeren Kacheln und Endpunktkacheln unterscheiden. Erstellen Sie eine Aufzählung, um diese Fälle anzuzeigen GameTileContentType.

 public enum GameTileContentType { Empty, Destination } 

Erstellen Sie als Nächstes einen Komponententyp GameTileContent, mit dem Sie den Inhaltstyp über den Inspektor festlegen können. Der Zugriff darauf erfolgt über eine gemeinsame Getter-Eigenschaft.

 using UnityEngine; public class GameTileContent : MonoBehaviour { [SerializeField] GameTileContentType type = default; public GameTileContentType Type => type; } 

Anschließend erstellen wir Fertighäuser für zwei Inhaltstypen, von denen jeder eine Komponente GameTileContentmit dem entsprechenden angegebenen Typ enthält. Verwenden wir einen blau abgeflachten Würfel, um Endpunktkacheln zu bestimmen. Da es fast flach ist, braucht er keinen Collider. Verwenden Sie ein leeres Spielobjekt, um leere Inhalte vorzufertigen.

Ziel

leer

Fertighäuser des Endpunkts und leerer Inhalte.

Wir geben das Inhaltsobjekt an die leeren Kacheln weiter, da dann alle Kacheln immer den Inhalt haben, was bedeutet, dass wir die Links zu den Inhalten nicht auf Gleichheit überprüfen müssen null.

Inhaltsfabrik


Um den Inhalt bearbeitbar zu machen, erstellen wir hierfür auch eine Factory, die denselben Ansatz wie im Tutorial zur Objektverwaltung verwendet . Dies bedeutet, dass Sie GameTileContentden Überblick über Ihre ursprüngliche Fabrik behalten müssen, die nur einmal eingestellt werden sollte, und sich in der Methode an die Fabrik zurücksenden müssen Recycle.

  GameTileContentFactory originFactory; … public GameTileContentFactory OriginFactory { get => originFactory; set { Debug.Assert(originFactory == null, "Redefined origin factory!"); originFactory = value; } } public void Recycle () { originFactory.Reclaim(this); } 

Dies setzt die Existenz voraus GameTileContentFactory, daher erstellen wir dafür einen skriptfähigen Objekttyp mit der erforderlichen Methode Recycle. In dieser Phase werden wir uns nicht mit der Schaffung einer voll funktionsfähigen Fabrik befassen, die den Inhalt nutzt, also werden wir dafür sorgen, dass der Inhalt einfach zerstört wird. Später ist es möglich, die Wiederverwendung von Objekten zur Fabrik hinzuzufügen, ohne den Rest des Codes zu ändern.

 using UnityEngine; using UnityEngine.SceneManagement; [CreateAssetMenu] public class GameTileContentFactory : ScriptableObject { public void Reclaim (GameTileContent content) { Debug.Assert(content.OriginFactory == this, "Wrong factory reclaimed!"); Destroy(content.gameObject); } } 

Fügen Sie Getder Factory eine versteckte Methode mit einem Fertighaus als Parameter hinzu. Hier überspringen wir wieder die Wiederverwendung von Objekten. Er erstellt eine Instanz des Objekts, legt seine ursprüngliche Fabrik fest, verschiebt es in die Fabrikszene und gibt es zurück.

  GameTileContent Get (GameTileContent prefab) { GameTileContent instance = Instantiate(prefab); instance.OriginFactory = this; MoveToFactoryScene(instance.gameObject); return instance; } 

Die Instanz wurde in die Factory-Content-Szene verschoben, die bei Bedarf erstellt werden kann. Wenn wir uns im Editor befinden, müssen wir vor dem Erstellen einer Szene überprüfen, ob sie vorhanden ist, falls wir sie während eines heißen Neustarts aus den Augen verlieren.

  Scene contentScene; … void MoveToFactoryScene (GameObject o) { if (!contentScene.isLoaded) { if (Application.isEditor) { contentScene = SceneManager.GetSceneByName(name); if (!contentScene.isLoaded) { contentScene = SceneManager.CreateScene(name); } } else { contentScene = SceneManager.CreateScene(name); } } SceneManager.MoveGameObjectToScene(o, contentScene); } 

Wir haben nur zwei Arten von Inhalten, also fügen Sie einfach zwei vorgefertigte Konfigurationsfelder für sie hinzu.

  [SerializeField] GameTileContent destinationPrefab = default; [SerializeField] GameTileContent emptyPrefab = default; 

Das Letzte, was getan werden muss, damit die Factory funktioniert, ist das Erstellen einer allgemeinen Methode Getmit einem Parameter GameTileContentType, der eine Instanz des entsprechenden Fertighauses empfängt.

  public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; } 

Ist es obligatorisch, jeder Kachel eine separate Instanz leeren Inhalts hinzuzufügen?
, . . , - , , , , . , . , , .

Lassen Sie uns ein Factory-Asset erstellen und seine Links zu Fertighäusern konfigurieren.


Inhaltsfabrik

Und dann den GameLink zur Fabrik weitergeben.

  [SerializeField] GameTileContentFactory tileContentFactory = default; 


Spiel mit einer Fabrik.

Eine Fliese antippen


Um das Feld zu ändern, müssen wir in der Lage sein, eine Kachel auszuwählen. Wir werden es im Spielemodus ermöglichen. Wir werden einen Strahl in die Szene an der Stelle senden, an der der Spieler auf das Spielfenster geklickt hat. Wenn sich der Strahl mit dem Plättchen schneidet, hat der Spieler es berührt, dh es muss geändert werden. Gameübernimmt die Eingabe des Spielers, ist jedoch dafür verantwortlich zu bestimmen, welches Plättchen der Spieler berührt hat GameBoard.

Nicht alle Strahlen schneiden sich mit der Kachel, daher erhalten wir manchmal nichts. Daher fügen wir der GameBoardMethode hinzu GetTile, die immer immer anfänglich zurückgibt null(dies bedeutet, dass die Kachel nicht gefunden wurde).

  public GameTile GetTile (Ray ray) { return null; } 

Um festzustellen, ob ein Strahl eine Kachel überschritten hat, müssen wir Physics.Raycastden Strahl als Argument angeben. Es gibt Informationen darüber zurück, ob es eine Kreuzung gab. Wenn ja, können wir die Kachel zurückgeben, obwohl wir noch nicht wissen, welche, also werden wir sie vorerst zurückgeben null.

  public GameTile TryGetTile (Ray ray) { if (Physics.Raycast(ray) { return null; } return null; } 

Um herauszufinden, ob es eine Kreuzung mit einer Kachel gab, benötigen wir weitere Informationen über die Kreuzung. Physics.Raycastkann diese Informationen mit dem zweiten Parameter bereitstellen RaycastHit. Dies ist der Ausgabeparameter, der durch das Wort outdavor angezeigt wird . Dies bedeutet, dass ein Methodenaufruf der Variablen, die wir an sie übergeben, einen Wert zuweisen kann.

  RaycastHit hit; if (Physics.Raycast(ray, out hit) { return null; } 

Wir können die Deklaration der Variablen einbetten, die für die Ausgabeparameter verwendet werden.

  if (Physics.Raycast(ray, out RaycastHit hit) { return null; } 

Es ist uns egal, mit welchem ​​Collider die Kreuzung aufgetreten ist, wir verwenden nur die XZ-Kreuzungsposition, um die Kachel zu bestimmen. Wir erhalten die Koordinaten der Kachel, indem wir den Koordinaten des Schnittpunkts die halbe Größe des Feldes hinzufügen und dann die Ergebnisse in ganzzahlige Werte konvertieren. Der endgültige Kachelindex ist die X-Koordinate plus die Y-Koordinate multipliziert mit der Feldbreite.

  if (Physics.Raycast(ray, out RaycastHit hit)) { int x = (int)(hit.point.x + size.x * 0.5f); int y = (int)(hit.point.z + size.y * 0.5f); return tiles[x + y * size.x]; } 

Dies ist jedoch nur möglich, wenn die Koordinaten der Kachel innerhalb des Feldes liegen. Wir werden dies überprüfen. Ist dies nicht der Fall, wird die Kachel nicht zurückgegeben.

  int x = (int)(hit.point.x + size.x * 0.5f); int y = (int)(hit.point.z + size.y * 0.5f); if (x >= 0 && x < size.x && y >= 0 && y < size.y) { return tiles[x + y * size.x]; } 

Inhaltsänderung


Fügen Sie der GameTileallgemeinen Eigenschaft hinzu, damit Sie den Inhalt der Kachel ändern können Content. Sein Getter gibt einfach den Inhalt zurück, und der Setter verwirft den vorherigen Inhalt, falls vorhanden, und platziert den neuen Inhalt.

  GameTileContent content; public GameTileContent Content { get => content; set { if (content != null) { content.Recycle(); } content = value; content.transform.localPosition = transform.localPosition; } } 

Dies ist der einzige Ort, an dem Sie den Inhalt überprüfen müssen null, da wir anfangs keinen Inhalt haben. Um dies zu gewährleisten, führen wir assert aus, damit der Setter nicht mit aufgerufen wird null.

  set { Debug.Assert(value != null, "Null assigned to content!"); … } 

Und schließlich brauchen wir eine Spielereingabe. Das Konvertieren eines Mausklicks in einen Strahl kann durch Aufrufen ScreenPointToRaymit Input.mousePositionals Argument erfolgen. Der Anruf muss für die Hauptkamera getätigt werden, auf die über zugegriffen werden kann Camera.main. Fügen Sie dazu die Eigenschaft c hinzu Game.

  Ray TouchRay => Camera.main.ScreenPointToRay(Input.mousePosition); 

Anschließend fügen wir eine Methode hinzu Update, mit der überprüft wird, ob während des Upgrades die Hauptmaus-Taste gedrückt wurde. Rufen Sie dazu Input.GetMouseButtonDownmit Null als Argument auf. Wenn die Taste gedrückt wurde, verarbeiten wir die Berührung des Spielers, dh wir nehmen das Plättchen vom Feld und legen den Endpunkt als Inhalt fest, wobei wir es ab Werk nehmen.

  void Update () { if (Input.GetMouseButtonDown(0)) { HandleTouch(); } } void HandleTouch () { GameTile tile = GetTile(TouchRay); if (tile != null) { tile.Content = tileContentFactory.Get(GameTileContentType.Destination); } } 

Jetzt können wir jede Kachel durch Drücken des Cursors in einen Endpunkt verwandeln.


Mehrere Endpunkte.

Das Feld richtig machen


Obwohl wir Kacheln in Endpunkte verwandeln können, hat dies bisher keine Auswirkungen auf die Pfade. Außerdem haben wir noch keinen leeren Inhalt für Kacheln festgelegt. Die Richtigkeit und Integrität des Feldes zu wahren GameBoard, ist eine Aufgabe . Geben wir ihm also die Verantwortung, den Inhalt der Kachel festzulegen. Um dies zu implementieren, geben wir ihm über seine Methode einen Link zur Content Factory Intializeund verwenden ihn, um allen Kacheln eine Instanz leeren Inhalts zu geben.

  GameTileContentFactory contentFactory; public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { this.size = size; this.contentFactory = contentFactory; ground.localScale = new Vector3(size.x, size.y, 1f); tiles = new GameTile[size.x * size.y]; for (int i = 0, y = 0; y < size.y; y++) { for (int x = 0; x < size.x; x++, i++) { … tile.Content = contentFactory.Get(GameTileContentType.Empty); } } FindPaths(); } 

Jetzt muss ich Gamemeine Fabrik auf das Feld verlegen.

  void Awake () { board.Initialize(boardSize, tileContentFactory); } 

Warum nicht ein Game Factory-Feld zum GameBoard hinzufügen?
, , . , .

Da wir jetzt mehrere Endpunkte haben, ändern wir diese GameBoard.FindPathsso, dass sie BecomeDestinationjeweils aufgerufen werden und alle an der Grenze hinzugefügt werden. Und das ist alles, was Sie brauchen, um mehrere Endpunkte zu unterstützen. Alle anderen Kacheln werden wie gewohnt gelöscht. Dann löschen wir den fest eingestellten Endpunkt in der Mitte.

  void FindPaths () { foreach (GameTile tile in tiles) { if (tile.Content.Type == GameTileContentType.Destination) { tile.BecomeDestination(); searchFrontier.Enqueue(tile); } else { tile.ClearPath(); } } //tiles[tiles.Length / 2].BecomeDestination(); //searchFrontier.Enqueue(tiles[tiles.Length / 2]); … } 

Wenn wir jedoch Kacheln in Endpunkte verwandeln können, sollten wir in der Lage sein, den umgekehrten Vorgang auszuführen und Endpunkte in leere Kacheln umzuwandeln. Aber dann können wir ein Feld ohne Endpunkte bekommen. In diesem Fall kann FindPathsseine Aufgabe nicht ausgeführt werden. Dies geschieht, wenn der Rand nach der Pfadinitialisierung für alle Zellen leer ist. Wir bezeichnen dies als einen ungültigen Zustand des Feldes, falseder die Ausführung zurückgibt und abschließt. Andernfalls kehren Sie am Ende zurück true.

  bool FindPaths () { foreach (GameTile tile in tiles) { … } if (searchFrontier.Count == 0) { return false; } … return true; } 

Der einfachste Weg, Unterstützung für das Entfernen von Endpunkten zu implementieren, sodass es sich um eine Switch-Operation handelt. Durch Klicken auf die leeren Kacheln werden sie in Endpunkte umgewandelt, und durch Klicken auf die Endpunkte werden sie gelöscht. Jetzt wird der Inhalt geändert GameBoard, und wir geben ihm eine allgemeine Methode, ToggleDestinationderen Parameter die Kachel ist. Wenn die Kachel der Endpunkt ist, machen Sie sie leer und rufen Sie auf FindPaths. Ansonsten machen wir es zum Endpunkt und nennen es auch FindPaths.

  public void ToggleDestination (GameTile tile) { if (tile.Content.Type == GameTileContentType.Destination) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } } 

Das Hinzufügen eines Endpunkts kann niemals einen ungültigen Feldstatus erzeugen, und das Löschen eines Endpunkts kann dies. Daher werden wir prüfen, ob die Ausführung erfolgreich war, FindPathsnachdem wir die Kachel leer gemacht haben. Wenn nicht, brechen Sie die Änderung ab, drehen Sie die Kachel zurück zum Endpunkt und rufen Sie erneut FindPathsauf, um zum vorherigen korrekten Status zurückzukehren.

  if (tile.Content.Type == GameTileContentType.Destination) { tile.Content = contentFactory.Get(GameTileContentType.Empty); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } } 

Kann die Validierung effizienter gestaltet werden?
, . , . , . FindPaths , .

Jetzt Initializekönnen wir am Ende ToggleDestinationmit der zentralen Kachel als Argument aufrufen, anstatt explizit aufzurufen FindPaths. Dies ist das einzige Mal, dass wir mit einem ungültigen Feldstatus beginnen, aber wir werden garantiert mit dem richtigen Status enden.

  public void Initialize ( Vector2Int size, GameTileContentFactory contentFactory ) { … //FindPaths(); ToggleDestination(tiles[tiles.Length / 2]); } 

Schließlich erzwingen wir den GameAufruf, ToggleDestinationanstatt den Inhalt der Kachel selbst festzulegen.

  void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { //tile.Content = //tileContentFactory.Get(GameTileContentType.Destination); board.ToggleDestination(tile); } } 


Mehrere Endpunkte mit korrekten Pfaden.

Sollten wir Game nicht verbieten, den Inhalt der Kachel direkt festzulegen?
. . , Game . , .

Die Wände


Das Ziel der Turmverteidigung ist es, zu verhindern, dass Feinde den Endpunkt erreichen. Dieses Ziel wird auf zwei Arten erreicht. Erstens töten wir sie und zweitens verlangsamen wir sie, damit mehr Zeit bleibt, sie zu töten. Auf dem Kachelfeld kann die Zeit verlängert werden, wodurch sich die Entfernung erhöht, die Feinde benötigen. Dies kann erreicht werden, indem Hindernisse auf dem Feld platziert werden. Normalerweise sind dies Türme, die auch Feinde töten, aber in diesem Tutorial beschränken wir uns nur auf Mauern.

Inhalt


Wände sind eine andere Art von Inhalten. Fügen wir GameTileContentTypeihnen also ein Element hinzu.

 public enum GameTileContentType { Empty, Destination, Wall } 

Erstellen Sie dann das Wandfertigteil. Dieses Mal erstellen wir ein Spielobjekt mit dem Inhalt der Kachel und fügen einen untergeordneten Würfel hinzu, der sich oben auf dem Feld befindet und die gesamte Kachel ausfüllt. Machen Sie es eine halbe Einheit hoch und speichern Sie den Collider, da die Wände einen Teil der dahinter liegenden Fliesen optisch überlappen können. Wenn ein Spieler eine Wand berührt, beeinflusst er daher das entsprechende Plättchen.

Wurzel

Würfel

Fertighaus

Fertighaus.

Fügen Sie das Wandfertigteil sowohl im Code als auch im Inspektor zur Fabrik hinzu.

  [SerializeField] GameTileContent wallPrefab = default; … public GameTileContent Get (GameTileContentType type) { switch (type) { case GameTileContentType.Destination: return Get(destinationPrefab); case GameTileContentType.Empty: return Get(emptyPrefab); case GameTileContentType.Wall: return Get(wallPrefab); } Debug.Assert(false, "Unsupported type: " + type); return null; } 


Fabrik mit vorgefertigter Wand.

Wände ein- und ausschalten


Fügen Sie GameBoarddie Ein / Aus-Methode der Wände hinzu, wie wir es für den Endpunkt getan haben. Zunächst werden wir den falschen Status des Feldes nicht überprüfen.

  public void ToggleWall (GameTile tile) { if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(GameTileContentType.Wall); FindPaths(); } } 

Wir bieten Unterstützung für das Umschalten nur zwischen leeren Kacheln und Wandkacheln, sodass Wände Endpunkte nicht direkt ersetzen können. Daher erstellen wir nur dann eine Wand, wenn die Kachel leer ist. Außerdem sollten die Wände die Suche nach dem Pfad blockieren. Aber jedes Plättchen muss einen Pfad zum Endpunkt haben, sonst bleiben die Feinde stecken. Dazu müssen wir erneut die Validierung verwenden FindPathsund die Änderungen verwerfen, wenn sie einen falschen Feldstatus erstellt haben.

  else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Wall); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } } 

Das Ein- und Ausschalten von Wänden wird viel häufiger verwendet als das Ein- und Ausschalten von Endpunkten. Daher werden wir das Umschalten von Wänden zum GameHauptanliegen machen. Die Endpunkte können durch eine zusätzliche Berührung (normalerweise die rechte Maustaste) umgeschaltet werden, die durch Übergabe an einen Input.GetMouseButtonDownWert von 1 erkannt werden kann .

  void Update () { if (Input.GetMouseButtonDown(0)) { HandleTouch(); } else if (Input.GetMouseButtonDown(1)) { HandleAlternativeTouch(); } } void HandleAlternativeTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { board.ToggleDestination(tile); } } void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { board.ToggleWall(tile); } } 


Jetzt haben wir die Wände.

Warum bekomme ich große Lücken zwischen den Schatten diagonal benachbarter Wände?
, , , . , , far clipping plane . , far plane 20 . , MSAA, .

Stellen wir außerdem sicher, dass Endpunkte Wände nicht direkt ersetzen können.

  public void ToggleDestination (GameTile tile) { if (tile.Content.Type == GameTileContentType.Destination) { … } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Destination); FindPaths(); } } 

Pfadsuchensperre


Damit die Wände die Suche nach dem Pfad blockieren, müssen wir dem Suchrahmen keine Kacheln mit Wänden hinzufügen. Dies kann erreicht werden, indem gezwungen wird, GameTile.GrowPathTokeine Fliesen mit Wänden zurückzugeben. Der Pfad sollte jedoch immer noch in Richtung der Wand wachsen, damit alle Kacheln auf dem Feld einen Pfad haben. Dies ist notwendig, da es möglich ist, dass ein Plättchen mit Feinden plötzlich zu einer Wand wird.

  GameTile GrowPathTo (GameTile neighbor) { if (!HasPath || neighbor == null || neighbor.HasPath) { return null; } neighbor.distance = distance + 1; neighbor.nextOnPath = this; return neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; } 

Um sicherzustellen, dass alle Kacheln einen Pfad haben, GameBoard.FindPathsmüssen sie diesen nach Abschluss der Suche überprüfen. Ist dies nicht der Fall, ist der Status des Feldes ungültig und muss zurückgegeben werden false. Es ist nicht erforderlich, die Pfadvisualisierung für ungültige Zustände zu aktualisieren, da das Feld zum vorherigen Zustand zurückkehrt.

  bool FindPaths () { … foreach (GameTile tile in tiles) { if (!tile.HasPath) { return false; } } foreach (GameTile tile in tiles) { tile.ShowPath(); } return true; } 


Wände beeinflussen den Weg.

Um sicherzustellen, dass die Wände tatsächlich die richtigen Wege haben, müssen Sie die Würfel durchscheinend machen.


Transparente Wände.

Beachten Sie, dass das Erfordernis der Richtigkeit aller Pfade nicht zulässt, dass Wände einen Teil des Feldes einschließen, in dem es keinen Endpunkt gibt. Wir können die Karte teilen, aber nur, wenn in jedem Teil mindestens ein Endpunkt vorhanden ist. Außerdem muss jede Wand an eine leere Kachel oder einen leeren Endpunkt angrenzen, da sie sonst keinen Pfad haben kann. Zum Beispiel ist es unmöglich, einen festen Block aus 3 × 3 Wänden herzustellen.

Versteck den Weg


Durch die Visualisierung der Pfade können wir sehen, wie die Pfadsuche funktioniert, und sicherstellen, dass sie tatsächlich korrekt ist. Aber es muss dem Spieler nicht gezeigt werden oder zumindest nicht unbedingt. Lassen Sie uns daher die Möglichkeit bieten, die Pfeile auszuschalten. Dies kann durch Hinzufügen zur GameTileallgemeinen Methode erfolgen HidePath, bei der der Pfeil einfach deaktiviert wird.

  public void HidePath () { arrow.gameObject.SetActive(false); } 

Der Pfadzuordnungsstatus ist Teil des Feldstatus. Fügen Sie GameBoarddem Standard ein boolesches Feld hinzu, das falsedem Status entspricht, sowie eine gemeinsame Eigenschaft als Getter und Setter. Der Setter muss Pfade auf allen Kacheln ein- oder ausblenden.

  bool showPaths; public bool ShowPaths { get => showPaths; set { showPaths = value; if (showPaths) { foreach (GameTile tile in tiles) { tile.ShowPath(); } } else { foreach (GameTile tile in tiles) { tile.HidePath(); } } } } 

Jetzt sollte die Methode FindPathsnur dann aktualisierte Pfade anzeigen, wenn das Rendern aktiviert ist.

  bool FindPaths () { … if (showPaths) { foreach (GameTile tile in tiles) { tile.ShowPath(); } } return true; } 

Standardmäßig ist die Pfadvisualisierung deaktiviert. Schalten Sie den Pfeil in der Kachelfertigung aus.


Der Fertighauspfeil ist standardmäßig inaktiv.

Wir machen es so, dass es Gameden Visualisierungsstatus ändert, wenn eine Taste gedrückt wird. Es wäre logisch, die P-Taste zu verwenden, aber es ist auch ein Hotkey zum Aktivieren / Deaktivieren des Spielemodus im Unity-Editor. Infolgedessen wechselt die Visualisierung, wenn der Hotkey zum Verlassen des Spielemodus verwendet wird, was nicht sehr gut aussieht. Verwenden wir also die V-Taste (kurz für Visualisierung).


Keine Pfeile.

Rasteranzeige


Wenn die Pfeile ausgeblendet sind, wird es schwierig, die Position jeder Kachel zu erkennen. Fügen wir die Gitterlinien hinzu. Laden Sie daher Textur mit einem quadratischen Grenze Netz , die als eine einzelne Fliese Kontur verwendet werden können.


Maschentextur.

Wir werden diese Textur nicht einzeln zu jeder Fliese hinzufügen, sondern auf den Boden auftragen. Wir werden dieses Raster jedoch optional machen sowie die Visualisierung von Pfaden. Daher werden wir GameBoarddem Konfigurationsfeld hinzufügen Texture2Dund eine Netztextur dafür auswählen.

  [SerializeField] Texture2D gridTexture = default; 


Feld mit Netzstruktur.

Fügen Sie ein weiteres boolesches Feld und eine Eigenschaft hinzu, um den Status der Rastervisualisierung zu steuern. In diesem Fall muss der Setter das Material der Erde ändern, was durch Aufrufen der GetComponent<MeshRenderer>Erde und Zugriff auf die Eigenschaft des materialErgebnisses implementiert werden kann . Wenn das Raster angezeigt werden muss, weisen wir die mainTextureRastertextur der Materialeigenschaft zu. Andernfalls weisen Sie es ihm zu null. Beachten Sie, dass beim Ändern der Textur des Materials Duplikate der Materialinstanz erstellt werden, sodass diese unabhängig vom Materialelement wird.

  bool showGrid, showPaths; public bool ShowGrid { get => showGrid; set { showGrid = value; Material m = ground.GetComponent<MeshRenderer>().material; if (showGrid) { m.mainTexture = gridTexture; } else { m.mainTexture = null; } } } 

Lassen Sie uns Gamedie Visualisierung des Gitters mit der G-Taste umschalten.

  void Update () { … if (Input.GetKeyDown(KeyCode.G)) { board.ShowGrid = !board.ShowGrid; } } 

Fügen Sie außerdem die Standard-Netzvisualisierung hinzu Awake.

  void Awake () { board.Initialize(boardSize, tileContentFactory); board.ShowGrid = true; } 


Unskaliertes Gitter.

Bisher haben wir eine Grenze um das gesamte Feld. Es passt zur Textur, aber das brauchen wir nicht. Wir müssen die Haupttextur des Materials so skalieren, dass sie der Größe des Gitters entspricht. Sie können dies tun, indem Sie die SetTextureScaleMaterialmethode mit dem Namen der Textur-Eigenschaft ( _MainTex ) und der zweidimensionalen Größe aufrufen . Wir können direkt die Größe des Feldes verwenden, das indirekt in einen Wert umgewandelt wird Vector2.

  if (showGrid) { m.mainTexture = gridTexture; m.SetTextureScale("_MainTex", size); } 

ohne

mit

Skaliertes Raster mit ein- und ausgeschalteter Pfadvisualisierung.

Zu diesem Zeitpunkt haben wir also ein funktionierendes Feld für ein Kachelspiel des Tower Defense-Genres. Im nächsten Tutorial werden wir Feinde hinzufügen.

Repository

Pdf

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


All Articles