Ontdekken en weergeven van betegelde TMX-indelingskaarten in uw eigen game-engine

In mijn vorige artikel keken we naar Tiled Map Editor als een hulpmiddel om levels te maken voor je games. In deze zelfstudie doorloop ik de volgende stap: het ontleden en weergeven van die kaarten in uw engine.

Notitie: Hoewel deze tutorial geschreven is met behulp van Flash en AS3, zou je in bijna elke game-ontwikkelomgeving dezelfde technieken en concepten moeten kunnen gebruiken.


Vereisten

  • Betegelde versie 0.8.1: http://www.mapeditor.org/
  • TMX-kaart en tileset vanaf hier. Als je mijn tutorial Introduction to Tiled hebt gevolgd, zou je deze al moeten hebben.

Opslaan in XML-indeling

Met behulp van de TMX-specificatie kunnen we de gegevens op verschillende manieren opslaan. Voor deze zelfstudie bewaren we onze kaart in het XML-formaat. Als u van plan bent om het TMX-bestand te gebruiken dat is opgenomen in het gedeelte met vereisten, kunt u doorgaan naar het volgende gedeelte.

Als u uw eigen kaart hebt gemaakt, moet u Tiled vertellen dat deze moet worden opgeslagen als XML. Open hiervoor je kaart met Tiled en selecteer Bewerken> Voorkeuren ...

Selecteer XML in de vervolgkeuzelijst 'Tegellaaggegevens opslaan als:', zoals wordt weergegeven in de onderstaande afbeelding:

Wanneer u de kaart opslaat, wordt deze nu opgeslagen in XML-indeling. Voel je vrij om het TMX-bestand te openen met een teksteditor om een ​​kijkje naar binnen te nemen. Hier is een fragment van wat u kunt verwachten te vinden:

            ...     ...       

Zoals u ziet, slaat het eenvoudigweg alle kaartinformatie op in dit handige XML-formaat. De eigenschappen moeten meestal eenvoudig zijn, met uitzondering van gid - Ik zal later in de tutorial in een meer diepgaande uitleg hierover ingaan.

Voordat we verdergaan, zou ik uw aandacht willen vestigen op de objectgroup "BotsingZoals u zich misschien herinnert uit de zelfstudie bij het maken van de kaart, hebben we het aanvaringsgebied rond de boom gespecificeerd, dit is hoe het wordt opgeslagen.

Je kunt power-ups of speler-spawn-punten op dezelfde manier opgeven, dus je kunt je voorstellen hoeveel mogelijkheden er zijn voor Tiled als kaarteditor!


Core overzicht

Hier is een kort overzicht van hoe we onze kaart in het spel krijgen:

  1. Lees het TMX-bestand in.
  2. Ontleed het TMX-bestand als een XML-bestand.
  3. Laad alle afbeeldingen van de stenen.
  4. Schik de afbeeldingen van de tegelset laag voor laag in onze kaartlay-out.
  5. Lees kaartobject.

Het TMX-bestand inlezen

Wat uw programma betreft, is dit slechts een XML-bestand, dus het eerste wat we willen doen is het inlezen. De meeste talen hebben hiervoor een XML-bibliotheek; in het geval van AS3 zal ik de XML-klasse gebruiken om de XML-informatie op te slaan en een URLLoader om in het TMX-bestand te lezen.

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

