Parsning och återgivning av klädda TMX-formatkartor i din egen spelmotor

I min tidigare artikel såg vi på Tiled Map Editor som ett verktyg för att skapa nivåer för dina spel. I den här handledningen tar jag dig igenom nästa steg: analysera och återge dessa kartor i din motor.

Notera: Även om denna handledning skrivs med Flash och AS3, borde du kunna använda samma tekniker och begrepp i nästan vilken spelutvecklingsmiljö som helst.


Krav

  • Sida vid sida version 0.8.1: http://www.mapeditor.org/
  • TMX karta och kakel från härifrån. Om du följde min introduktion till kaklade handledning bör du redan ha dessa.

Spara i XML-format

Med TMX-specifikationen kan vi lagra data på olika sätt. För den här handledningen sparar vi vår karta i XML-formatet. Om du planerar att använda TMX-filen som ingår i kraven kan du gå vidare till nästa avsnitt.

Om du skapade din egen karta måste du meddela Tiled att spara den som XML. För att göra detta, öppna din karta med sida vid sida och välj Redigera> Inställningar ...

För rullgardinsrutan "Spara kakel lagerdata som:" väljer du XML, som visas i bilden nedan:

Nu när du sparar kartan sparas den i XML-format. Ta gärna upp TMX-filen med en textredigerare för att ta en titt inuti. Här är ett utdrag av vad du kan förvänta dig att hitta:

            ...     ...       

Som du kan se lagras det helt enkelt alla kartinformation i det här praktiska XML-formatet. Egenskaperna ska mestadels vara enkla, med undantag för gid - Jag kommer att gå in i en fördjupad förklaring av detta senare i handledningen.

Innan vi går vidare vill jag rikta er uppmärksamhet åt objectgroup "Kollision"-elementet. Som du kanske kommer ihåg från kartläggningshandledningen angav vi kollisionsområdet kring trädet, så här lagras det.

Du kan ange power-ups eller spelaren spawn punkt på samma sätt, så du kan föreställa dig hur många möjligheter det finns för sida vid sida som en kartredigerare!


Core Outline

Nu är det kortfattat om hur vi ska få vår karta i spelet:

  1. Läs i TMX-filen.
  2. Parsa TMX-filen som en XML-fil.
  3. Ladda alla tegelbilder.
  4. Ordna tegelbilderna i vår kartlayout, lag för lager.
  5. Läs kartobjektet.

Läser i TMX-filen

När det gäller ditt program är detta bara en XML-fil, så det första vi vill göra är att läsa det. De flesta språk har ett XML-bibliotek för detta; i fallet med AS3 ska jag använda XML-klassen för att lagra XML-informationen och en URLLoader att läsa i TMX-filen.

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

