Analysieren und Rendern gekachelter TMX-Karten in Ihrer eigenen Game Engine

In meinem vorherigen Artikel haben wir den Tiled Map Editor als Werkzeug zum Erstellen von Levels für Ihre Spiele betrachtet. In diesem Lernprogramm werde ich Sie durch den nächsten Schritt führen: Analysieren und Rendern dieser Karten in Ihrer Engine.

Hinweis: Obwohl dieses Tutorial mit Flash und AS3 geschrieben wurde, sollten Sie in der Lage sein, in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte zu verwenden.


Bedarf

  • Gekachelte Version 0.8.1: http://www.mapeditor.org/
  • TMX-Karte und Tileset von hier aus. Wenn Sie meinem Tutorial Einführung in Kacheln gefolgt sind, sollten Sie diese bereits haben.

Speichern im XML-Format

Mit der TMX-Spezifikation können wir die Daten auf verschiedene Arten speichern. Für dieses Tutorial speichern wir unsere Karte im XML-Format. Wenn Sie die im Anforderungsabschnitt enthaltene TMX-Datei verwenden möchten, können Sie mit dem nächsten Abschnitt fortfahren.

Wenn Sie Ihre eigene Karte erstellt haben, müssen Sie Tiled mitteilen, dass diese als XML gespeichert werden soll. Öffnen Sie dazu Ihre Karte mit Tiled und wählen Sie Bearbeiten> Voreinstellungen….

Wählen Sie für das Dropdown-Feld "Kachelebenendaten speichern als:" XML aus, wie in der folgenden Abbildung dargestellt:

Wenn Sie nun die Karte speichern, wird sie im XML-Format gespeichert. Sie können die TMX-Datei mit einem Texteditor öffnen, um einen Blick darauf zu werfen. Hier ist ein Ausschnitt aus dem, was Sie erwarten können:

                      

Wie Sie sehen, speichert es einfach alle Karteninformationen in diesem praktischen XML-Format. Die Eigenschaften sollten größtenteils unkompliziert sein, mit Ausnahme von gid - Auf eine ausführlichere Erklärung hierzu werde ich später im Tutorial eingehen.

Bevor wir fortfahren, möchte ich Ihre Aufmerksamkeit auf die richten Objektgruppe "Kollision"element. Wie Sie sich vielleicht an das Lernprogramm zur Kartenerstellung erinnern, haben wir den Kollisionsbereich um den Baum festgelegt; so wird er gespeichert.

Sie können auf dieselbe Weise Power-Ups oder Spieler-Spawn-Punkte angeben. So können Sie sich vorstellen, wie viele Möglichkeiten Tiled als Karteneditor bietet!


Kernumriss

Hier ein kurzer Überblick darüber, wie wir unsere Karte ins Spiel bringen werden:

  1. Lesen Sie die TMX-Datei ein.
  2. Analysieren Sie die TMX-Datei als XML-Datei.
  3. Laden Sie alle Tileset-Bilder.
  4. Ordnen Sie die Kachelsätze Layer für Layer in unserem Kartenlayout an.
  5. Kartenobjekt lesen.

Einlesen der TMX-Datei

Was Ihr Programm angeht, ist dies nur eine XML-Datei. Deshalb möchten wir es als Erstes einlesen. Die meisten Sprachen verfügen über eine XML-Bibliothek. Im Fall von AS3 verwende ich die XML-Klasse zum Speichern der XML-Informationen und einen URLLoader zum Lesen der TMX-Datei.

 xmlLoader = neuer URLLoader (); xmlLoader.addEventListener (Event.COMPLETE, xmlLoadComplete); xmlLoader.load (neue URLRequest ("… /assets/example.tmx"));

