Analisi e rendering di mappe in formato TMX affiancate nel tuo motore di gioco

Nel mio precedente articolo, abbiamo esaminato Tiled Map Editor come uno strumento per creare livelli per i tuoi giochi. In questo tutorial ti illustrerò il passaggio successivo: analizzando e visualizzando tali mappe nel tuo motore.

Nota: Sebbene questo tutorial sia stato scritto usando Flash e AS3, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.


Requisiti

  • Versione piastrellata 0.8.1: http://www.mapeditor.org/
  • Mappa TMX e set di tessere da qui. Se hai seguito il tutorial Introduzione a Tiled dovresti già averlo.

Salvataggio in formato XML

Utilizzando le specifiche TMX possiamo memorizzare i dati in vari modi. Per questo tutorial, salveremo la nostra mappa nel formato XML. Se si prevede di utilizzare il file TMX incluso nella sezione dei requisiti, è possibile passare alla sezione successiva.

Se hai creato la tua mappa, dovrai dire a Tiled di salvarlo come XML. Per fare ciò, apri la mappa con Tiled e seleziona Modifica> Preferenze ...

Per la casella a discesa "Memorizza i dati del livello tile come:", seleziona XML, come mostrato nell'immagine seguente:

Ora, quando salvi la mappa, questa verrà archiviata in formato XML. Sentiti libero di aprire il file TMX con un editor di testo per dare un'occhiata all'interno. Ecco un frammento di ciò che puoi aspettarti di trovare:

            ...     ...       

Come puoi vedere, memorizza semplicemente tutte le informazioni della mappa in questo pratico formato XML. Le proprietà dovrebbero essere per lo più semplici, ad eccezione di gid - Entrerò in una spiegazione più approfondita di questo più avanti nel tutorial.

Prima di andare avanti, vorrei indirizzare la tua attenzione su objectgroup "Collisione"Come puoi ricordare dal tutorial di creazione della mappa, abbiamo specificato l'area di collisione attorno all'albero, questo è il modo in cui è memorizzato.

Puoi specificare i power-up o il punto di spawn del giocatore nello stesso modo, quindi puoi immaginare quante possibilità ci sono per Tiled come editor di mappe!


Profilo principale

Ora ecco una breve carrellata su come avremo la nostra mappa nel gioco:

  1. Leggi nel file TMX.
  2. Analizzare il file TMX come un file XML.
  3. Carica tutte le immagini del tileset.
  4. Disporre le immagini dei tileset nel layout della mappa, strato per strato.
  5. Leggi l'oggetto mappa.

Lettura nel file TMX

Per quanto riguarda il tuo programma, questo è solo un file XML, quindi la prima cosa che vogliamo fare è leggerla. La maggior parte delle lingue ha una libreria XML per questo; nel caso di AS3 userò la classe XML per memorizzare le informazioni XML e un URLLoader per leggere nel file TMX.

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

Questo è un semplice lettore di file per "... /assets/example.tmx". Presume che il file TMX si trovi nella directory del progetto nella cartella "assets". Abbiamo solo bisogno di una funzione per gestire quando il file letto è completo:

 funzione privata xmlLoadComplete (e: Event): void xml = new XML (e.target.data); mapWidth = xml.attribute ("width"); mapHeight = xml.attribute ("height"); tileWidth = xml.attribute ("tilewidth"); tileHeight = xml.attribute ("tileheight"); var xmlCounter: uint = 0; per ogni (set di caratteri: 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 tilesetName: 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, tilesetName, tilesetTileWidth, tilesetTileHeight, tilesetImagePath, imageWidth, imageHeight)); xmlCounter ++;  totalTileSets = xmlCounter; 

È qui che si svolge l'analisi iniziale. (Ci sono alcune variabili che terremo al di fuori di questa funzione poiché le useremo in seguito).

Una volta memorizzati i dati della mappa, passiamo all'analisi di ogni tileset. Ho creato una classe per memorizzare le informazioni di ogni tileset. Spingeremo ciascuna di queste istanze di classe in un array poiché le utilizzeremo in un secondo momento:

 public class TileSet public var firstgid: uint; public var lastgid: uint; nome var pubblico: String; public var tileWidth: uint; public var source: String; public var tileHeight: uint; public var imageWidth: uint; public var imageHeight: uint; public var bitmapData: BitmapData; public var tileAmountWidth: uint; funzione pubblica TileSet (firstgid, name, tileWidth, tileHeight, source, imageWidth, imageHeight) this.firstgid = firstgid; this.name = nome; 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; 

