Opprette isometriske verdener En primer for spillutviklere

I denne opplæringen gir jeg deg en bred oversikt over hva du trenger å vite for å lage isometriske verdener. Du vil lære hva isometrisk projeksjon er, og hvordan å representere isometriske nivåer som 2D-arrays. Vi formulerer sammenhenger mellom visningen og logikken, slik at vi enkelt kan manipulere objekter på skjermen og håndtere flisebasert kollisjonsdeteksjon. Vi vil også se på dybdsortering og tegn animasjon.

For å hjelpe fart utviklingen din, kan du finne en rekke isometriske spillverdier på Envato Market, klar til bruk i spillet ditt.

Isometrisk spill eiendeler på Envato Market Relaterte innlegg

Ønsker du enda flere tips om å lage isometriske verdener? Se oppfølgingsposten, Opprett isometriske verdener: En Primer for Gamedevs, Fortsatt, og Juwals bok, Starling Game Development Essentials.


1. Den isometriske verden

Isometrisk visning er en visningsmetode som brukes til å lage en illusjon av 3D for et ellers 2D-spill - noen ganger referert til som pseudo 3D eller 2.5D. Disse bildene (hentet fra Diablo 2 og Age of Empires) illustrerer hva jeg mener:

Diablo 2 Age of Empires

Implementering av en isometrisk visning kan gjøres på mange måter, men for enkelhets skyld vil jeg fokusere på a filbasert tilnærming, som er den mest effektive og brukte metoden. Jeg har lagt hvert skjermbilde over med et diamantgitter som viser hvordan terrenget deles opp i fliser.


2. Tilebaserte spill

I flisbasert tilnærming er hvert visningselement nedbrutt 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 en 2D-serie.

Relaterte innlegg
  • Tony Pas fliserbaserte opplæringsprogrammer.

For eksempel, la oss se på en standard topp-down 2D-visning med to fliser - en gressflise og en veggflate - som vist her:

Noen enkle fliser

Disse fliser er hver i samme størrelse som hverandre, og hver er firkantet, slik at flisens høyde og flisbredde er de samme.

For et nivå med gressletter lukket på alle sider av vegger, vil nivådataens 2D-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 nedenstående nivåbilde:

Et enkelt nivå, vises i en topp ned-visning.

Vi kan forbedre dette ved å legge til hjørnefliser og skille vertikale og horisontale veggfliser, som krever fem ekstra fliser:

[[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]]
Forbedret nivå med fliser tall

Jeg håper konseptet med flisebasert tilnærming er nå klart. Dette er en enkel 2D-nettverksimplementering, som vi kunne kode slik:

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)

Her antar vi at flisbredde og flisens høyde er like (og det samme for alle fliser), og samsvarer med fliserens dimensjoner. Så er flisens bredde og flishøyde for dette eksemplet begge 50px, som utgjør den totale størrelsen på 300x300px - det vil si seks rader og seks kolonner fliser som måler 50x50px hver.

I en normal flisebasert tilnærming implementerer vi enten en topp-nedvisning eller en sidevisning; For en isometrisk visning trenger vi å implementere isometrisk projeksjon.


3. Isometrisk projeksjon

Den beste tekniske forklaringen på hva "isometrisk projeksjon" betyr, så vidt jeg vet, er 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 rett og slett å implementere denne visningen. Det vi trenger å forstå er forholdet mellom 2D-rom og isometrisk plass - det vil si forholdet mellom nivådata og visning. Transformasjonen fra top-down "Cartesian" koordinerer til isometriske koordinater.

Cartesian grid vs isometrisk rutenett.

(Vi vurderer ikke en sekskantet fliserbasert teknikk, noe som er en annen måte å implementere isometriske verdener på.)

Plasser isometriske fliser

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åre inngjerdede grøntnivånivådata:

[[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]]

I dette scenariet kan vi bestemme et walkable område ved å sjekke om arrayelementet er 0 på den koordinaten, og indikerer dermed gress. 2D-visningsimplementeringen av det ovennevnte nivået var en enkel iterasjon med to looper, og plasserte kvadratiske fliser utlignet hver med den faste flisens høyde og flisbreddeverdier.

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)

For isometrisk visning forblir koden den samme, men placeTile () funksjonsendringer.

For en isometrisk visning må vi beregne de tilsvarende isometriske koordinatene inne i løkkene.
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;

