Articles

Unity: So erstellen Sie programmgesteuert 2D-Tilemaps

Für ein Video-Tutorial klicken Sie hier

Wenn Sie verschiedene Tilemaps in verschiedenen Größen erstellen müssen, lesen Sie den zweiten Teil dieses Tutorials

Die Tilemap-Komponente wurde in Unity 2017.2 eingeführt und hat den 2D-Spieleentwicklungsprozess erheblich vereinfacht. Mit der Version 2018.3 wurde eine isometrische Kachelkarte eingeführt, die großartige Unterstützung für 2.5D-Spiele bietet. Kürzlich hatte ich die Gelegenheit, eng mit dieser Komponente zu arbeiten, und wurde mit der Aufgabe herausgefordert, Kacheln programmgesteuert zu erstellen. In meinem letzten Spiel habe ich viele Levels und alle haben das gleiche Tilemap-basierte Board. Aber das Board selbst hat ein Level-einzigartiges Setup. Als professioneller Entwickler wollte ich natürlich keine 60 Spielszenen erstellen und alle Ebenen von Hand bemalen, sondern einen Mechanismus haben, um das Brett je nach Eingabe mit den richtigen Elementen zu füllen. Wenn Sie neugierig sind, wie das Endergebnis aussieht, finden Sie hier den Link zum Spiel.

Der Quellcode ist auf GitHub verfügbar, bitte finden Sie den Link am Ende dieses Tutorials.

Ich verwende die neueste verfügbare Unity 2019.2.0.1f. Um mit diesem Tutorial arbeiten zu können, sollten Sie mindestens Version 2018.3 haben. Wir werden ein isometrisches Gitter verwenden, aber die beschriebene Technik ist auf jeden Typ anwendbar.

Bevor ich anfange, empfehle ich dringend, diesen brillanten isometrischen 2D-Umgebungen mit Tilemap-Blogeintrag durchzulesen, um ein grundlegendes Verständnis der isometrischen Tilemap zu erhalten.

Da wir in einer 2D-Umgebung arbeiten, sollten Sie ein neues 2D-Projekt einrichten (siehe 2DAnd3DModeSettings)

Es sind 3 Hauptkomponenten erforderlich, um unser Ziel zu erreichen: ein Spielbrett, auf dem das Spiel stattfindet, ein Ort, an dem eine Levelbeschreibung und ein Code aufbewahrt werden, der eine Verbindung zueinander herstellt.

Teil 1. Erstellen des Boards

Beginnen wir mit dem Import der benötigten Bild-Assets. Ich werde die gleichen Bilder verwenden, die ich für mein Spiel verwendet habe:

Used to create a base tilemap level

Used as a path

Used to mark the start and end points of a pfad

Wir behalten Bilder im Kachelordner, ziehen sie einfach per Drag & Drop dorthin.

Kacheln einrichten

Hinweis! Sie sollten für jedes Kachelbild die richtige Größe „Pixel pro Einheit“ festlegen (weitere Informationen finden Sie unter Isometrische 2D-Umgebungen mit Kachelkarte). Für diese Bilder ist der Wert 1096.

Bildeinrichtung

Es empfiehlt sich, verschiedene Ebenen einer Szene in dedizierte Spielobjekte mit beschreibenden Namen zu unterteilen. Stellen Sie sich den typischen Spielbildschirm vor, der eine Spielzone, eine Benutzeroberfläche, Anzeigenplatzierungen usw. enthalten könnte.

Nach diesem Ansatz erstellen wir ein neues leeres Spielobjekt mit dem Namen GameZone, in dem das Brett und alle Spielelemente platziert werden können.

Jetzt ist es an der Zeit, aus den zuvor importierten Bildern tatsächliche Kacheln zu erstellen

Drücken Sie Fenster -> 2D -> Kachelpalette

Kachelpalette

Die Kachelpalettenansicht wird geöffnet. Klicken Sie auf „Neue Palette erstellen“ und erstellen Sie eine Palette:

New Palette

Drag and drop tile images one by one to create an actual tiles.

Tiles setup

With those tiles we can finally create the game board which will be filled with elements programmatically on a level startup later.

Suchen Sie GameZone Klicken Sie in der Hierarchieansicht mit der rechten Maustaste → 2D-Objekt → Isometrische Kachelkarte. Ein neues Spielobjekt Grid wird erstellt.

Die Plattengröße beträgt 11×11 Kacheln und wir können mit dem Malen beginnen. Wählen Sie das Kastenpinsel-Werkzeug (4. Element von links) in der Ansicht „Kachelpalette“ und dann „Kachel reinigen“. Malen Sie es in der Szenenansicht.

Nachdem Sie mit dem Malen fertig sind, sollten Sie die Tilemap-Grenzen manuell komprimieren. Dazu müssen Sie Tilemap in der Hierarchieansicht auswählen, auf eine Einstellungstaste (Zahnrad) in der Tilemap-Komponente klicken und „Tilemap-Grenzen komprimieren“ auswählen

