I min forrige artikkel så vi på Tiled Map Editor som et verktøy for å lage nivåer for spillene dine. I denne opplæringen tar jeg deg gjennom det neste trinnet: parsing og rendering av disse kartene i motoren din.
Merk: Selv om denne opplæringen er skrevet med Flash og AS3, bør du kunne bruke de samme teknikkene og konseptene i nesten hvilket som helst spillutviklingsmiljø.
Ved hjelp av TMX-spesifikasjonen kan vi lagre dataene på en rekke måter. For denne opplæringen lagrer vi kartet vårt i XML-formatet. Hvis du planlegger å bruke TMX-filen som er inkludert i kravene, kan du hoppe til neste avsnitt.
Hvis du har laget ditt eget kart, må du fortelle Tiled å lagre det som XML. For å gjøre dette åpner du kartet med flislagt, og velger Rediger> Innstillinger ...
For "Lagre fliser lag data som:" dropdown boksen, velg XML, som vist på bildet nedenfor:
Nå når du lagrer kartet, lagres det i XML-format. Du er velkommen til å åpne TMX-filen med en tekstredigerer for å ta en titt innvendig. Her er en utdrag av hva du kan forvente å finne:
Som du kan se, lagrer den bare alle kartinformasjonen i dette praktiske XML-formatet. Eiendommene skal for det meste være enkle, med unntak av gid
- Jeg vil gå inn i en mer grundig forklaring på dette senere i opplæringen.
Før vi går videre, vil jeg rette oppmerksomheten til objectgroup
"Kollisjon
"element. Som du kanskje husker fra opplæringsveiledningen for kart, angav vi kollisjonen rundt treet, slik lagres det.
Du kan spesifisere power-ups eller spilleren gyte peker på samme måte, slik at du kan forestille deg hvor mange muligheter det er for flislagt som kartredigerer!
Nå er det et kort trekk på hvordan vi får kortet vårt inn i spillet:
Så langt som programmet ditt angår, er dette bare en XML-fil, så det første vi vil gjøre er å lese det. De fleste språk har et XML-bibliotek for dette; i tilfelle av AS3 vil jeg bruke XML-klassen til å lagre XML-informasjonen og en URLLoader å lese i TMX-filen.
xmlLoader = ny URLLoader (); xmlLoader.addEventListener (Event.COMPLETE, xmlLoadComplete); xmlLoader.load (ny URLRequest ("... /assets/example.tmx"));
Dette er en enkel filleser for "... /assets/example.tmx"
. Det antas at TMX-filen ligger i prosjektkatalogen under mappen "assets". Vi trenger bare en funksjon som skal håndteres når filen leses er fullført:
privat funksjon xmlLoadComplete (e: Event): void xml = ny XML (e.target.data); mapWidth = xml.attribute ("width"); mapHeight = xml.attribute ("height"); tileWidth = xml.attribute ("tilewidth"); tileHeight = xml.attribute ("tileheight"); var xmlCounter: uint = 0; for hver (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 ("navn") [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 er her den første parsingen finner sted. (Det er noen variabler vi vil holde oversikt over utenfor denne funksjonen, siden vi vil bruke dem senere.)
Når vi har lagret kartdataene, beveger vi oss på å analysere hver fliserett. Jeg har laget en klasse for å lagre hver flisettinformasjon. Vi vil skyve hver av disse klassene i en gruppe siden vi vil bruke dem senere:
offentlig klasse TileSet public var firstgid: uint; offentlig var sistgid: uint; offentlig var navn: streng; offentlig var flisbredde: uint; offentlig var kilde: streng; offentlige var fliser Høyde: uint; offentlig var imageWidth: uint; offentlig var imageHeight: uint; public var bitmapData: BitmapData; offentlig var tileAmountWidth: uint; offentlig funksjon TileSet (firstgid, navn, fliser, fliser, kilde, imageWidth, imageHeight) this.firstgid = firstgid; this.name = navn; 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;
Igjen kan du se det gid
vises igjen, i firstgid
og lastgid
variabler. La oss se på hva dette er for.
gid
"For hver flis må vi på en eller annen måte knytte den sammen med et fliseresett og et bestemt sted på det flisete. Dette er formålet med gid
.
Se på gress-fliser-to-small.png
brikker. Den inneholder 72 forskjellige fliser:
Vi gir hver av disse flisene en unik gid
fra 1-72, slik at vi kan referere til noen med et enkelt nummer. Imidlertid spesifiserer TMX-formatet bare det første gid
av fliserett, siden alle andre gid
s kan utledes fra å kjenne størrelsen på flisene og størrelsen på hver enkelt flis.
Her er et praktisk bilde for å visualisere og forklare prosessen.
Så hvis vi legger nederste høyre flis av denne fliser på et kart et sted, vil vi lagre gid
72 på den plasseringen på kartet.
Nå, i eksemplet TMX-fil over, vil du legge merke til det tree2-final.png
har en firstgid
av 73. Det er fordi vi fortsetter å telle opp på gid
s, og vi tilbakestiller det ikke til 1 for hver fliserett.
Sammendrag a gid
er en unik ID gitt til hver flis av hver fliser i en TMX-fil, basert på flisens posisjon innenfor flisettene, og antallet fliser som refereres til i TMX-filen.
Nå vil vi laste alle tegningene til teglkilden inn i minnet, slik at vi kan sette vårt kart sammen med dem. Hvis du ikke skriver dette i AS3, er det eneste du trenger å vite at vi laster bildene for hvert fliserett her:
// last bilder for fliser for (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 er noen AS3-spesifikke ting som skjer her, for eksempel å bruke Loader-klassen for å ta med fliserettbildene. (Nærmere bestemt er det en utvidet loader
, ganske enkelt slik at vi kan lagre brikker
forekomster i hver loader
. Dette er slik at når lasteren fullfører, kan vi enkelt korrelere Loader med fliserett.)
Dette kan høres komplisert, men koden er veldig ganske enkel:
offentlig klasse TileCodeEventLoader utvider Loader public var tileSet: TileSet;
Nå før vi begynner å ta disse fliser og lage kartet med dem, må vi lage et grunnbilde for å sette 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 kopierer flisdataene på disse bitmapbildene, slik at vi kan bruke dem som bakgrunn. Grunnen til at jeg satt opp to bilder er at vi kan ha et toppslag og et bunnlag, og spilleren beveger seg mellom dem for å gi perspektiv. Vi spesifiserer også at topplaget skal ha en alfakanal.
For de aktuelle hendelseslytterne for lastere kan vi bruke denne koden:
privat funksjon fremgangshandler (hendelse: ProgressEvent): void trace ("progressHandler: bytesLoaded =" + event.bytesLoaded + "bytesTotal =" + event.bytesTotal);
Dette er en morsom funksjon siden du kan spore hvor langt bildet har lastet inn, og kan derfor gi tilbakemelding til brukeren om hvor raskt det går, for eksempel en fremdriftslinje.
private funksjon fliserLoadComplete (e: Event): void var currentTileset = e.target.loader.tileSet; currentTileset.bitmapData = Bitmap (e.target.content) .bitmapData; tileSetsLoaded ++; // vente til alle teglbildene er lastet inn før vi kombinerer lag for lag til en bitmap hvis (tileSetsLoaded == totalTileSets) addTileBitmapData ();
Her lagrer vi bitmapdataene med fliserettene som er knyttet til det. Vi teller også hvor mange fliser som er fullstendig lastet, og når de er ferdige, kan vi ringe en funksjon (jeg heter den addTileBitmapData
i dette tilfellet) for å begynne å sette flisene sammen.
For å kombinere fliser i et enkelt bilde, vil vi bygge det opp lag for lag slik at det blir vist på samme måte som forhåndsvisningsvinduet i fliser vises.
Her er hva den endelige funksjonen vil se ut som; kommentarene jeg har tatt med i kildekoden, skal tilstrekkelig forklare hva som skjer uten å bli for rotete i detaljene. Jeg bør merke at dette kan implementeres på mange forskjellige måter, og implementeringen din kan se helt annerledes ut enn min.
privat funksjon addTileBitmapData (): void // last hvert lag for hver (var lag: XML i xml.layer) var fliser: Array = new Array (); var tileLength: uint = 0; // tilordne gid til hvert sted i laget for hver (var flis: XML i layer.data.tile) var gid: Number = tile.attribute ("gid"); // hvis gid> 0 hvis (gid> 0) fliser [tileLength] = gid; fliselengde ++; // ytre for sløyfe fortsetter i neste utdrag
Hva som skjer her er at vi bare analyserer fliser med gid
s som er over 0, siden 0 indikerer en tom flis, og lagrer dem i en matrise. Siden det er så mange "0 fliser" i vårt topplag, ville det være ineffektivt å lagre dem alle i minnet. Det er viktig å merke seg at vi lagrer plasseringen av gid
med en teller fordi vi vil bruke indeksen i matrisen senere.
var useBitmap: BitmapData; var layerName: String = layer.attribute ("name") [0]; // bestemme hvor vi skal legge laget var layerMap: int = 0; bytt (lagnavn) case "Top": layerMap = 1; gå i stykker; standard: spor ("bruker grunnlag");
I denne delen parserer vi ut lagnavnet, og kontrollerer om det er lik «Topp». Hvis det er, setter vi et flagg, så vi vet at du skal kopiere det til det øverste bitmap-laget. Vi kan være veldig fleksible med funksjoner som dette, og bruk enda flere lag ordnet i hvilken som helst rekkefølge.
// lagre skjermen i en 2d array var tileCoordinates: Array = new Array (); for (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))];
Nå lagrer vi gid
, som vi analyserte i begynnelsen, til en 2D-serie. Du vil legge merke til de dobbelte arrayinitialiseringer; Dette er bare en måte å håndtere 2D-arrays på i AS3.
Det er litt matematikk som skjer også. Husk da vi initialiserte fliser
array ovenfra, og hvordan holdt vi indeksen med den? Vi vil nå bruke indeksen til å beregne koordinaten som gid
tilhører. Dette bildet viser hva som skjer:
Så for dette eksempelet får vi gid
på indeks 27 i fliser
array, og lagre den på 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 >= tileset1.firstgid-1 && tileGid // vi fant riktig tileset for denne gid! currentTileset = tileset1; gå i stykker; var destY: int = spriteForY * tileWidth; var destX: int = spriteForX * tileWidth; // grunnleggende matte for å finne ut hvor flisen kommer fra på kildebildet tileGid - = currentTileset.firstgid -1; var sourceY: int = Math.ceil (tileGid / currentTileset.tileAmountWidth) -1; var sourceX: int = tileGid - (currentTileset.tileAmountWidth * sourceY) - 1; // kopi flisen fra fliser til vår bitmap hvis (layerMap == 0) screenBitmap.bitmapData.copyPixels (currentTileset.bitmapData, nytt rektangel (sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWidth, currentTileset. tileHeight), nytt Point (destX, destY), null, null, true); annet hvis (layerMap == 1) screenBitmapTopLayer.bitmapData.copyPixels (currentTileset.bitmapData, nytt rektangel (sourceX * currentTileset.tileWidth, sourceY * currentTileset.tileWidth, currentTileset.tileWidth, currentTileset.tileHight), nytt Point (destX, destY ), null, null, sant);
Det er her vi endelig kommer ned for å kopiere flisene til kartet vårt.
I utgangspunktet begynner vi å løkke gjennom hver flisekoordinat på kartet, og for hver flisekoordinat får vi gid
og sjekk for det lagrede fliserettet som samsvarer med det, ved å sjekke om det ligger mellom firstgid
og våre beregnet lastgid
.
Hvis du forsto Forståelse "gid
" delen ovenfor, bør denne matte være fornuftig. I de mest grunnleggende termer tar det flisekoordinatet på fliset (sourceX
og sourceY
) og kopiere den til vårt kart på flisestedet vi har slått til (destX
og destY
).
Til slutt, på slutten kaller vi copyPixel
funksjonen til å kopiere flisbildet på enten topp- eller grunnlaget.
Nå som kopieringen av lagene på kartet er gjort, la oss se på å laste inn kollisionsobjektene. Dette er veldig kraftig fordi det, i tillegg til å bruke det for kollisjonobjekter, kan vi også bruke det til noe annet objekt, for eksempel en oppstart eller en spiller, gyteplass, så lenge vi har angitt den med flislagt flis.
Så på bunnen av addTileBitmapData
funksjon, la oss legge inn følgende kode:
for hver (var objektgruppe: XML i xml.objectgroup) var objectGroup: String = objectgroup.attribute ("name"); switch (objectGroup) case "Kollisjon": for hver (var objekt: XML i objektgruppeobjekt) var rektangel: Shape = ny Shape (); rectangle.graphics.beginFill (0x0099CC, 1); rektangel.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 (rektangel); addChild (rektangel); gå i stykker; standard: spor ("ukjent objekttype:", objektgruppe.attributt ("navn"));
Dette vil løpe gjennom objektlagene, og se etter laget med navnet "Kollisjon
"Når den finner det, tar det hvert objekt i det laget, lager et rektangel på den posisjonen og lagrer det i collisionTiles
array. På den måten har vi fortsatt en referanse til det, og vi kan gå gjennom for å sjekke det for kollisjoner hvis vi hadde en spiller.
(Avhengig av hvordan systemet håndterer kollisjoner, kan det hende du vil gjøre noe annet.)
Til slutt, for å vise kartet, vil vi først gjengi bakgrunnen og deretter forgrunnen, for å få lagringen riktig. På andre språk er dette bare et spørsmål om å gjengi bildet.
// laste bakgrunnslag addChild (screenBitmap); // rektangel bare for å demonstrere hvordan noe ville se mellom lagene var spiller Eksempel: Shape = Ny Shape (); playerExample.graphics.beginFill (0x0099CC, 1); playerExample.graphics.lineStyle (2); // disposisjon rektangel playerExample.graphics.drawRect (0, 0, 100, 100); playerExample.graphics.endFill (); playerExample.x = 420; playerExample.y = 260; collisionTiles.push (playerExample); addChild (playerExample); // laste topplag addChild (screenBitmapTopLayer);
Jeg har lagt til litt kode mellom lagene her bare for å demonstrere med et rektangel at lagleggingen faktisk fungerer. Her er sluttresultatet:
Takk for at du tok deg tid til å fullføre opplæringen. Jeg har tatt med en zip som inneholder et komplett FlashDevelop-prosjekt med all kildekoden og eiendelene.
Hvis du er interessert i å gjøre flere ting med flislagt, var det en ting jeg ikke dekker eiendommer. Bruke egenskaper er et lite hopp fra å analysere lagnavnene, og det lar deg sette et stort antall alternativer. Hvis du for eksempel ville ha en fiendspion punkt, kan du spesifisere typen fiende, størrelse, farge og alt, fra innsiden av flisekarteditoren!
Til slutt, som du kanskje har lagt merke til, er XML ikke det mest effektive formatet for å lagre TMX-dataene. CSV er et fint medium mellom enkel parsing og bedre lagring, men det er også base64 (ukomprimert, zlib komprimert og gzip komprimert). Hvis du er interessert i å bruke disse formatene i stedet for XML, kan du sjekke ut fliset wiki-siden på TMX-formatet.