Dies ist ein einfacher Datei-Reader für "… /Assets/example.tmx". Es wird davon ausgegangen, dass sich die TMX-Datei in Ihrem Projektverzeichnis im Ordner "Assets" befindet. Wir brauchen nur eine Funktion, um zu arbeiten, wenn das Lesen der Datei abgeschlossen ist:

 private Funktion xmlLoadComplete (e: Event): void xml = neues XML (e.target.data); mapWidth = xml.attribute ("width"); mapHeight = xml.attribute ("height"); tileWidth = xml.attribute ("tilewidth"); tileHeight = xml.attribute ("tileheight"); var xmlCounter: uint = 0; (var tileet: XML in xml.tileset) var imageWidth: uint = xml.tileset.image.attribute ("width") [xmlCounter]; var imageHeight: uint = xml.tileset.image.attribute ("height") [xmlCounter]; var firstGid: uint = xml.tileset.attribute ("firstgid") [xmlCounter]; var tileetName: String = xml.tileset.attribute ("name") [xmlCounter]; var tilesetTileWidth: uint = xml.tileset.attribute ("tilewidth") [xmlCounter]; var tilesetTileHeight: uint = xml.tileset.attribute ("tileheight") [xmlCounter]; var tilesetImagePath: String = xml.tileset.image.attribute ("source") [xmlCounter]; tileSets.push (new TileSet (firstGid, tileetName, tileetTileWidth, tileetTileHeight, tileetImagePath, imageWidth, imageHeight)); xmlCounter ++;  totalTileSets = xmlCounter; 

Hier findet die erste Analyse statt. (Es gibt einige Variablen, die wir außerhalb dieser Funktion nachverfolgen werden, da wir sie später verwenden werden.)

Sobald die Kartendaten gespeichert sind, wird jedes Kachelset analysiert. Ich habe eine Klasse erstellt, um die Informationen jedes Tilesets zu speichern. Wir werden jede dieser Klasseninstanzen in ein Array verschieben, da wir sie später verwenden werden:

 öffentliche Klasse TileSet public var firstgid: uint; public var lastgid: uint; öffentlicher Variablenname: String; public var tileWidth: uint; public var source: String; public var tileHöhe: uint; public var imageWidth: uint; public var imageHöhe: uint; public var bitmapData: BitmapData; public var tileAmountWidth: uint; public function TileSet (firstgid, name, tileWidth, tileHeight, source, imageWidth, imageHeight) this.firstgid = firstgid; this.name = name; this.tileWidth = tileWidth; this.tileHeight = tileHeight; this.source = source; this.imageWidth = imageWidth; this.imageHeight = imageHeight; tileAmountWidth = Math.floor (imageWidth / tileWidth); lastgid = tileAmountWidth * Math.floor (imageHeight / tileHeight) + firstgid - 1; 

Das können Sie wieder sehen gid erscheint wieder in der firstgid und lastgid Variablen. Schauen wir uns jetzt an, wozu das dient.


Verstehen "gid"

Für jede Kachel müssen wir sie irgendwie mit einem Kachelsatz und einer bestimmten Position auf diesem Kachelsatz verknüpfen. Dies ist der Zweck des gid.

Schaue auf die Grasfliesen-2-small.png Tileset. Es enthält 72 verschiedene Kacheln:

Wir geben jeder dieser Kacheln ein Unikat gid von 1-72, so dass wir uns auf eine beliebige Nummer beziehen können. Das TMX-Format gibt jedoch nur das erste an gid von dem Tileset, da alle anderen gids kann aus der Kenntnis der Größe der Kacheln und der Größe jeder einzelnen Kachel abgeleitet werden.

Hier ist ein praktisches Bild, um den Prozess zu visualisieren und zu erklären.

Wenn wir also das untere rechte Feld dieses Plättchens irgendwo auf einer Karte platzieren, speichern wir das gid 72 an diesem Ort auf der Karte.

Nun, in der obigen TMX-Datei werden Sie das bemerken tree2-final.png hat ein firstgid von 73. Das liegt daran, dass wir weiterhin auf die gids, und wir setzen es nicht für jedes Tileset auf 1 zurück.

Zusammenfassend ist a gid ist eine eindeutige ID, die jeder Kachel jedes Kachelsatzes in einer TMX-Datei zugewiesen wird, basierend auf der Position der Kachel innerhalb des Kachelsets und der Anzahl der Kachelsätze, auf die in der TMX-Datei verwiesen wird.


Laden der Tilesets

Jetzt möchten wir alle Kachelsatz-Quellbilder in den Speicher laden, damit wir unsere Karte zusammenstellen können. Wenn Sie dies nicht in AS3 schreiben, müssen Sie nur wissen, dass wir die Bilder für jedes Kachelset hier laden:

 // Bilder für Tileset laden für (var i = 0; i < totalTileSets; i++)  var loader = new TileCodeEventLoader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, tilesLoadComplete); loader.contentLoaderInfo.addEventListener(ProgressEvent.PROGRESS, progressHandler); loader.tileSet = tileSets[i]; loader.load(new URLRequest("… /assets/" + tileSets[i].source)); eventLoaders.push(loader); 