Dit is een eenvoudige bestandslezer voor "... /assets/example.tmx". Het gaat ervan uit dat het TMX-bestand zich in uw projectdirectory onder de map "assets" bevindt. We hebben alleen een functie nodig om te verwerken wanneer het lezen van het bestand voltooid is:

 private function xmlLoadComplete (e: Event): void xml = nieuwe XML (e.target.data); mapWidth = xml.attribute ("width"); mapHeight = xml.attribute ("height"); tileWidth = xml.attribute ("tilewidth"); tileHeight = xml.attribute ("tileheight"); var xmlCounter: uint = 0; voor elk (var tileset: 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 (nieuwe TileSet (firstGid, tilesetName, tilesetTileWidth, tilesetTileHeight, tilesetImagePath, imageWidth, imageHeight)); xmlCounter ++;  totalTileSets = xmlCounter; 

Dit is waar de eerste ontleding plaatsvindt. (Er zijn een paar variabelen die we buiten deze functie zullen bijhouden omdat we ze later zullen gebruiken.)

Zodra de kaartgegevens zijn opgeslagen, gaan we verder met het parseren van elke tileset. Ik heb een klasse gemaakt om de informatie van elke tile op te slaan. We zullen elk van die klasseninstanties in een array pushen, omdat we ze later zullen gebruiken:

 public class TileSet public var firstgid: uint; public var lastgid: uint; public var name: 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; openbare functie TileSet (firstgid, name, tileWidth, tileHeight, source, imageWidth, imageHeight) this.firstgid = firstgid; this.name = naam; 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; 

Nogmaals, dat zie je gid verschijnt opnieuw in de firstgid en lastgid variabelen. Laten we nu kijken naar waar dit voor is.


Begrijpen "gid"

Voor elke tegel moeten we deze op de een of andere manier associëren met een tileset en een bepaalde locatie op die tileset. Dit is het doel van de gid.

Kijk naar de grass-tiles-2-small.png tileset. Het bevat 72 verschillende tegels:

We geven elk van deze tegels een uniek gid van 1-72, zodat we kunnen verwijzen naar iemand met een enkel nummer. Het TMX-formaat geeft echter alleen de eerste aan gid van de tileset, sinds de andere gids kunnen worden afgeleid uit het kennen van de grootte van de tegelset en de grootte van elke afzonderlijke tegel.

Hier is een handig beeld om het proces te visualiseren en uit te leggen.

Dus als we de tegel rechtsonder van deze tegelset ergens op een kaart plaatsen, slaan we de gid 72 op die locatie op de kaart.

Nu, in het voorbeeld TMX-bestand hierboven, zult u dat opmerken tree2-final.png heeft een firstgid van 73. Dat komt omdat we blijven aftellen op de gids, en we zetten het niet terug naar 1 voor elke tileset.

Samenvattend, a gid is een unieke ID gegeven aan elke tegel van elke tileset binnen een TMX-bestand, gebaseerd op de positie van de tile binnen de tileset, en het aantal tilesets waarnaar wordt verwezen in het TMX-bestand.


Tilesets laden

Nu willen we alle bronfoto's van de tileset in het geheugen laden, zodat we onze kaart samen met hen kunnen plaatsen. Als je dit niet in AS3 schrijft, is het enige dat je moet weten dat we de afbeeldingen voor elke tileset hier laden:

 // afbeeldingen voor tileset laden voor (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); 

Er zijn een paar AS3-specifieke dingen die hier aan de hand zijn, zoals het gebruik van de klasse Loader om de afbeeldingen met de tileset in te voeren. (Meer specifiek, het is een uitgebreid lader, eenvoudig zodat we het kunnen opslaan stenenset instanties binnen elk lader. Dit is zo dat wanneer de lader klaar is, we de Loader gemakkelijk kunnen correleren met de tileset.)

Dit klinkt misschien ingewikkeld, maar de code is heel simpel:

 public class TileCodeEventLoader breidt Loader uit public var tileSet: TileSet; 

Voordat we deze setsets gaan maken en de kaart met hen maken, moeten we een basisafbeelding maken om ze aan te zetten:

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

We kopiëren de tegelgegevens naar deze bitmapafbeeldingen zodat we ze als achtergrond kunnen gebruiken. De reden dat ik twee afbeeldingen heb ingesteld, is zodat we een bovenste laag en een onderste laag kunnen hebben en de speler tussen die twee laten bewegen om perspectief te bieden. We geven ook aan dat de bovenste laag een alfakanaal moet hebben.

Voor de echte gebeurtenislisteners voor de loaders kunnen we deze code gebruiken:

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

Dit is een leuke functie omdat je kunt bijhouden hoe ver de afbeelding is geladen en daarom feedback kunt geven aan de gebruiker over hoe snel dingen gaan, zoals een voortgangsbalk.

 private function tilesLoadComplete (e: Event): void var currentTileset = e.target.loader.tileSet; currentTileset.bitmapData = Bitmap (e.target.content) .bitmapData; tileSetsLoaded ++; // wacht tot alle afbeeldingen van de tileset zijn geladen voordat we ze laag voor laag combineren in één bitmap if (tileSetsLoaded == totalTileSets) addTileBitmapData (); 

Hier slaan we de bitmapgegevens op met de daaraan gekoppelde tileset. We tellen ook hoeveel tilesets volledig zijn geladen en wanneer ze allemaal klaar zijn, kunnen we een functie noemen (ik heb hem genoemd) addTileBitmapData in dit geval) om de tegelstukken samen te voegen.