Tilemap-Grenzen komprimieren

Gut gemacht, das Spielbrett ist einsatzbereit! Wir werden in Teil 3 darauf zurückkommen.

Teil 2. Level-Daten

Da wir dieselbe Szene und dasselbe Spielbrett wiederverwenden möchten, sollten die Level-Daten irgendwo aufbewahrt werden. Eine einfache Lösung dafür ist eine einfache JSON-Datei, die beschreibt, wie jede Ebene erstellt werden soll.

Es ist wichtig zu verstehen, was wir erreichen wollen, deshalb muss ich ein paar Worte über die Mechanik sagen. Im Spiel gibt es Objekte, die sich vom Anfang bis zum Ende entlang des Pfades bewegen (ähnlich wie in Zuma), und das Ziel des Spielers ist es, sie alle zu zerstören. In diesem Tutorial erstellen wir diesen Pfad, der für jedes Level einzigartig ist.

Okay, zurück zum Projekt.

Es gibt mehrere Möglichkeiten, über ein Skript auf externe Daten in einer Laufzeit zuzugreifen. Hier werden wir Ressourcenordner verwenden

Erstellen wir einen neuen Ordner — Dateien — und einen Unterordner Ressourcen. Das ist der Ort, wo wir die Daten behalten wollen, so erstellen Sie eine neue Datei — Ebenen.json und legen Sie es dort ab.

Für das Tutorial haben wir nur zwei Felder, um jede Ebene zu beschreiben:

  • number — ein int, um eine Ebene zu identifizieren
  • path — der kachelbasierte Pfad, den wir programmgesteuert erstellen möchten. Array von Werten, wobei der erste Wert der Startpunkt und der letzte Wert der Endpunkt ist.

Dies ist die Datei, die ich verwenden werde. Mach dir keine Sorgen über diese Werte in path , wir werden später darauf kommen.

{
"levels":
},
{
"number": 2,
"path":
},
{
"number": 3,
"path":
}
]
}

Erstellen wir einen weiteren Ordner — Skripte — und jetzt beginnt endlich die Codierung.

Wir möchten auf die Daten aus der Datei im Code zugreifen, daher benötigen wir eine Modellklasse dafür. Es ist Zeit, unser allererstes Skript zu erstellen – LevelsData. Es soll nicht von Unity instanziiert werden, daher sollten MonoBehaviour und StartUpdate Methoden entfernt werden. Aus der obigen Datei können wir sehen, dass das Wurzelelement eine array von Ebenen ist, wobei jede Ebene eine int Feldnummer und einen int array Feldpfad haben sollte. Vergessen Sie auch nicht, die Annotation .


public class LevelsData
{
public LevelData levels;
public class LevelData
{
public int number;
public int path;
}
}

Schön, jetzt haben wir die Datei und das Modell. Der nächste Schritt besteht darin, einen in einen anderen zu verwandeln. Lassen Sie uns ein weiteres Skript erstellen — GameZone — und es an das GameZone Objekt in der Szene anhängen. Dieses Skript wird später verwendet, um das gesamte Spielbrett einzurichten.

Folgen Sie dem Single responsibility principle Lassen Sie uns ein weiteres Skript erstellen — LevelsDataLoader — das die gesamte Transformation durchführt. Hängen Sie es auch an das GameZone -Objekt an.

public class LevelsDataLoader : MonoBehaviour
{
private const string LevelsPath = "Levels";
public Dictionary<int, LevelsData.LevelData> ReadLevelsData()
{
var jsonFile = Resources.Load(LevelsPath, typeof(TextAsset)) as TextAsset;
if (jsonFile == null)
{
throw new ApplicationException("Levels file is not accessible");
}
var loadedData = JsonUtility.FromJson<LevelsData>(jsonFile.text);
return loadedData.levels.ToDictionary(level => level.number, level => level);
}
}

Diese Klasse lädt die Daten und gibt sie als Wörterbuch zurück, wobei der Schlüssel die Ebenennummer und die Daten die Ebenendaten selbst sind.

Jetzt sollten wir in GameZone Skript auf die Daten zugreifen können.

public class GameZone : MonoBehaviour
{
private Dictionary<int, LevelsData.LevelData> _levelsData;
private LevelsDataLoader _dataLoader;private void Awake()
{
_dataLoader = GetComponent<LevelsDataLoader>();
}private void Start()
{
_levelsData = _dataLoader.ReadLevelsData();Debug.Log(_levelsData.Count + " levels have been stored in the dictionary!");
}
}

Wechseln Sie zurück zu Unity, drücken Sie die Play—Taste und überprüfen Sie die Konsole – Sie sollten die Meldung „3 Ebenen wurden im Wörterbuch gespeichert!“

Teil 3. Anschließen des Boards und der Daten