Hier gibt es ein paar AS3-spezifische Dinge, z. B. die Verwendung der Loader-Klasse, um die Tileset-Bilder einzubringen. (Genauer gesagt, es ist eine erweiterte Lader, Einfach so, dass wir das speichern können TileSet Instanzen in jedem Lader. Dies ist so, dass, wenn der Lader fertig ist, der Lader leicht mit dem Kachelsatz korreliert werden kann.)

Das hört sich vielleicht kompliziert an, aber der Code ist sehr einfach:

 public class TileCodeEventLoader erweitert den Loader public var tileSet: TileSet; 

Bevor wir nun mit diesen Kacheln beginnen und die Karte mit ihnen erstellen, müssen wir ein Basisbild erstellen, um sie zu erstellen:

 screenBitmap = new Bitmap (neue BitmapData (mapWidth * tileWidth, mapHeight * tileHeight, false, 0x22ffff)); screenBitmapTopLayer = neue Bitmap (neue BitmapData (mapWidth * tileWidth, mapHeight * tileHeight, true, 0));

Wir werden die Kacheldaten auf diese Bitmap-Bilder kopieren, damit wir sie als Hintergrund verwenden können. Der Grund für das Einrichten von zwei Bildern besteht darin, dass wir eine obere und eine untere Ebene haben können und der Spieler sich zwischen ihnen bewegen kann, um Perspektive zu bieten. Wir legen auch fest, dass die oberste Ebene einen Alphakanal haben sollte.

Für die eigentlichen Ereignis-Listener für die Loader können wir diesen Code verwenden:

 private Funktion progressHandler (Ereignis: ProgressEvent): void trace ("progressHandler: bytesLoaded =" + event.bytesLoaded + "bytesTotal =" + event.bytesTotal); 

Dies ist eine unterhaltsame Funktion, da Sie verfolgen können, wie weit das Bild geladen wurde, und dem Benutzer daher Rückmeldung geben können, wie schnell die Dinge ablaufen, z. B. eine Fortschrittsleiste.

 private Funktion tileLoadComplete (e: Event): void var currentTileset = e.target.loader.tileSet; currentTileset.bitmapData = Bitmap (e.target.content) .bitmapData; tileSetsLoaded ++; // Warten Sie, bis alle Tileset-Bilder geladen sind, bevor Sie sie Layer für Layer in einer Bitmap kombinieren, wenn (tileSetsLoaded == totalTileSets) addTileBitmapData (); 

Hier speichern wir die Bitmap-Daten mit dem zugehörigen Tileset. Wir zählen auch, wie viele Kachelsets vollständig geladen wurden, und wenn sie fertig sind, können wir eine Funktion aufrufen (ich habe sie benannt) addTileBitmapData in diesem Fall), um die Fliesenteile zusammenzusetzen.


Fliesen kombinieren

Um die Kacheln zu einem einzigen Bild zusammenzufassen, möchten wir sie Layer für Layer aufbauen, sodass sie auf dieselbe Weise angezeigt werden, wie das Vorschaufenster in Tiled angezeigt wird.

So sieht die endgültige Funktion aus: Die Kommentare, die ich in den Quellcode aufgenommen habe, sollten ausreichend erklären, was vor sich geht, ohne in die Details zu geraten. Ich sollte beachten, dass dies auf viele verschiedene Arten implementiert werden kann und dass Ihre Implementierung völlig anders aussehen kann als meine.

 private Funktion addTileBitmapData (): void // lädt jede Ebene für jede (var-Ebene: XML in xml.layer) var tile: Array = new Array (); var tileLength: uint = 0; // weise jedem Ort in der Ebene die GID zu (var tile: XML in layer.data.tile) var gid: Number = tile.attribute ("gid"); // wenn gid> 0 if (gid> 0) tile [tileLength] = gid;  tileLength ++;  // Die äußere for-Schleife wird in den nächsten Ausschnitten fortgesetzt