Combinatie van de tegels

Om de tegels in één afbeelding te combineren, willen we deze stap voor laag opbouwen, zodat deze op dezelfde manier wordt weergegeven als het voorbeeldvenster in Tegel wordt weergegeven.

Hier is hoe de laatste functie eruit zal zien; de opmerkingen die ik in de broncode heb opgenomen, moeten op gepaste wijze uitleggen wat er aan de hand is zonder dat de details te ingewikkeld worden. Ik moet er rekening mee houden dat dit op veel verschillende manieren kan worden geïmplementeerd en dat uw implementatie er compleet anders kan uitzien dan de mijne.

 private function addTileBitmapData (): void // laad elke laag voor elk (var layer: XML in xml.layer) var tiles: Array = new Array (); var tileLength: uint = 0; // wijs de gid toe aan elke locatie in de laag voor elke (var-tegel: XML in layer.data.tile) var gid: Number = tile.attribute ("gid"); // if gid> 0 if (gid> 0) tiles [tileLength] = gid;  tileLength ++;  // outer for loop gaat verder in volgende fragmenten

Wat hier gebeurt, is dat we alleen de tegels ontleden gids die boven 0 zijn, omdat 0 een lege tegel aangeeft en deze in een array opslaat. Aangezien er zoveel "0-tegels" in onze bovenste laag zitten, zou het inefficiënt zijn om ze allemaal in het geheugen op te slaan. Het is belangrijk om op te merken dat we de locatie van de gid met een teller omdat we later de index in de array zullen gebruiken.

 var useBitmap: BitmapData; var layerName: String = layer.attribute ("name") [0]; // beslis waar we de laag var layerMap gaan plaatsen: int = 0; switch (layerName) case "Top": layerMap = 1; breken; standaard: trace ("met basislaag"); 

In deze sectie analyseren we de laagnaam en controleren of deze gelijk is aan "Top". Als dat zo is, zetten we een vlag zodat we weten dat deze moet worden gekopieerd naar de bovenste bitmaplaag. We kunnen erg flexibel zijn met functies zoals deze, en gebruiken nog meer lagen die in willekeurige volgorde zijn gerangschikt.

 // sla de gid op in een 2D-matrix var tileCoordinates: Array = new Array (); voor (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 bewaren we hier het gid, die we aan het begin hebben geanalyseerd in een 2D-array. U zult de dubbele array-initialisaties opmerken; dit is gewoon een manier om 2D-arrays in AS3 te verwerken.

Er is ook een beetje wiskunde aan de hand. Weet je nog toen we de tegels matrix van boven en hoe hebben we de index ermee behouden? We zullen nu de index gebruiken om de coördinaat te berekenen die de gid hoort bij. Deze afbeelding laat zien wat er gebeurt:

Dus voor dit voorbeeld krijgen we de gid op index 27 in de tegels array en sla deze op tileCoordinates [7] [1]. Perfect!

 voor (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 // we vonden de juiste tileset voor deze gid! currentTileset = tileset1; breken;  var destY: int = spriteForY * tileWidth; var destX: int = spriteForX * tileWidth; // basisberekening om uit te zoeken waar de tegel vandaan komt op de bronafbeelding tileGid - = currentTileset.firstgid -1; var sourceY: int = Math.ceil (tileGid / currentTileset.tileAmountWidth) -1; var sourceX: int = tileGid - (currentTileset.tileAmountWidth * sourceY) - 1; // kopieer de tegel uit de tileset naar onze bitmap if (layerMap == 0) screenBitmap.bitmapData.copyPixels (currentTileset.bitmapData, new Rectangle (sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWidth, currentTileset. tileHeight), nieuw punt (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); 

Hier komen we eindelijk aan het kopiëren van de tegelset naar onze kaart.

Aanvankelijk beginnen we met het doorlopen van elke tegelcoördinaat op de kaart en voor elke tegelcoördinaat krijgen we de gid en controleer de opgeslagen set bijbehorende sets, door te kijken of deze tussen de firstgid en onze berekend lastgid.

Als je de Begrijpen "gid" sectie van bovenaf, zou deze wiskunde logisch moeten zijn. In de meest eenvoudige termen neemt het de tegelcoördinaat op de tileset (sourceX en sourceY) en kopieer deze naar onze kaart op de tegellocatie waar we een lus naar hebben gemaakt (destX en Desty).

Eindelijk, aan het eind noemen we de copyPixel functie om de tegelafbeelding naar de bovenste of onderste laag te kopiëren.


Objecten toevoegen

Nu het kopiëren van de lagen naar de kaart is voltooid, laten we kijken naar het laden van de botsingobjecten. Dit is zeer krachtig omdat we het naast het gebruiken voor botsingsobjecten ook kunnen gebruiken voor elk ander object, zoals een power-up of spawnlocatie van een speler, net zo lang als we het hebben gespecificeerd met Betegelde.

Dus onderaan het addTileBitmapData functie, laten we de volgende code invoegen:

 voor elk (var objectgroep: XML in xml.objectgroup) var objectGroup: String = objectgroup.attribute ("name"); switch (objectGroup) case "Collision": voor elk (var-object: 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 (rechthoek); addChild (rechthoek);  pauze; default: trace ("niet-herkende objecttype:", objectgroup.attribute ("name")); 

Dit doorloopt de objectlagen en zoekt naar de laag met de naam "BotsingWanneer het het vindt, neemt het elk object in die laag, maakt het een rechthoek op die positie en slaat het op in de collisionTiles matrix. Op die manier hebben we er nog steeds een verwijzing naar, en kunnen we doorlopen om het te controleren op botsingen als we een speler hadden.

(Afhankelijk van hoe uw systeem omgaat met botsingen, wilt u misschien iets anders doen.)


De kaart weergeven

Ten slotte willen we, om de kaart weer te geven, eerst de achtergrond en vervolgens de voorgrond renderen om de laagjes correct te krijgen. In andere talen is dit gewoon een kwestie van het renderen van de afbeelding.

 // laad achtergrondlaag addChild (screenBitmap); // rechthoek alleen om te demonstreren hoe iets eruit zou zien tussen de lagen var playerVoorbeeld: 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); // laad toplaag addChild (screenBitmapTopLayer);

Ik heb hier tussen de lagen een stukje code toegevoegd om met een rechthoek aan te tonen dat de laagjes inderdaad werken. Dit is het eindresultaat:

Bedankt dat je de tijd hebt genomen om de tutorial te voltooien. Ik heb een zip bijgevoegd met een compleet FlashDevelop-project met alle broncode en -items.


Extra lezen

Als je geïnteresseerd bent om meer dingen met Tiled te doen, was er één ding dat ik niet behandelde eigenschappen. Het gebruik van eigenschappen is een kleine sprong van het ontleden van de laagnamen en stelt u in staat een groot aantal opties in te stellen. Als je bijvoorbeeld een vijandelijk spawn point wilt, kun je het type vijand, de grootte, de kleur en alles opgeven vanuit de betegelde kaarteditor!

Ten slotte is XML, zoals u misschien al gemerkt heeft, niet het meest efficiënte formaat om de TMX-gegevens op te slaan. CSV is een aardig medium tussen eenvoudig parseren en betere opslag, maar er is ook base64 (ongecomprimeerd, zlib gecomprimeerd en gzip gecomprimeerd). Als je geïnteresseerd bent in het gebruik van deze formaten in plaats van XML, bekijk dan de Tiled wiki-pagina in het TMX-formaat.