Vi har alle spilt vår rettferdige andel av fantastiske isometriske spill, det være seg den opprinnelige Diablo, eller Age of Empires eller Commandos. Første gang du kom over et isometrisk spill, har du kanskje lurt på om det var en 2D-spill eller a 3D-spill eller noe helt annet. Verden av isometriske spill har sin mystiske attraksjon for spillutviklere også. La oss prøve å unravel mysteriet med isometrisk projeksjon og prøve å lage en enkel isometrisk verden i denne opplæringen.
Denne opplæringen er en oppdatert versjon av min eksisterende veiledning om å skape isometriske verdener. Den opprinnelige opplæringen brukte Flash med ActionScript og er fortsatt relevant for Flash- eller OpenFL-utviklere. I denne nye opplæringen har jeg besluttet å bruke Phaser med JS-kode, og derved opprette interaktiv HTML5-utgang i stedet for SWF-utgang.
Vær oppmerksom på at dette ikke er en Phaser-utviklingsopplæring, men vi bruker bare Phaser til å enkelt kommunisere de sentrale konseptene for å skape en isometrisk scene. Dessuten er det mye bedre og enklere måter å lage isometrisk innhold på i Phaser, for eksempel Phaser Isometric Plugin.
For enkelhets skyld vil vi bruke flisebasert tilnærming til å skape vår isometriske scene.
I 2D-spill ved hjelp av flisbasert tilnærming, deles hvert visningselement ned i mindre stykker, kalt fliser, med en standardstørrelse. Disse fliser vil bli arrangert for å danne spillverdenen i henhold til forhåndsbestemte nivådata - vanligvis et todimensjonalt utvalg.
Vanligvis bruker fliser basert spill enten a top-down vis eller a sidevisning for spillscenen. La oss se på en standard topp-down 2D-visning med to fliser-a gress flis og a veggfliser-som vist her:
Begge disse flisene er firkantede bilder av samme størrelse, derfor flis høyde og flisbredde er det samme. La oss se på et spillnivå som er et gresslette lukket på alle sider av vegger. I et slikt tilfelle vil nivådataene representert med et todimensjonalt array se slik ut:
[[1,1,1,1,1,1], [1,0,0,0,0,1], [1,0,0,0,0,1], [1,0,0, 0,0,1], [1,0,0,0,0,1], [1,1,1,1,1,1]]
Her, 0
betegner en gressflise og 1
betegner en veggflis. Å arrangere flisene i henhold til nivådataene, vil produsere vår inngjerdet gresslette som vist på bildet nedenfor:
Vi kan gå litt lenger ved å legge til hjørnefliser og skille vertikale og horisontale veggfliser, som krever fem ekstra fliser, noe som fører oss til våre oppdaterte nivådata:
[[3,1,1,1,1,4], [2,0,0,0,0,2], [2,0,0,0,0,2], [2,0,0, 0,0,2], [2,0,0,0,0,2], [6,1,1,1,1,5]]
Sjekk ut bildet nedenfor, hvor jeg har merket fliser med tilhørende fliser i nivådataene:
Nå som vi har forstått konseptet med flisbasert tilnærming, la meg vise deg hvordan vi kan bruke en enkel 2D-nettpseudokode for å gjengi vårt nivå:
for (jeg, loop gjennom rader) for (j, loop gjennom kolonner) x = j * flisbredde y = i * flisens høyde tileType = levelData [i] [j] placetile (flisetype, x, y)
Hvis vi bruker ovennevnte fliser bilder, er flisens bredde og flisens høyde lik (og det samme for alle fliser), og vil samsvare med fliserens dimensjoner. Så flisebredden og flisens høyde for dette eksemplet er begge 50 px, noe som utgjør den totale nivåstørrelsen på 300 x 300 px-det vil si seks rader og seks kolonner med fliser som måler 50 x 50 px hver.
Som diskutert tidligere, i en normal flisbasert tilnærming, implementerer vi enten en topp-nedvisning eller en sidevisning; For en isometrisk visning må vi implementere isometrisk projeksjon.
Den beste tekniske forklaringen på hva isometrisk projeksjon betyr, så vidt jeg er klar over, fra denne artikkelen av Clint Bellanger:
Vi vinker kameraet vårt langs to akser (sving kameraet 45 grader til en side, deretter 30 grader ned). Dette skaper et diamant (rhombus) formet rutenett hvor rutenettene er dobbelt så brede som de er høye. Denne stilen ble popularisert av strategispill og action-rollespill. Hvis vi ser på en terning i denne visningen, er tre sider synlige (to og to sider).
Selv om det høres litt komplisert, er det faktisk veldig enkelt å implementere denne visningen. Det vi trenger å forstå er forholdet mellom 2D-rom og det isometriske rommet, det vil si forholdet mellom nivådata og visningen. Transformasjonen fra topp-down kartesiske koordinater til isometriske koordinater. Bildet nedenfor viser den visuelle transformasjonen:
La meg prøve å forenkle forholdet mellom nivådata lagret som 2D-array og isometrisk visning-det vil si hvordan vi forvandler kartesiske koordinater til isometriske koordinater. Vi vil forsøke å lage isometrisk visning for vårt nåberømte, befestede gresslette. 2D-visningen av nivået var en enkel iterasjon med to sløyfer, og plasserte kvadratiske fliser utlignet hver med den faste flisens høyde og flisbreddeverdier. For isometrisk visning forblir pseudokoden den samme, men placeTile ()
funksjonsendringer.
Den opprinnelige funksjonen tegner bare flisbildene ved de angitte koordinatene x
og y
, men for en isometrisk visning må vi beregne de tilsvarende isometriske koordinatene. Ligningene for å gjøre dette er som følger, hvor isoX
og isoY
representere isometriske x- og y-koordinater, og cartX
og Carty
representerer kartesiske x- og y-koordinater:
// kartesisk til isometrisk: isoX = cartX - cartY; isoY = (cartX + cartY) / 2;
// Isometrisk til kartesisk: cartX = (2 * isoY + isoX) / 2; cartY = (2 * isoY - isoX) / 2;
Ja, det er det. Disse enkle ligningene er den magiske bak isometriske fremspring. Her er Phaser hjelperfunksjoner som kan brukes til å konvertere fra ett system til et annet ved hjelp av det veldig praktiske Punkt
klasse:
funksjon cartesianToIsometric (cartPt) var tempPt = ny Phaser.Point (); tempPt.x = cartPt.x-cartPt.y; tempPt.y = (cartPt.x + cartPt.y) / 2; retur (tempPt);
funksjon isometricToCartesian (isoPt) var tempPt = ny Phaser.Point (); tempPt.x = (2 * + isoPt.y isoPt.x) / 2; tempPt.y = (2 * isoPt.y-isoPt.x) / 2; retur (tempPt);
Så vi kan bruke cartesianToIsometric
hjelpemetode for å konvertere de innkommende 2D-koordinatene til isometriske koordinater inne i placeTile
metode. Bortsett fra dette er gjengivelsen koden den samme, men vi må ha nye bilder for fliser. Vi kan ikke bruke de gamle firkantede fliser som brukes til topp-down-rendering. Bildet nedenfor viser de nye isometriske gress- og veggfliser sammen med det gjengitte isometriske nivået:
Utrolig, er det ikke? La oss se hvordan en typisk 2D-posisjon blir konvertert til en isometrisk posisjon:
2D punkt = [100, 100]; // isometrisk punkt beregnes som under isoX = 100-100; // = 0 isoY = (100 + 100) / 2; // = 100 Iso punkt == [0, 100];
Tilsvarende, et innspill av [0, 0]
vil resultere i [0, 0]
, og [10, 5]
vil gi [5, 7,5]
.
For vårt inngjerdede gressområde kan vi bestemme et walkable område ved å sjekke om arrayelementet er 0
på den koordinaten, og indikerer dermed gress. For dette må vi bestemme array koordinatene. Vi kan finne flisens koordinater i nivådataene fra sine kartesiske koordinater ved hjelp av denne funksjonen:
funksjon getTileCoordinates (cartPt, tileHeight) var tempPt = new Phaser.Point (); tempPt.x = Math.floor (cartPt.x / tileHeight); tempPt.y = Math.floor (cartPt.y / tileHeight); retur (tempPt);
(Her antar vi hovedsakelig at flisens høyde og flisbredde er like, som i de fleste tilfeller.)
Derfor, fra et par skjerm (isometriske) koordinater, kan vi finne fliser koordinater ved å ringe:
getTileCoordinates (isometricToCartesian (skjermpunkt), flis høyde);
Dette skjermpunktet kan for eksempel si en musekliksposisjon eller en opptaksstilling.
I Flash kan vi angi vilkårlig poeng for en grafikk som midtpunkt eller [0,0]
. Phaser-ekvivalenten er Dreie
. Når du plasserer grafikken, sier du det [10,20]
, så dette Dreie
punktet vil bli justert med [10,20]
. Som standard betraktes det øverste venstre hjørnet av en grafikk som sin [0,0]
eller Dreie
. Hvis du prøver å opprette over nivået ved hjelp av koden som er oppgitt, vil du ikke få det viste resultatet. I stedet vil du få et flatt land uten vegger, som nedenfor:
Dette skyldes at flisebildene er av forskjellige størrelser, og vi tar ikke opp høydeattributtet til veggflisen. Bildet under viser de forskjellige flisbildene vi bruker med sine avgrensningsbokser og en hvit sirkel der deres standard [0,0] er:
Se hvordan helten blir feiljustert når du tegner med standardpivotene. Legg også merke til hvordan vi mister høyden til veggflisen hvis den er tegnet med standard pivoter. Bildet til høyre viser hvordan de må være riktig justert slik at veggflisen får sin høyde og helten blir plassert midt på gresflisen. Dette problemet kan løses på forskjellige måter.
For denne opplæringen har jeg valgt å bruke den tredje metoden slik at dette fungerer selv med et rammeverk uten muligheten til å sette svingpunkter.
Vi vil aldri prøve å flytte vår karakter eller prosjektil i isometriske koordinater direkte. I stedet vil vi manipulere våre spillverden data i kartesiske koordinater og bare bruke de ovennevnte funksjonene for å oppdatere de på skjermen. For eksempel, hvis du vil flytte et tegn fremover i den positive y-retningen, kan du bare øke dens verdi y
eiendom i 2D koordinater og deretter konvertere den resulterende posisjonen til isometriske koordinater:
y = y + hastighet; placetile (kartesiskToIsometrisk (ny Phaser.Point (x, y)))
Dette vil være en god tid å se gjennom alle de nye konseptene vi har lært så langt, og å prøve å skape et fungerende eksempel på noe som beveger seg i en isometrisk verden. Du finner de nødvendige bildemidlene i eiendeler
mappe av kilde git repository.
Hvis du prøvde å flytte ballbildet i vår inngjerdede hage, så ville du komme over problemene med dybdsortering. I tillegg til normal plassering må vi ta vare på dybdsortering for å tegne isometriske verden, hvis det er bevegelige elementer. Korrekt dybdsortering sørger for at gjenstander nærmere skjermen er trukket på toppen av gjenstander lenger unna.
Den enkleste dybdsorteringsmetoden er ganske enkelt å bruke den kartesiske y-koordinatverdien, som nevnt i denne Quick Tip: Jo lenger opp skjermen objektet er, jo tidligere skal det tegnes. Dette kan fungere bra for svært enkle isometriske scener, men en bedre måte vil være å omdanne den isometriske scenen når en bevegelse skjer, i henhold til flisens samordningskoordinater. La meg forklare dette konseptet i detalj med vår pseudokode for nivåtegning:
for (jeg, loop gjennom rader) for (j, loop gjennom kolonner) x = j * flisbredde y = i * flisens høyde tileType = levelData [i] [j] placetile (flisetype, x, y)
Tenk på at varen vår eller tegnet er på flisen [1,1]
-det vil si den øverste grønne flisen i isometrisk visning. For å kunne tegne nivået, må tegnet tegnes etter tegning av hjørneveggflisen, både venstre og høyre veggfliser, og bakken flis, som nedenfor:
Hvis vi følger vår tegningsløype i henhold til pseudokoden ovenfor, tegner vi den midtre hjørneveggen først, og deretter fortsetter å tegne alle veggene i høyden til høyre når den kommer til høyre hjørne.
Så, i neste sløyfe, vil den tegne veggen til venstre for tegnet, og deretter gressflisen som tegnet står på. Når vi bestemmer dette er flisen som okkuperer vår karakter, tegner vi tegnet etter tegner gressflisen. På denne måten, hvis det var vegger på de tre frie gressfliser som er koblet til den som tegnet står på, vil disse veggene overlappe karakteren, noe som resulterer i riktig dybde sortert gjengivelse.
Isometrisk kunst kan være piksel kunst, men det trenger ikke å være. Når du håndterer isometrisk pikselkunst, forteller RhysDs guide deg nesten alt du trenger å vite. Noen teori finnes også på Wikipedia.
Når du lager isometrisk kunst, er de generelle reglene:
Isometriske fliser som er større enn enkeltflisdimensjonene, vil skape problemer med dybdsortering. Noen av problemene er diskutert i disse linkene:
Først må vi fikse hvor mange bevegelsesretninger som er tillatt i vårt spill. Vanligvis vil spillene gi fireveisbevegelse eller åtte-veis bevegelse. Ta en titt på bildet nedenfor for å forstå sammenhengen mellom 2D-rommet og det isometriske rommet:
Vær oppmerksom på at et tegn vil bevege seg vertikalt opp når vi trykker på pil opp tast inn et topp-ned spill, men for et isometrisk spill vil tegnet bevege seg i en 45 grader vinkel mot øverste høyre hjørne.
For en topp ned-visning kunne vi lage et sett med tegneanimasjoner som vender mot en retning, og bare rotere dem for alle de andre. For isometrisk tegneliste må vi gjengi hver animasjon i hver av de tillatte retningene, så for åtteveis bevegelse må vi lage åtte animasjoner for hver handling.
For enkel forståelse betegner vi vanligvis retningene som Nord, Nordvest, Vest, Sørvest, Sør, Sør-Øst, Øst og Nord-Øst. Karakterrammene nedenfor viser inaktive rammer som starter fra Sør-Øst og går med klokken:
Vi plasserer tegn på samme måte som vi legger fliser. Bevegelsen av et tegn oppnås ved å beregne bevegelsen i kartesiske koordinater og deretter konvertere til isometriske koordinater. La oss anta at vi bruker tastaturet til å kontrollere tegnet.
Vi vil stille to variabler, dX
og dY
, basert på retningstastene trykket. Som standard vil disse variablene være 0
og vil bli oppdatert som i tabellen nedenfor, hvor U
, D
, R
, og L
betegne Opp, Ned, Ikke sant, og Venstre piltastene, henholdsvis. En verdi på 1
under en tast representerer at nøkkelen blir presset; 0
innebærer at nøkkelen ikke blir presset.
Nøkkelpos UDRL dX dY =========== 0 0 0 0 0 0 1 0 0 0 0 1 0 1 0 0 0 -1 0 0 1 0 1 0 0 0 0 1 -1 0 1 0 1 0 1 1 1 0 0 1 -1 1 0 1 1 0 1 -1 0 1 0 1 -1 -1
Nå bruker du verdiene til dX
og dY
, vi kan oppdatere kartesiske koordinatene slik:
newX = currentX + (dX * hastighet); newY = currentY + (dY * hastighet);
Så dX
og dY
stå for endringen i tegnets x- og y-posisjon, basert på tastene trykket. Vi kan enkelt beregne de nye isometriske koordinatene, som vi allerede har diskutert:
Iso = cartesianToIsometric (ny Phaser.Point (newX, newY))
Når vi har den nye isometriske posisjonen, må vi bevege seg tegnet til denne stillingen. Basert på verdiene vi har for dX
og dY
, Vi kan bestemme hvilken retning karakteren står overfor, og bruk den tilhørende tegneserien. Når tegnet er flyttet, må du ikke glemme å male på nytt med riktig dybdsortering ettersom tegnetekninatene til tegnet kan ha endret seg.
Kollisjonsdeteksjon gjøres ved å sjekke om flisene på den nylig beregnede posisjonen er en ikke-gangbar flis. Så, når vi finner den nye stillingen, flytter vi ikke umiddelbart tegnet der, men kontroller først for å se hvilken flis som tar plass.
fliser koordinat = getTileCoordinates (isometricToCartesian (nåværende posisjon), flis høyde); hvis (isWalkable (flisekoordinat)) moveCharacter (); annet // gjør ingenting;
I funksjonen isWalkable ()
, vi kontrollerer om nivådata arrayverdien ved den angitte koordinaten er en walkable flis eller ikke. Vi må passe på å oppdatere retningen karakteren står overfor-selv om han ikke beveger seg, som i tilfelle han slår en ikke-walkable flis.
Nå kan dette høres ut som en skikkelig løsning, men det vil bare fungere for gjenstander uten volum. Dette er fordi vi bare vurderer et enkelt punkt, som er midtpunktet på tegnet, for å beregne kollisjon. Det vi virkelig trenger å gjøre er å finne alle de fire hjørnene av tegnet fra den tilgjengelige 2D midtpunktskoordinaten og beregne kollisjoner for alle disse. Hvis et hjørne faller inne i en ikke-walkable flis, bør vi ikke flytte tegnet.
Tenk på et tegn og en flis i den isometriske verden, og de begge har de samme bildestørrelsene, men urealistisk det høres ut.
For å forstå dybdsorteringen riktig må vi forstå at når tegnet x- og y-koordinater er mindre enn treets tre, overlapper treet tegnet. Når karakterens x- og y-koordinater er større enn treets karakter, overlapper tegnet treet. Når de har samme x-koordinat, bestemmer vi basert på y-koordinatet alene: avhengig av hvilken høyere y-koordinat overlapper den andre. Når de har den samme y-koordinaten, bestemmer vi ut fra x-koordinatet alene: hvilken har den høyere x-koordinat som overlapper den andre.
Som forklart tidligere, er en forenklet versjon av dette bare å tegne nivåene som starter fra den lengste flisen, det vil si, flis [0] [0]
-og deretter tegne alle flisene i hver rad en etter en. Hvis et tegn opptar en flis, tegner vi først bakken og deretter gir tegnet flisen. Dette vil fungere bra, fordi tegnet ikke kan oppta en veggflis.
Dette er en demonstrasjon i Phaser. Klikk for å fokusere på det interaktive området og bruk piltastene dine for å flytte tegnet. Du kan bruke to piltastene til å bevege seg i diagonale retninger.
Du finner den komplette kilden til demoen i kildearkivet for denne opplæringen.