Was hier passiert ist, dass wir nur die Kacheln mit analysieren gids über 0, da 0 eine leere Kachel angibt und diese in einem Array speichert. Da es so viele "0 Kacheln" in unserer obersten Schicht gibt, wäre es ineffizient, alle im Speicher abzulegen. Es ist wichtig zu wissen, dass wir den Speicherort der gid mit einem Zähler, da wir den Index später im Array verwenden werden.

 var useBitmap: BitmapData; var layerName: String = layer.attribute ("name") [0]; // Entscheide wo wir die Ebene var setzen layerMap: int = 0; switch (layerName) case "Top": layerMap = 1; brechen; default: trace ("using base layer"); 

In diesem Abschnitt analysieren wir den Ebenennamen und prüfen, ob er gleich "Oben" ist. Wenn dies der Fall ist, setzen wir ein Flag, damit wir es auf die oberste Bitmap-Ebene kopieren können. Mit solchen Funktionen können wir sehr flexibel sein und noch mehr Ebenen in beliebiger Reihenfolge verwenden.

 // speichere die gid in einem 2d Array var tileCoordinates: Array = new Array (); für (var tileX: int = 0; tileX < mapWidth; tileX++)  tileCoordinates[tileX] = new Array(); for (var tileY:int = 0; tileY < mapHeight; tileY++)  tileCoordinates[tileX][tileY] = tiles[(tileX+(tileY*mapWidth))];  

Jetzt speichern wir die gid, die wir am Anfang in ein 2D-Array geparst haben. Sie werden die Initialisierung des Double Arrays feststellen. Dies ist einfach eine Möglichkeit, mit 2D-Arrays in AS3 umzugehen.

Es gibt auch ein bisschen Mathe. Denken Sie daran, wann wir das initialisiert haben Fliesen Array von oben und wie haben wir den Index dabei behalten? Wir werden jetzt den Index verwenden, um die Koordinate zu berechnen, die der gid gehört. Dieses Bild zeigt, was los ist:

Also für dieses Beispiel bekommen wir die gid bei Index 27 in der Fliesen Array und speichern Sie es unter tileCoordinates [7] [1]. Perfekt!

 for (var spriteForX: int = 0; spriteForX) < mapWidth; spriteForX++)  for (var spriteForY:int = 0; spriteForY < mapHeight; spriteForY++)  var tileGid:int = int(tileCoordinates[spriteForX][spriteForY]); var currentTileset:TileSet; // only use tiles from this tileset (we get the source image from here) for each( var tileset1:TileSet in tileSets)  if (tileGid >= tileet1.firstgid-1 && tileGid // Wir haben das richtige Tileset für diese Gid gefunden! currentTileset = tileet1; brechen;  var destY: int = spriteForY * tileWidth; var destX: int = spriteForX * tileWidth; // Grundlegende Mathematik, um herauszufinden, woher die Kachel auf dem Quellbild kommt tileGid - = currentTileset.firstgid -1; var sourceY: int = Math.ceil (tileGid / currentTileset.tileAmountWidth) -1; var sourceX: int = tileGid - (currentTileset.tileAmountWidth * sourceY) - 1; // Die Kachel aus dem Tile-Set in unser Bitmap kopieren, wenn (layerMap == 0) screenBitmap.bitmapData.copyPixels (currentTileset.bitmapData, neues Rechteck (sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWidth, currentTileset.tileWidth, currentTileset.tileWidth. tileHeight), neuer Punkt (destX, destY), null, null, wahr);  else if (layerMap == 1) screenBitmapTopLayer.bitmapData.copyPixels (currentTileset.bitmapData, neues Rechteck (sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWile,) ), null, null, wahr); 

Hier können wir endlich das Kachelset in unsere Karte kopieren.

Anfangs durchlaufen wir jede Kachelkoordinate auf der Karte, und für jede Kachelkoordinate erhalten wir die gid Überprüfen Sie, ob das gespeicherte Kachelset mit dem übereinstimmt, indem Sie prüfen, ob es zwischen den beiden liegt firstgid und unsere kalkuliert lastgid.

Wenn du das verstanden hast Verstehen "gid" Abschnitt von oben sollte diese Mathematik sinnvoll sein. Im Grunde genommen nimmt man die Kachelkoordinate auf dem Kachelsatz (sourceX und sourceY) und kopiere es auf unsere Karte an der Kachelposition, zu der wir geloopt sind (destX und destY).