Herzlichen Glückwunsch, Sie haben den letzten und interessantesten Teil dieses Tutorials erreicht. Wie werden wir das Board und die Daten tatsächlich verbinden? Lesen Sie weiter, um es herauszufinden!

Lassen Sie uns zunächst horizontal und start_stop Kacheln, die im ersten Teil des Tutorials erstellt wurden, im Ressourcenordner unter dem Kachelordner ablegen. Fügen Sie dann ein neues Skript hinzu — TilesResourcesLoader — eine statische Hilfsklasse, um Kacheln aus diesem Ordner zur Laufzeit zu laden.

public static class TilesResourcesLoader
{
private const string PathHorizontal = "horizontal";
private const string StartStop = "start_stop"; public static Tile GetPathHorizontalTile()
{
return GetTileByName(PathHorizontal);
} public static Tile GetStartStopTile()
{
return GetTileByName(StartStop);
} private static Tile GetTileByName(string name)
{
return (Tile) Resources.Load(name, typeof(Tile));
}
}

Als letzten Schritt sollten wir diese Kacheln beim Start der Szene auf das Brett legen. Kehren wir zum Skript GameZone zurück. Zuerst müssen wir die Levelauswahl simulieren, im realen Spiel passiert es normalerweise, wenn ein Benutzer eine Leveltaste drückt. Fügen wir der Einfachheit halber eine öffentliche Feldebene zu GameZone hinzu und ändern Sie den Wert für start in 1. Ich zeige Ihnen zuerst das endgültige Skript:

public class GameZone : MonoBehaviour
{
public int Level; private const int FieldLineSize = 11;
private const int FieldTotalTiles = FieldLineSize * FieldLineSize;
private Dictionary<int, LevelsData.LevelData> _levelsData; private void Start()
{
_levelsData = GetComponent<LevelsDataLoader>().ReadLevelsData();
SetupTiles();
} private void SetupTiles()
{
var baseLevel = GetComponentsInChildren<Tilemap>(); var localTilesPositions = new List<Vector3Int>(FieldTotalTiles);
foreach (var pos in baseLevel.cellBounds.allPositionsWithin)
{
Vector3Int localPlace = new Vector3Int(pos.x, pos.y, pos.z);
localTilesPositions.Add(localPlace);
} SetupPath(localTilesPositions, baseLevel);
} private void SetupPath(List<Vector3Int> localTilesPositions, Tilemap baseLevel)
{
var path = _levelsData.path;
var pathHorizontalTile = TilesResourcesLoader.GetPathHorizontalTile();
var first = path.First();
var last = path.Last();
foreach (var localPosition in localTilesPositions.GetRange(first, Math.Abs(first - last)))
{
baseLevel.SetTile(localPosition, pathHorizontalTile);
} var startStopTile = TilesResourcesLoader.GetStartStopTile();
baseLevel.SetTile(localTilesPositions, startStopTile);
baseLevel.SetTile(localTilesPositions, startStopTile);
}
}

Wow, das ist eine Menge Action! Lass mich dich durch sie führen.

In der SetupTiles -Methode sollten wir zuerst die Kachelkarte selbst abrufen, da wir die Positionen der Kacheln kennen müssen, um sie ändern zu können. Um dies zu erreichen, verwenden wir die tilemap.cellBounds.allPositionsWithin Methode, die alle Positionen von Kacheln beginnend mit der allerersten zurückgibt — in der gegebenen Konfiguration ist es eine Kachel ganz unten.

Siehe das folgende Bild, wobei jede Zahl den Index in der localTilesPositions Liste darstellt.

Nummerierte Kachelkarte

Erinnern Sie sich an die Werte, die wir im Pfad in Levels.json ? Wie Sie vielleicht schon erraten haben, sind diese Werte die Indizes der Kacheln im Array. Alles, was Sie jetzt tun müssen, ist ein Cheatsheet-Bild, das Ihnen beim Erstellen von Levels hilft. Das ist das, was ich während der Entwicklung verwendet habe:

Meine hässliche nummerierte Kachelkarte

In diesem Beispiel richten wir eine horizontale Linie im Pfad ein, siehe SetupPath Methode. Der Schlüsselteil ist die folgende Schleife:

foreach (var localPosition in localTilesPositions.GetRange(first, Math.Abs(first - last)))
{
baseLevel.SetTile(localPosition, pathHorizontalTile);
}

Hier iterieren wir über localTilesPositions, um diejenigen zu finden, die die gewünschte Kachel einstellen — in diesem Fall horizontal.

Hinweis! GetRange Methode hat zwei Parameter — index und count.

Um die Start- und Endpositionen des Pfades zu markieren, wird die start_stop-Kachel verwendet.

Hier ist das Ergebnis unserer harten Arbeit:

Endergebnis

Versuchen Sie nun, das Ebenennummernfeld des GameZone Skripts von 1 auf 2 oder 3 zu ändern, und Sie werden sehen, dass der Pfad für die angegebene Ebene ordnungsgemäß geladen ist.

Danach