Detta är en enkel filläsare för "... /assets/exempel.tmx". Det förutsätter att TMX-filen finns i din projektkatalog under mappen "tillgångar". Vi behöver bara en funktion som ska hanteras när filläsningen är klar:

 privat funktion 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; för varje (var tileset: XML i 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; 

Det här är där den inledande analyseringen äger rum. (Det finns några variabler vi kommer att hålla reda på utanför denna funktion eftersom vi kommer att använda dem senare.)

När vi har lagrat kartdata sparas vi på att analysera varje kakel. Jag har skapat en klass för att lagra varje kakel s information. Vi kommer att trycka på var och en av de här klasserna i en grupp eftersom vi senare kommer att använda dem:

 public class TileSet public var firstgid: uint; allmänhet var lastgid: uint; public var namn: String; allmänheten var kakel bredd: uint; offentlig var källa: String; offentlig var tilleHight: uint; public var imageWidth: uint; public var imageHeight: uint; public var bitmapData: BitmapData; offentlig var tilleAmountWidth: uint; allmän funktion 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; 

Återigen kan du se det gid visas igen, i firstgid och lastgid variabler. Låt oss nu titta på vad det här är för.


Förstå "gid"

För varje kakel måste vi på något sätt associera den med en kakel och en viss plats på den kakelstenen. Detta är syftet med gid.

Titta på gräs kakel-2-small.png brickuppsättning. Den innehåller 72 olika plattor:

Vi ger var och en av dessa plattor en unik gid från 1-72, så att vi kan referera till någon med ett enda nummer. TMX-formatet anger emellertid endast den första gid av kakelstenen, sedan alla andra gids kan härledas från att känna till storleken på kakelstenen och storleken på varje enskild kakel.

Här är en praktisk bild som hjälper till att visualisera och förklara processen.

Så om vi placerar den nedre högra kakel på den här plattan på en karta någonstans, skulle vi lagra gid 72 på den platsen på kartan.

Nu, i exemplet TMX-filen ovan kommer du att märka det tree2-final.png har en firstgid av 73. Det beror på att vi fortsätter att räkna upp på gids, och vi återställer inte den till 1 för varje kakel.

Sammanfattningsvis a gid är ett unikt ID som ges till varje kakel av varje kakel i en TMX-fil, baserat på kakelpositionen inom kakelstenen och antalet kakel som avses i TMX-filen.


Hämtar kakelarna

Nu vill vi ladda alla tegelkällans bilder till minnet så att vi kan lägga vår karta tillsammans med dem. Om du inte skriver detta i AS3 är det enda du behöver veta att vi laddar bilderna för varje kakel här:

 // ladda bilder för kakel 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); 

Det finns några AS3-specifika saker som händer här, till exempel genom att använda Loader-klassen för att ta in flisbilden. (Mer specifikt är det en förlängd Lastare, helt enkelt så vi kan lagra brickuppsättning instanser inom varje Lastare. Detta är så att när lastaren fullbordar kan vi enkelt korrelera lastaren med kakelstenen.)

Det här låter komplicerat men koden är verkligen ganska enkel:

 public class TileCodeEventLoader utökar Loader public var tileSet: TileSet; 

Nu innan vi börjar ta dessa kakel och skapa kartan med dem behöver vi skapa en basbild för att sätta dem på:

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

Vi kopierar kakelinformationen på dessa bitmappsbilder så att vi kan använda dem som bakgrund. Anledningen till att jag ställer in två bilder är så att vi kan ha ett övre lager och ett bottenlager, och få spelaren att flytta in mellan dem för att ge perspektiv. Vi anger också att toppskiktet ska ha en alfakanal.

För de faktiska händelselyttarna för lastare kan vi använda den här koden:

 privat funktion progressHandler (händelse: ProgressEvent): void trace ("progressHandler: bytesLoaded =" + event.bytesLoaded + "bytesTotal =" + event.bytesTotal); 

Det här är en rolig funktion eftersom du kan spåra hur långt bilden har laddats och kan därför ge användaren feedback om hur snabba saker som ska gå, till exempel en framdriftsfält.

 privata funktionen plattorLoadComplete (e: Event): void var currentTileset = e.target.loader.tileSet; currentTileset.bitmapData = Bitmapp (e.target.content) .bitmapData; tileSetsLoaded ++; // vänta tills alla tegelbilder är laddade innan vi kombinerar dem lag för lager till en bitmapp om (tileSetsLoaded == totalTileSets) addTileBitmapData (); 

Här lagrar vi bitmappsdata med kakelplattan som är associerad med den. Vi räknar också med hur många kakelplattor helt laddas, och när de är färdiga kan vi ringa en funktion (jag heter den addTileBitmapData i det här fallet) för att börja sätta ihop kakelbitarna.


Kombinationen av plattorna

För att kombinera plattorna i en enda bild vill vi bygga upp det i lager för lager så att det kommer att visas på samma sätt som förhandsgranskningsfönstret i sida vid sida visas.

Här är vad den sista funktionen kommer att se ut; De kommentarer jag har tagit med i källkoden ska på ett adekvat sätt förklara vad som händer utan att bli alltför rörigt i detaljerna. Jag bör notera att detta kan genomföras på många olika sätt, och din implementering kan se helt annorlunda ut än mina.

 privat funktion addTileBitmapData (): void // ladda varje lager för varje (var lager: XML i xml.layer) var plattor: Array = new Array (); var tileLängd: uint = 0; // tilldela gidet till varje plats i lagret för varje (varelägg: XML i layer.data.tile) var gid: Number = tile.attribute ("gid"); // om gid> 0 om (gid> 0) plattor [tileLength] = gid;  kakellängd ++;  // yttre för slinga fortsätter i nästa snippets

Vad som händer här är att vi analyserar endast plattorna med gids som är över 0, eftersom 0 indikerar en tom kakel och lagrar dem i en array. Eftersom det finns så många "0 plattor" i vårt topplager, skulle det vara ineffektivt att lagra dem alla i minnet. Det är viktigt att notera att vi lagrar platsen för gid med en räknare eftersom vi kommer att använda sitt index i array senare.

 var useBitmap: BitmapData; var layerName: String = layer.attribute ("name") [0]; // bestämmer vart vi ska lägga laget var layerMap: int = 0; switch (layerName) case "Top": layerMap = 1; ha sönder; standard: spår ("använder baslager"); 

I det här avsnittet analyserar vi lagets namn och kontrollerar om det är lika med "Top". Om det är så sätter vi en flagga så vi vet att du kopierar den till det övre bitmappskiktet. Vi kan vara väldigt flexibla med funktioner som denna och använda ännu fler lager arrangerade i vilken ordning som helst.

 // lagra gidan i en 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))];  

Nu lagrar vi gid, som vi analyserade i början till en 2D-array. Du märker dubbla arrayinitialiseringarna; Det här är helt enkelt ett sätt att hantera 2D-arrays i AS3.

Det är lite matematik som händer också. Kom ihåg när vi initialiserade plattor array ovanifrån och hur vi behöll indexet med det? Vi använder nu indexet för att beräkna koordinaten som gid tillhör. Den här bilden visar vad som händer:

Så för det här exemplet får vi gid på index 27 i plattor array och lagra den på tileCoordinates [7] [1]. Perfekt!

 för (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 // vi hittade rätt tilleset för denna gid! currentTileset = tileset1; ha sönder;  var destY: int = spriteForY * tileWidth; var destX: int = spriteForX * tileWidth; // basic math för att ta reda på var kakan kommer från källbilden tilleGid - = currentTileset.firstgid -1; var sourceY: int = Math.ceil (tileGid / currentTileset.tileAmountWidth) -1; var sourceX: int = tileGid - (currentTileset.tileAmountWidth * sourceY) - 1; // kopiera kakel från kakel till vår bitmapp om (layerMap == 0) screenBitmap.bitmapData.copyPixels (currentTileset.bitmapData, new Rectangle (sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWidth, currentTileset. tegelhöjd), ny punkt (destX, destY), null, null, sant);  annars om (layerMap == 1) screenBitmapTopLayer.bitmapData.copyPixels (currentTileset.bitmapData, ny rektangel (sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWidth, currentTileset.tileHeight), ny Point (destX, destY ), null, null, sant); 

Det är här vi äntligen kommer ner för att kopiera kakel till vår karta.

Ursprungligen börjar vi genom att slingra igenom varje kakel koordinat på kartan, och för varje kakel koordinat får vi gid och kolla efter den lagrade kakel som matchar den, genom att kontrollera om den ligger mellan firstgid och vår beräknade lastgid.

Om du förstod Förstå "gid" avsnitt ovanifrån bör denna matematik vara meningsfullt. I de mest grundläggande termerna tar det kakelkoordinaten på kakelstenen (sourceX och sourceY) och kopiera den till vår karta på kakelplatsen vi har slungat till (destX och Desty).

Slutligen, i slutet kallar vi copyPixel funktion för att kopiera kakelbilden på antingen topp- eller basskiktet.


Lägga till objekt

Nu när kopieringen av lagren på kartan är klar, låt oss se på att ladda kollisionsobjekten. Det här är väldigt kraftfullt eftersom vi även kan använda det för andra objekt, till exempel en uppstart eller en spelare, så länge vi har angett den med Tiled.

Så i botten av addTileBitmapData funktion, låt oss ange följande kod:

 för varje (var objektgrupp: XML i xml.objectgroup) var objectGroup: String = objectgroup.attribute ("name"); switch (objectGroup) case "Kollision": för varje (varobjekt: XML i objektgruppobjekt) var rektangel: Form = Ny Form (); rektangel.graphics.beginFill (0x0099CC, 1); rektangel.graphics.drawRect (0, 0, object.attribute ("width"), object.attribute ("height")); rectangle.graphics.endFill (); rektangel.x = objekt.attribut ("x"); rektangel.y = objekt.attribut ("y"); collisionTiles.push (rektangel); addChild (rektangel);  ha sönder; standard: spår ("oigenkänd objekttyp:", objektgrupp.attribut ("namn")); 

Detta kommer att gå igenom objektlagren och leta efter lagret med namnet "Kollision"När den finner det tar det varje objekt i det lagret, skapar en rektangel i den positionen och lagrar den i collisionTiles array. På det sättet har vi fortfarande en hänvisning till det, och vi kan gå igenom för att kontrollera det om kollisioner om vi hade en spelare.

(Beroende på hur ditt system hanterar kollisioner kanske du vill göra något annat.)


Visar kartan

Slutligen, för att visa kartan vill vi först göra bakgrunden och sedan förgrunden för att få lagret korrekt. På andra språk handlar det bara om att göra bilden.

 // ladda bakgrundslag addChild (screenBitmap); // rektangel bara för att visa hur något skulle se in mellan lag var playerExample: Shape = new Shape (); playerExample.graphics.beginFill (0x0099CC, 1); playerExample.graphics.lineStyle (2); // skiss rektangel playerExample.graphics.drawRect (0, 0, 100, 100); playerExample.graphics.endFill (); playerExample.x = 420; playerExample.y = 260; collisionTiles.push (playerExample); addChild (playerExample); // ladda topplager addChild (screenBitmapTopLayer);

Jag har lagt till lite kod mellan lagren här bara för att visa med en rektangel att lagret verkligen fungerar. Här är det slutliga resultatet:

Tack för att du tog dig tid att slutföra handledningen. Jag har inkluderat en zip som innehåller ett komplett FlashDevelop-projekt med all källkod och tillgångar.


Ytterligare läsning

Om du är intresserad av att göra fler saker med Tiled, var en sak som jag inte täckte var egenskaper. Att använda egenskaper är ett litet hopp från att analysera lagnamnen, och det låter dig ställa in ett stort antal alternativ. Om du till exempel vill ha en fientlig gympunkt, kan du ange vilken typ av fiende som är, storleken, färgen och allt, från insidan av kakelplaneringsredigeraren!

Slutligen, som du kanske har märkt, är XML inte det effektivaste formatet för att lagra TMX-data. CSV är ett bra medium mellan enkel parsing och bättre lagring, men det finns också base64 (okomprimerad, zlib komprimerad och gzip komprimerad). Om du är intresserad av att använda dessa format istället för XML, kolla in sidan med sida vid sida på TMX-formatet.