Zum Schluss nennen wir das copyPixel Funktion zum Kopieren des Kachelbildes auf die oberste oder Basisebene.


Objekte hinzufügen

Nachdem nun die Ebenen auf die Karte kopiert wurden, schauen wir uns an, wie die Kollisionsobjekte geladen werden. Dies ist sehr leistungsfähig, da es nicht nur für Kollisionsobjekte verwendet werden kann, sondern auch für jedes andere Objekt, z. B. ein Power-Up oder einen Spieler-Spawn-Standort, vorausgesetzt, wir haben es mit Tiled angegeben.

Also am Ende der addTileBitmapData Funktion, geben wir den folgenden Code ein:

 für jedes (var objectgroup: XML in xml.objectgroup) var objectGroup: String = objectgroup.attribute ("name"); switch (objectGroup) case "Collision": für jedes (var-Objekt: XML in objectgroup.object) var-Rechteck: Shape = new Shape (); Rechteckgrafiken.beginFill (0x0099CC, 1); rechteck.graphics.drawRect (0, 0, object.attribute ("width"), object.attribute ("height")); rechteck.graphics.endFill (); rechteckle.x = object.attribute ("x"); rechteck.y = object.attribute ("y"); collisionTiles.push (Rechteck); addChild (Rechteck);  brechen; default: trace ("unbekannter Objekttyp:", objectgroup.attribute ("name")); 

Dies durchläuft die Objekt-Layer und sucht nach dem Layer mit dem Namen "KollisionWenn er es findet, nimmt er jedes Objekt in dieser Ebene, erstellt an dieser Position ein Rechteck und speichert es in der collisionTiles Array. Auf diese Weise haben wir immer noch einen Verweis darauf, und wir können durchlaufen, um nach Kollisionen zu suchen, wenn wir einen Spieler hätten.

(Je nachdem, wie Ihr System mit Kollisionen umgeht, möchten Sie möglicherweise etwas anderes tun.)


Anzeigen der Karte

Um die Karte anzuzeigen, möchten wir zum Schluss zuerst den Hintergrund und dann den Vordergrund rendern, um die Schichtung korrekt zu gestalten. In anderen Sprachen handelt es sich lediglich um das Rendern des Bildes.

 // Hintergrundebene laden addChild (screenBitmap); // Rechteck, nur um zu zeigen, wie etwas zwischen Ebenen aussehen würde var playerExample: Shape = new Shape (); playerExample.graphics.beginFill (0x0099CC, 1); playerExample.graphics.lineStyle (2); // Gliederung Rechteck playerExample.graphics.drawRect (0, 0, 100, 100); playerExample.graphics.endFill (); playerExample.x = 420; playerExample.y = 260; collisionTiles.push (playerExample); addChild (playerExample); // Lade die oberste Ebene addChild (screenBitmapTopLayer);

Ich habe hier zwischen den Layern etwas Code eingefügt, um mit einem Rechteck zu zeigen, dass das Layering tatsächlich funktioniert. Hier ist das Endergebnis:

Vielen Dank, dass Sie sich die Zeit genommen haben, um das Tutorial abzuschließen. Ich habe eine ZIP-Datei mit einem vollständigen FlashDevelop-Projekt mit sämtlichem Quellcode und allen Ressourcen hinzugefügt.


Zusätzliche Lesung

Wenn Sie daran interessiert sind, mehr mit Tiled zu tun, war eines der Dinge, die ich nicht behandelt habe Eigenschaften. Die Verwendung von Eigenschaften ist ein kleiner Sprung vom Analysieren der Ebenennamen und ermöglicht Ihnen das Festlegen einer großen Anzahl von Optionen. Wenn Sie zum Beispiel einen Feind-Spawn-Punkt wünschen, können Sie die Art des Feindes, die Größe, die Farbe und alles im Tiled-Map-Editor angeben!

Wie Sie vielleicht bemerkt haben, ist XML nicht das effizienteste Format zum Speichern der TMX-Daten. CSV ist ein gutes Medium zwischen einfacher Analyse und besserer Speicherung, es gibt jedoch auch base64 (unkomprimiert, Zlib-komprimiert und GZIP-komprimiert). Wenn Sie diese Formate anstelle von XML verwenden möchten, besuchen Sie die Seite der gekachelten Wikis im TMX-Format.