Disse funksjonene viser hvordan du kan konvertere fra ett system til et annet:

funksjon isoTo2D (pt: Point): Punkt var tempPt: Punkt = nytt punkt (0, 0); tempPt.x = (2 * pt.y + pt.x) / 2; tempPt.y = (2 * pt.y - pt.x) / 2; retur (tempPt); 
funksjon twoDToIso (pt: Point): Punkt var tempPt: Punkt = nytt punkt (0,0); tempPt.x = pt.x - pt.y; tempPt.y = (pt.x + pt.y) / 2; retur (tempPt); 

Pseudokoden for sløyfen ser så ut som dette:

for (jeg, løp gjennom rader) for (j, loop gjennom kolonner) x = j * flisbredde y = i * flisens høyde tileType = levelData [i] [j] placetile (flisetype, twoDToIso (nytt punkt (x, y) ))
Våre inngjerdet gressletter i en isometrisk utsikt.

For eksempel, la oss se hvordan en typisk 2D-posisjon blir konvertert til en isometrisk posisjon:

2D punkt = [100, 100]; // twoDToIso (2D punkt) vil bli beregnet 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].

Ovennevnte metode gjør det mulig for oss å skape en direkte korrelasjon mellom 2D-nivådataene og de isometriske koordinatene. Vi kan finne flisens koordinater i nivådataene fra sine kartesiske koordinater ved hjelp av denne funksjonen:

funksjon getTileCoordinates (pt: Point, tileHeight: Number): Punkt var tempPt: Punkt = nytt punkt (0, 0); tempPt.x = Math.floor (pt.x / tileHeight); tempPt.y = Math.floor (pt.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 (isoTo2D (skjermpunkt), flis høyde);

Dette skjermpunktet kan for eksempel si en musekliksposisjon eller en opptaksstilling.

Tips: En annen metode for plassering er Zigzag-modellen, som tar en helt annen tilnærming til sammen.

Flytter i isometriske koordinater

Bevegelsen er veldig enkel: Du manipulerer dine spillverdendata i kartesiske koordinater, og bare bruk de ovennevnte funksjonene for å oppdatere den på skjermen. For eksempel, hvis du vil flytte et tegn fremover i den positive y-retningen, kan du bare øke dens verdi y eiendom og konverter deretter sin posisjon til isometriske koordinater:

y = y + hastighet; placetile (twoDToIso (nytt punkt (x, y)))

Dybde sortering

I tillegg til normal plassering må vi ta vare på dybdsortering for å tegne den isometriske verden. Dette sørger for at ting nærmere spilleren trekkes 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 fungerer så lenge vi ikke har noen sprites som opptar mer enn et enkelt fliserum.

Den mest effektive måten å dybde sortering for isometriske verdener er å kutte alle fliser i standard enkeltfliser dimensjoner og ikke tillate større bilder. For eksempel, her er en flis som ikke passer inn i standard flisestørrelsen - se hvordan vi kan dele den i flere fliser som hver passer til flisens dimensjoner:

Et stort bilde er delt inn i flere fliser med standard isometriske dimensjoner

4. Skape kunsten

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

  • Start med et tomt isometrisk rutenett og hold deg til perfekt perfekt presisjon.
  • Prøv å bryte kunst inn i enkelt isometriske fliser bilder.
  • Prøv å sørge for at hver flis også er gang eller ikke-gangbare. Det vil være komplisert hvis vi trenger å ta imot en enkelt flis som inneholder både walkable og non-walkable områder.
  • De fleste fliser må sømløst fliser i en eller flere retninger.
  • Skygger kan være vanskelig å implementere, med mindre vi bruker en lagdelt tilnærming der vi tegner skygger på bakken og trekker helten (eller trær eller andre gjenstander) på topplaget. Hvis tilnærmingen du bruker ikke er flerlagd, sørg for at skyggene faller til forsiden slik at de ikke faller på, si helten når han står bak et tre.
  • Hvis du trenger å bruke et fliserbilde som er større enn standard isometrisk flisestørrelse, kan du prøve å bruke en dimensjon som er et flertall av isoflisstørrelsen. Det er bedre å ha en lagdelt tilnærming i slike tilfeller, hvor vi kan dele kunsten i forskjellige stykker basert på høyden. For eksempel kan et tre oppdeles i tre stykker: roten, stammen og bladverket. Dette gjør det lettere å sortere dybder som vi kan tegne brikker i tilsvarende lag som tilsvarer høydene deres.

Isometriske fliser som er større enn enkeltflisdimensjonene, vil skape problemer med dybdsortering. Noen av problemene er diskutert i disse linkene:

Relaterte innlegg
  • Større fliser.
  • Splitting and Painter's algoritme.
  • Openspaces innlegg på effektive måter å splitte opp større fliser.

5. Isometriske tegn

Implementering av tegn i isometrisk visning er ikke komplisert, da det kan høres ut. Karakterkunst må opprettes i henhold til visse standarder. Først må vi fikse hvor mange bevegelsesretninger som er tillatt i spillet vårt - vanligvis spill vil gi fireveis bevegelse eller åtte-veis bevegelse.

Åtteveis navigasjonsretninger i topp-ned og isometriske visninger.

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 gjengive 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, mot klokka, i den rekkefølge.

En isometrisk karakter vendt i forskjellige retninger.

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 nøkkel representerer denne nøkkelen 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);

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 = twoDToIso (nytt punkt (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.

Kollisjonsdeteksjon

Kollisjonsdeteksjon gjøres ved å sjekke om flisen på den beregnede nye 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.

flis koordinat = getTileCoordinates (isoTo2D (iso punkt), 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.

Dybde sortering med tegn

Tenk på en karakter og en flis i den isometriske verden.

For å forstå dybdsorteringen riktig må vi forstå at når tegnet er 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 samme y-koordinat, bestemmer vi basert på x-koordinatet alene: hvilken av de høyere x-koordinatene som overlapper den andre.

En forenklet versjon av dette er å bare sekvensielt tegne nivåene som starter fra den lengste flisen - det vil si, flis [0] [0] - Deretter tegner du 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.

Dybde sortering må gjøres hver gang en flis endrer posisjon. For eksempel må vi gjøre det når tegn beveger seg. Vi oppdaterer deretter den viste scenen, etter at dybden har blitt utført, for å reflektere dybdeendringene.


6. ha en go!

Nå legger du ny kunnskap til nytte ved å lage en arbeidsprototype, med tastaturkontroller og riktig dybdsortering og kollisjonsdeteksjon. Her er min demo:

Klikk for å gi SWF fokus, bruk piltastene. Klikk her for fullversjonen.

Du kan finne denne brukerklassen nyttig (jeg har skrevet den i AS3, men du bør kunne forstå det på noe annet programmeringsspråk):

pakke com.csharks.juwalbose import flash.display.Sprite; importer flash.geom.Point; offentlig klasse IsoHelper / ** * konvertere et isometrisk punkt til 2D * * / offentlig statisk funksjon isoTo2D (pt: Punkt): Punkt // gx = (2 * isoy + isox) / 2; // gy = (2 * isoy-isox) / 2 var tempPt: Punkt = nytt punkt (0,0); tempPt.x = (2 * + pt.y pt.x) / 2; tempPt.y = (2 * pt.y-pt.x) / 2; retur (tempPt);  / ** * konvertere et 2d-punkt til isometrisk * * / offentlig statisk funksjon twoDToIso (pt: Point): Punkt // gx = (isox-isoxy; // gy = (isoy + isox) / 2 var tempPt: Punkt = nytt punkt (0,0); tempPt.x = pt.x-pt.y; tempPt.y = (pt.x + pt.y) / 2; retur (tempPt); / ** * konvertere en 2d pek på spesifikke fliser rad / kolonne * * / offentlig statisk funksjon getTileCoordinates (pt: Point, fliserHeight: Number): Punkt var tempPt: Punkt = nytt punkt (0,0); tempPt.x = Math.floor (pt.x / tileHeight); tempPt.y = Math.floor (pt.y / tileHeight); return (tempPt); / ** * konverter spesifikke fliser rad / kolonne til 2d punkt * * / offentlig statisk funksjon get2dFromTileCoordinates (pt: tegningHet: Nummer): Punkt varempPt: Punkt = nytt punkt (0,0); tempPt.x = pt.x * teglHet; tempPt.y = pt.y * fliserHjel; retur (tempPt);

Hvis du blir fast, er det full koden fra min demo (i Flash og AS3 tidslinjekode):

// Bruker senocularens KeyObject klasse // http://www.senocular.com/flash/actionscript/?file=ActionScript_3.0/com/senocular/utils/KeyObject.as import flash.display.Sprite; importer com.csharks.juwalbose.IsoHelper; importer flash.display.MovieClip; importer flash.geom.Point; importer flash.filters.GlowFilter; importere flash.events.Event; importer com.senocular.utils.KeyObject; importere flash.ui.Keyboard; importer flash.display.Bitmap; importer flash.display.BitmapData; importer flash.geom.Matrix; importer flash.geom.Rectangle; var levelData = [[1,1,1,1,1,1], [1,0,0,2,0,1], [1,0,1,0,0,1], [1,0 , 0,0,0,1], [1,0,0,0,0,1], [1,1,1,1,1,1]]; var flisbredde: uint = 50; var borderOffsetY: uint = 70; var borderOffsetX: uint = 275; var vendt: String = "sør"; var currentFacing: String = "south"; var helt: MovieClip = ny herotile (); hero.clip.gotoAndStop (vender); var heltepointer: Sprite; var nøkkel: KeyObject = ny KeyObject (scene); // Senocular KeyObject Class var heroHalfSize: uint = 20; // flisene var grassTile: MovieClip = new TileMc (); grassTile.gotoAndStop (1); var wallTile: MovieClip = new TileMc (); wallTile.gotoAndStop (2); // lerretet var bg: Bitmap = ny Bitmap (ny BitmapData (650,450)); addChild (bg); var rekt: rektangel = bg.bitmapData.rect; // for å håndtere dybden var overlegget Container: Sprite = ny Sprite (); addChild (overlayContainer); // for å håndtere retningsbevegelse var dX: Nummer = 0; var dY: tall = 0; var tomgang: boolsk = sant; var hastighet: uint = 5; var heroCartPos: Punkt = nytt punkt (); var heroTile: Point = new Point (); // legge til elementer for å starte nivå, legg til spillsløyfe funksjon createLevel () var tileType: uint; for (var jeg: uint = 0; i 

Registreringspoeng

Ta spesielt hensyn til registreringspoengene til flisene og helten. (Registreringspoeng kan betraktes som opprinnelsespunkter for hvert bestemt sprite.) Disse vil vanligvis ikke falle inne i bildet, men vil helst være øverste venstre hjørne av spriteens avgrensningsboks.

Vi må endre vår tegningskode for å fikse registreringspoengene riktig, hovedsakelig for helten.

Kollisjonsdeteksjon

Et annet interessant poeng å merke seg er at vi beregner kollisjonsdeteksjon basert på punktet hvor helten er.

Men helten har volum og kan ikke representeres nøyaktig av et enkelt punkt, så vi må representere helten som et rektangel og se etter sammenstøt mot hvert hjørne av dette rektangelet slik at det ikke er noen overlapper med andre fliser og dermed ingen dybdefelter.

snarveier

I demoen redigerer jeg bare scenen igjen hver ramme basert på heltenes nye posisjon. Vi finner flisen som helten okkuperer og tegner helten på bakken av bakken når de gjengivende løkkene når disse flisene.

Men hvis vi ser nærmere, finner vi at det ikke er behov for å løpe gjennom alle fliser i dette tilfellet. Gressflisene og de øverste og venstre veggfliserne er alltid tegnet før helten er trukket, så vi trenger aldri å overdrage dem i det hele tatt. Også bunnen og høyre veggfliser er alltid foran helten og dermed tegnet etter helten er tegnet.

I det vesentlige må vi bare utføre dybdsortering mellom veggen i det aktive området og helten - det vil si to fliser. Hvis du merker disse snarveiene, kan du spare mye behandlingstid, noe som kan være avgjørende for ytelsen.


Konklusjon

Nå skal du ha et godt grunnlag for å bygge isometriske spill på egen hånd: Du kan gjøre verden og objektene i den, representere nivådata i enkle 2D-arrays, konvertere mellom kartesiske og isometriske koordinater, og håndtere begreper som dybdesortering og tegn animasjon. Nyt å skape isometriske verdener!

Relaterte innlegg
  • Opprette isometriske verdener: En primer for Gamedevs, fortsetter
  • Rask Tips: Billige 'n' Enkle Isometriske Nivåer
  • Isometrisk fliser Math
  • 6 Utrolig dybdeveiledninger til spillutvikling og design for nybegynnere