Di nuovo, puoi vederlo gid appare di nuovo, nel firstgid e lastgid variabili. Vediamo ora a cosa serve.


Comprensione "gid"

Per ogni tessera, dobbiamo in qualche modo associarlo con un tileset e una posizione particolare su quel tileset. Questo è lo scopo del gid.

Guarda al grass-tessere-2-small.png tileset. Contiene 72 tessere distinte:

Diamo a ciascuna di queste piastrelle un aspetto unico gid da 1 a 72, in modo che possiamo riferirci a chiunque abbia un solo numero. Tuttavia, il formato TMX specifica solo il primo gid del tileset, poiché tutti gli altri gids può essere derivato dal conoscere la dimensione del tileset e la dimensione di ogni singola tessera.

Ecco un'immagine utile per aiutare a visualizzare e spiegare il processo.

Quindi se avessimo posizionato la tessera in basso a destra di questo tileset su una mappa da qualche parte, avremmo memorizzato il gid 72 in quella posizione sulla mappa.

Ora, nel file TMX di esempio sopra, lo noterai tree2-final.png ha un firstgid di 73. Questo perché continuiamo a contare su gids, e non lo resettiamo a 1 per ogni set di tessere.

In breve, a gid è un ID univoco assegnato a ciascuna tessera di ogni set di tessere all'interno di un file TMX, in base alla posizione della tessera all'interno del set di tessere e al numero di set di tessere a cui si fa riferimento nel file TMX.


Caricamento dei riquadri

Ora vogliamo caricare tutte le immagini sorgente dei tileset in memoria in modo da poter mettere insieme la nostra mappa. Se non stai scrivendo questo in AS3, l'unica cosa che devi sapere è che stiamo caricando le immagini per ogni tileset qui:

 // carica le immagini per tileset per (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); 

Ci sono alcune cose specifiche per AS3 in corso qui, come usare la classe Loader per inserire le immagini del tileset. (Più specificamente, è un esteso caricatore, semplicemente così possiamo memorizzare il Set di riquadri istanze all'interno di ciascuna caricatore. In questo modo, quando il caricatore termina, è possibile correlare facilmente il Loader con il tileset.)

Questo può sembrare complicato ma il codice è davvero piuttosto semplice:

 public class TileCodeEventLoader estende Loader public var tileSet: TileSet; 

Ora, prima di iniziare a prendere questi tileset e creare la mappa con loro, dobbiamo creare un'immagine di base per metterli su:

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

Copieremo i dati delle tessere su queste immagini bitmap in modo che possiamo utilizzarle come sfondo. La ragione per cui ho impostato due immagini è che possiamo avere uno strato superiore e uno strato inferiore, e fare in modo che il giocatore si sposti tra loro per fornire una prospettiva. Specifichiamo anche che il livello superiore dovrebbe avere un canale alfa.

Per gli ascoltatori di eventi effettivi per i caricatori possiamo usare questo codice:

 funzione privata progressHandler (event: ProgressEvent): void trace ("progressHandler: bytesLoaded =" + event.bytesLoaded + "bytesTotal =" + event.bytesTotal); 

Questa è una funzione divertente dal momento che è possibile tenere traccia di quanto è stata caricata l'immagine e quindi fornire all'utente un riscontro sulla rapidità delle cose, come una barra di avanzamento.

 tile funzione privataLoadComplete (e: Event): void var currentTileset = e.target.loader.tileSet; currentTileset.bitmapData = Bitmap (e.target.content) .bitmapData; tileSetsLoaded ++; // aspetta che tutte le immagini del tileset siano caricate prima di unirle strato per strato in una bitmap se (tileSetsLoaded == totalTileSets) addTileBitmapData (); 

Qui memorizziamo i dati bitmap con il tileset ad esso associato. Contiamo anche quanti tileset sono stati caricati completamente, e quando sono tutti fatti, possiamo chiamare una funzione (l'ho chiamata così) addTileBitmapData in questo caso) per iniziare a mettere insieme i pezzi delle piastrelle.


Combinare le piastrelle

Per combinare le tessere in una singola immagine, vogliamo costruirla strato per strato in modo che venga visualizzata nello stesso modo in cui appare la finestra di anteprima in Tiled.

Ecco come sarà la funzione finale; i commenti che ho incluso nel codice sorgente dovrebbero spiegare adeguatamente cosa sta succedendo senza entrare troppo nei dettagli. Devo notare che questo può essere implementato in molti modi diversi e la tua implementazione può sembrare completamente diversa dalla mia.

 funzione privata addTileBitmapData (): void // carica ogni livello per ciascuno (livello var: XML in xml.layer) var tiles: Array = new Array (); var tileLength: uint = 0; // assegna il gid a ogni posizione nel layer per ogni (var tile: XML in layer.data.tile) var gid: Number = tile.attribute ("gid"); // if gid> 0 if (gid> 0) tiles [tileLength] = gid;  tileLength ++;  // outer for loop continua nei snippet successivi

Quello che sta succedendo qui è che stiamo analizzando solo le tessere con gids che sono sopra lo 0, poiché 0 indica un riquadro vuoto e li memorizza in un array. Poiché ci sono così tante "0 tessere" nel nostro strato superiore, sarebbe inefficiente memorizzarle tutte in memoria. È importante notare che stiamo memorizzando la posizione di gid con un contatore perché useremo il suo indice nell'array in seguito.

 var useBitmap: BitmapData; var layerName: String = layer.attribute ("nome") [0]; // decidi dove mettere il livello var layerMap: int = 0; switch (layerName) case "Top": layerMap = 1; rompere; default: trace ("using base layer"); 

In questa sezione stiamo analizzando il nome del livello e controllando se è uguale a "Superiore". Se lo è, impostiamo un flag in modo che sappiamo copiarlo sul livello bitmap superiore. Possiamo essere davvero flessibili con funzioni come questa, e usare ancora più livelli disposti in qualsiasi ordine.

 // memorizza il gid in un array 2d var tileCoordinates: Array = new Array (); per (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))];  

Ora qui stiamo memorizzando il gid, che abbiamo analizzato all'inizio, in un array 2D. Noterai le inizializzazioni del doppio array; questo è semplicemente un modo di gestire gli array 2D in AS3.

C'è anche un po 'di matematica. Ricorda quando inizializzammo il piastrelle array dall'alto e come abbiamo mantenuto l'indice con esso? Ora useremo l'indice per calcolare la coordinata che il gid appartiene a. Questa immagine mostra cosa sta succedendo:

Quindi per questo esempio, otteniamo il gid all'indice 27 nel piastrelle array e memorizzarlo su tileCoordinates [7] [1]. Perfezionare!

 per (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 >= tileset1.firstgid-1 && tileGid // abbiamo trovato il set di tessere giusto per questo gid! currentTileset = tileset1; rompere;  var destY: int = spriteForY * tileWidth; var destX: int = spriteForX * tileWidth; // matematica di base per scoprire da dove proviene la piastrella sull'immagine tileGid - = currentTileset.firstgid -1; var sourceY: int = Math.ceil (tileGid / currentTileset.tileAmountWidth) -1; var sourceX: int = tileGid - (currentTileset.tileAmountWidth * sourceY) - 1; // copia la tessera dal tileset sulla nostra bitmap if (layerMap == 0) screenBitmap.bitmapData.copyPixels (currentTileset.bitmapData, new Rectangle (sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWidth, currentTileset. tileHeight), new Point (destX, destY), null, null, true);  else if (layerMap == 1) screenBitmapTopLayer.bitmapData.copyPixels (currentTileset.bitmapData, new Rectangle (sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWidth, currentTileset.tileHeight), new Point (destX, destY) ), null, null, true); 

È qui che finalmente arriviamo a copiare il tileset sulla nostra mappa.

Inizialmente iniziamo eseguendo il looping di ogni coordinata di tile sulla mappa, e per ogni coordinata di tile otteniamo il gid e verificare il set di tessere memorizzato che lo corrisponda, controllando se si trova tra il firstgid e il nostro calcolato lastgid.

Se hai capito il Comprensione "gid" sezione dall'alto, questa matematica dovrebbe avere senso. Nei termini più elementari, sta prendendo la coordinata della tessera sul set di tessere (sourceX e sourceY) e copiandolo sulla nostra mappa nella posizione della tessera su cui siamo collegati (destX e destY).

Alla fine, alla fine chiamiamo il copyPixel funzione per copiare l'immagine della tessera sul livello superiore o di base.


Aggiungere oggetti

Ora che la copia dei livelli sulla mappa è terminata, esaminiamo il caricamento degli oggetti di collisione. Questo è molto potente perché, oltre a usarlo per gli oggetti di collisione, possiamo anche usarlo per qualsiasi altro oggetto, come un power-up o un punto di spawn del giocatore, purché lo abbiamo specificato con Tiled.

Quindi in fondo al addTileBitmapData funzione, inseriamo il seguente codice:

 per ogni (var objectgroup: XML in xml.objectgroup) var objectGroup: String = objectgroup.attribute ("name"); switch (objectGroup) case "Collision": per ciascuna (oggetto var: XML in objectgroup.object) var rectangle: Shape = new Shape (); rectangle.graphics.beginFill (0x0099CC, 1); rectangle.graphics.drawRect (0, 0, object.attribute ("width"), object.attribute ("height")); rectangle.graphics.endFill (); rectangle.x = object.attribute ("x"); rectangle.y = object.attribute ("y"); collisionTiles.push (rettangolo); addChild (rettangolo);  rompere; default: trace ("tipo di oggetto non riconosciuto:", objectgroup.attribute ("nome")); 

Questo passerà in rassegna i livelli dell'oggetto e cercherà il livello con il nome "CollisioneQuando lo trova, prende ogni oggetto in quel livello, crea un rettangolo in quella posizione e lo memorizza nel collisionTiles array. In questo modo abbiamo ancora un riferimento ad esso, e possiamo scorrere per controllarlo per le collisioni se avessimo un giocatore.

(A seconda di come il tuo sistema gestisce le collisioni, potresti voler fare qualcosa di diverso).


Visualizzazione della mappa

Infine, per visualizzare la mappa, vogliamo prima rendere lo sfondo e poi il primo piano, in modo da ottenere il layering corretto. In altre lingue, si tratta semplicemente di rendere l'immagine.

 // carica il livello di sfondo addChild (screenBitmap); // rettangolo solo per dimostrare come qualcosa apparirebbe tra i vari livelli var playerExample: Shape = new Shape (); playerExample.graphics.beginFill (0x0099CC, 1); playerExample.graphics.lineStyle (2); // outline rectangle playerExample.graphics.drawRect (0, 0, 100, 100); playerExample.graphics.endFill (); playerExample.x = 420; playerExample.y = 260; collisionTiles.push (playerExample); addChild (playerExample); // carica top layer addChild (screenBitmapTopLayer);

Ho aggiunto un po 'di codice tra gli strati qui solo per dimostrare con un rettangolo che il layering funziona davvero. Ecco il risultato finale:

Grazie per aver dedicato del tempo per completare il tutorial. Ho incluso un file zip contenente un progetto FlashDevelop completo con tutto il codice sorgente e le risorse.


Letture aggiuntive

Se sei interessato a fare più cose con Tiled, una cosa che non ho coperto è stata proprietà. L'uso delle proprietà è un piccolo salto dall'analisi dei nomi dei livelli e consente di impostare un numero elevato di opzioni. Ad esempio, se si desidera un punto di spawn nemico, è possibile specificare il tipo di nemico, la dimensione, il colore e tutto, dall'interno dell'editor di mappe Tiled!

Infine, come avrai notato, XML non è il formato più efficiente per archiviare i dati TMX. CSV è un bel supporto tra l'analisi semplice e l'archiviazione migliore, ma c'è anche base64 (non compresso, compresso con zlib e compresso con gzip). Se sei interessato a utilizzare questi formati anziché XML, controlla la pagina del wiki di Tiled sul formato TMX.