Introduksjon til aksiale koordinater for sekskantede fliserbaserte spill

Hva du skal skape

Den grunnleggende sekskantede fliserbaserte tilnærmingen som er forklart i den sekskantede minesveiperopplæringen, får arbeidet gjort, men er ikke veldig effektivt. Den bruker direkte konvertering fra de todimensjonale array-baserte nivådataene og skjermkoordinatene, noe som gjør det unødvendig komplisert å bestemme tappede fliser. 

Dessuten er behovet for å bruke forskjellig logikk, avhengig av ulik eller jevne rad / kolonne av en flis, ikke praktisk. Denne opplæringsserien utforsker de alternative skjermkoordinatsystemene som kan brukes til å lette logikken og gjøre tingene mer praktiske. Jeg vil sterkt foreslå at du leser den sekskantede minesveiper opplæringen før du går videre med denne opplæringen som den forklarer grid rendering basert på en todimensjonal array.

1. Aksialkoordinater

Standardmetoden som brukes for skjermkoordinater i sekskantveiledningen for minesveger kalles offset-koordinat-tilnærmingen. Dette skyldes at de alternative radene eller kolonnene er motvirket av en verdi mens du justerer sekskantet rutenettet. 

For å oppdatere minnet, vennligst se bildet nedenfor, som viser den horisontale tilpasningen med forskjellige koordinatverdier som vises.

I bildet over, en rad med det samme Jeg verdien er uthevet i rødt, og en kolonne med samme j verdien er uthevet i grønt. For å gjøre alt enkelt, vil vi ikke diskutere de ulike og kompenserte varianter, da begge er bare forskjellige måter å få det samme resultatet på. 

La meg introdusere et bedre skjermkoordinatalternativ, den aksiale koordinaten. Konvertering av en kompensasjonskoordinat til en aksial variant er veldig enkel. De Jeg verdien forblir den samme, men j verdien konverteres ved hjelp av formelen aksialJ = i - gulv (j / 2). En enkel metode kan brukes til å konvertere en forskyvning Phaser.Point til sin aksiale variant, som vist nedenfor.

funksjon offsetToAxial (offsetPoint) offsetPoint.y = (offsetPoint.y- (Math.floor (offsetPoint.x / 2))); returner offsetPoint; 

Omvendt konvertering vil være som vist nedenfor.

funksjon axialToOffset (axialPoint) axialPoint.y = (axialPoint.y + (Math.floor (axialPoint.x / 2))); returnere aksialPoint; 

Her x verdien er Jeg verdi og y verdien er j verdi for den todimensjonale gruppen. Etter konvertering vil de nye verdiene se ut som bildet nedenfor.

Legg merke til at den grønne linjen der j verdien forblir den samme, ikke zigzag lenger, men er nå en diagonal til vårt sekskantede rutenett.

For det vertikalt justerte sekskantede ruten, vises forskyvningskoordinatene i bildet under.

Konvertering til aksiale koordinater følger de samme ligningene, med forskjellen som vi beholder j verdien det samme og endre Jeg verdi. Metoden nedenfor viser konverteringen.

funksjon offsetToAxial (offsetPoint) offsetPoint.x = (offsetPoint.x- (Math.floor (offsetPoint.y / 2))); returner offsetPoint; 

Resultatet er som vist nedenfor.

Før vi bruker de nye koordinatene for å løse problemer, la meg raskt introdusere deg til et annet skjermkoordinatalternativ: kubekoordinater.

2. Kube eller kubiske koordinater

Å rette opp sikksagen selv har potensielt løst de fleste ulemper vi hadde med kompensasjonssystemet. Kube eller kubiske koordinater vil videre hjelpe oss med å forenkle komplisert logikk som heuristikk eller rotere rundt en sekskantet celle. 

Som du kanskje har gjettet fra navnet, har det kubiske systemet tre verdier. Den tredje k eller z verdien er avledet fra ligningen x + y + z = 0, hvor x og y er de aksiale koordinatene. Dette fører oss til denne enkle metoden for å beregne z verdi.

funksjon calculateCubicZ (axialPoint) return -axialPoint.x-axialPoint.y; 

Ligningen x + y + z = 0 er egentlig et 3D-plan som passerer gjennom diagonal av et tredimensjonalt kubegitter. Visning av alle tre verdiene for rutenettet vil resultere i følgende bilder for de forskjellige sekskantede justeringene.

Den blå linjen indikerer fliser hvor z verdien forblir den samme. 

3. Fordeler ved det nye koordinatsystemet

Du lurer kanskje på hvordan disse nye koordinatsystemene hjelper oss med sekskantet logikk. Jeg vil forklare noen fordeler før vi fortsetter å lage en sekskantet Tetris ved hjelp av vår nye kunnskap.

Bevegelse

La oss betrakte midtflisen i bildet ovenfor, som har kubiske koordinatverdier for 3,6, -9. Vi har lagt merke til at en koordinatverdi forblir den samme for fliser på de fargede linjene. Videre kan vi se at de gjenværende koordinatene enten øker eller reduseres med 1 mens du sporer noen av de fargede linjene. For eksempel, hvis x verdien forblir den samme og y verdien øker med 1 langs en retning, den z Verdien avtar med 1 for å tilfredsstille vår styrende ligning x + y + z = 0. Denne funksjonen gjør det enklere å kontrollere bevegelsen. Vi vil sette dette til bruk i den andre delen av serien.

Naboer

Av samme logikk er det greit å finne naboene til fliser x, y, z. Ved å holde x Det samme, vi får to diagonale naboer, x, y-1, z + 1 og x, y + 1, z-1. Ved å holde y det samme får vi to vertikale naboer, x-1, y, z + 1 og x + 1, y, z-1. Ved å holde z det samme, får vi de resterende to diagonale naboene, x + 1, y-1, z og x-1, y + 1, z. Bildet nedenfor illustrerer dette for en flis ved opprinnelsen.

Det er så mye lettere nå at vi ikke trenger å bruke forskjellig logikk basert på like eller ulige rader / kolonner.

Flytter rundt en flis

En interessant ting å legge merke til i bildet ovenfor er en slags syklisk symmetri for alle fliser rundt den røde flisen. Hvis vi tar koordinatene til en nabobrik, kan koordinatene til den nærliggende flisen oppnås ved å sykle koordinatverdiene enten til venstre eller høyre og deretter multiplisere med -1. 

For eksempel har den øverste naboen en verdi på -1,0,1, som på roterende høyre blir en gang 1, -1,0 og etter multiplikasjon med -1 blir -1,1,0, som er koordinaten til høyre nabo. Roterende til venstre og multiplikasjon med -1 utbytter 0, -1,1, som er koordinaten til venstre nabo. Ved å gjenta dette, kan vi hoppe mellom alle nabostegene rundt senterflisen. Dette er en veldig interessant funksjon som kan hjelpe til med logikk og algoritmer. 

Vær oppmerksom på at dette skjer bare på grunn av at midtflisen anses å være fra opprinnelsen. Vi kunne lett lage noen fliser x, y, z å være ved opprinnelsen ved å trekke verdiene  x, y og z fra den og alle andre fliser.

heuristikk

Beregning av effektiv heuristikk er nøkkelen når det gjelder patfinding eller lignende algoritmer. Kubiske koordinater gjør det lettere å finne enkle heuristikker for sekskantede grid på grunn av de ovennevnte aspektene. Vi vil diskutere dette i detalj i den andre delen av denne serien.

Dette er noen av fordelene ved det nye koordinatsystemet. Vi kunne bruke en blanding av de forskjellige koordinatsystemene i våre praktiske implementeringer. For eksempel er det todimensjonale arrayet fortsatt den beste måten å lagre nivådataene på, hvor koordinatene er kompensasjonskoordinatene. 

La oss prøve å lage en sekskantet versjon av det berømte Tetris-spillet ved hjelp av denne nye kunnskapen.

4. Opprette en sekskantet tetris

Vi har alle spilt Tetris, og hvis du er en spillutvikler, kan du også ha opprettet din egen versjon også. Tetris er en av de enkleste fliserbaserte spillene man kan implementere, bortsett fra tic tac toe eller checkers, ved hjelp av en enkel todimensjonal matrise. La oss først vise funksjonene til Tetris.

  • Den starter med et tomt todimensjonalt rutenett.
  • Ulike blokker vises øverst og beveger seg ned en flis av gangen til de når bunnen.
  • Når de når bunnen, blir de sementert der eller blir ikke-interaktive. I utgangspunktet blir de en del av rutenettet.
  • Når du går ned, kan blokken beveges sidelengs, roteres med urviseren / mot urviseren og faller ned.
  • Målet er å fylle opp alle flisene i en hvilken som helst rad, hvor hele raden forsvinner, sammenlegger resten av det fylte rutenettet på den.
  • Spillet slutter når det ikke er flere frie fliser på toppen for at en ny blokk skal komme inn i rutenettet.

Representerer de forskjellige blokkene

Siden spillet har blokker som faller vertikalt, vil vi bruke et vertikalt rettet sekskantet rutenett. Dette betyr at de beveger seg sidelengs, vil få dem til å bevege seg på en zigzag måte. En full rad i rutenettet består av et sett med fliser i sikksag rekkefølge. Fra dette punktet kan du begynne å referere til kildekoden som følger med denne opplæringen. 

Nivådataene lagres i en todimensjonal array som heter levelData, og gjengivelsen gjøres ved å bruke forskyvningskoordinatene, som forklart i sekskantveiledningen for minesveger. Vennligst referer til det hvis du har problemer med å følge koden. 

Det interaktive elementet i neste avsnitt viser de forskjellige blokkene som vi skal bruke. Det er en ytterligere blokk, som består av tre fylte fliser som er justert vertikalt som en søyle. BlockData brukes til å lage de forskjellige blokkene. 

funksjon BlockData (topB, topRightB, bottomRightB, bottomB, bottomLeftB, topLeftB) this.tBlock = topB; this.trBlock = topRightB; this.brBlock = bottomRightB; this.bBlock = bottomB; this.blBlock = bottomLeftB; this.tlBlock = topLeftB; this.mBlock = 1; 

En blank blokkmal er et sett med syv fliser som består av en midtflate omgitt av sine seks naboer. For enhver Tetris-blokk, er midtflisen alltid fylt betegnet med en verdi på 1, mens en tom flis vil bli betegnet med en verdi på 0. De forskjellige blokkene er opprettet ved å fylle fliser av BlockData som Nedenfor.

var block1 = ny BlockData (1,1,0,0,0,1); var block2 = ny BlockData (0,1,0,0,0,1); var block3 = ny BlockData (1,1,0,0,0,0); var block4 = ny BlockData (1,1,0,1,0,0); var block5 = ny BlockData (1,0,0,1,0,1); var block6 = ny BlockData (0,1,1,0,1,1); var block7 = ny BlockData (1,0,0,1,0,0);

Vi har totalt syv forskjellige blokker.

Roterer blokkene

La meg vise deg hvordan blokkene roterer ved hjelp av det interaktive elementet nedenfor. Trykk og hold for å rotere blokkene, og trykk x for å endre rotasjonsretningen.

For å rotere blokken må vi finne alle fliser som har en verdi av 1, sett verdien til 0, roter en gang rundt midtflisen for å finne nabobrikken, og sett verdien til 1. For å rotere en flis rundt en annen flis, kan vi bruke logikken som er forklart i beveger seg rundt en flis seksjon over. Vi ankommer til fremgangsmåten nedenfor for dette formålet.

funksjonen rotateTileAroundTile (tileToRotate, anchorTile) tileToRotate =; konvertere til aksial var tileToRotateZ = calculateCubicZ (tileToRotate); // finn z verdi anchorTile = offsetToAxial (anchorTile); // konvertere til aksial var anchorTileZ = calculateCubicZ ankerTile); // finn z verdi tileToRotate.x = tileToRotate.x-anchorTile.x; // finn x forskjell tileToRotate.y = tileToRotate.y-anchorTile.y; // finn y forskjell tileToRotateZ = tileToRotateZ-anchorTileZ; // finne z forskjell var pointArr = [tileToRotate.x, tileToRotate.y, tileToRotateZ]; // fylle array for å rotere pointArr = arrayRotate (pointArr, clockWise); // rotere array, sant for klokken tilteToRotate.x = (- 1 * pointArr [0]) + ankerTile.x; // multipliser med -1 og fjern x differansen tileToRotate.y = (- 1 * pointArr [1]) + anchorTile.y; // multiply med -1 og fjern y differanse tegnetToRotate = axialToOffset (tileToRotate); // konvertere til offset return tileToRotate;  // ... funksjon arrayRotate (arr, reverse) // nifty metode for å rotere array elementer hvis (revers) arr.unshift (arr.pop ()) else arr.push (arr.shift ()) return ar 

Variabelen med urviseren brukes til å rotere med urviseren eller mot urviseren, som oppnås ved å flytte arrayverdiene i motsatt retning arrayRotate.

Flytte blokken

Vi holder styr på Jeg og j forskyvningskoordinater for blokkens midterste flis ved hjelp av variablene blockMidRowValue og blockMidColumnValue henholdsvis. For å flytte blokken øker eller avtar vi disse verdiene. Vi oppdaterer de tilsvarende verdiene i levelData med blokkverdiene ved hjelp av paintBlock metode. Den oppdaterte levelData brukes til å gjengi scenen etter hver statssendring.

var blockMidRowValue; var blockMidColumnValue; // ... funksjon moveLeft () blockMidColumnValue--;  funksjon moveRight () blockMidColumnValue ++;  funksjon dropDown () paintBlock (true); blockMidRowValue ++;  funksjon paintBlock () clockWise = true; var val = 1; changeLevelData (blockMidRowValue, blockMidColumnValue, val); var rotatingTile = ny Phaser.Point (blockMidRowValue-1, blockMidColumnValue); hvis (currentBlock.tBlock == 1) changeLevelData (roterendeTile.x, roterendeTile.y, val * currentBlock.tBlock);  var midPoint = ny Phaser.Point (blockMidRowValue, blockMidColumnValue); rotatingTile = rotateTileAroundTile (rotatingTile, Midpoint); hvis (currentBlock.trBlock == 1) changeLevelData (roterendeTile.x, roterendeTile.y, val * currentBlock.trBlock);  midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotatingTile = rotateTileAroundTile (rotatingTile, Midpoint); hvis (currentBlock.brBlock == 1) changeLevelData (roterendeTile.x, roterendeTile.y, val * currentBlock.brBlock);  midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotatingTile = rotateTileAroundTile (rotatingTile, Midpoint); hvis (currentBlock.bBlock == 1) changeLevelData (roterendeTile.x, roterendeTile.y, val * currentBlock.bBlock);  midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotatingTile = rotateTileAroundTile (rotatingTile, Midpoint); hvis (currentBlock.blBlock == 1) changeLevelData (roterendeTile.x, roterendeTile.y, val * currentBlock.blBlock);  midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotatingTile = rotateTileAroundTile (rotatingTile, Midpoint); hvis (currentBlock.tlBlock == 1) changeLevelData (roterendeTile.x, roterendeTile.y, val * currentBlock.tlBlock);  funksjon endreLevelData (iVal, jVal, newValue, slette) if (! validIndexes (iVal, jVal)) returnere; hvis (slette) if (levelData [iVal] [jVal] == 1) levelData [iVal] [jVal] = 0;  else levelData [iVal] [jVal] = newValue;  funksjon validIndexes (iVal, jVal) if (iVal<0 || jVal<0 || iVal>= levelData.length || jVal> = levelData [0] .length) return false;  returnere sann;  

Her, currentBlock peker til blockData i scenen. I paintBlock, først satte vi levelData verdi for blokkens midtflate til 1 som det alltid er 1 for alle blokker. Midtpunktets indeks er blockMidRowValueblockMidColumnValue

Så flytter vi til levelData indeksen av flisen på toppen av midten flis  blockMidRowValue-1,  blockMidColumnValue, og sett den til 1 hvis blokken har denne flisen som 1. Da roterer vi med urviseren en gang rundt midtflisen for å få neste flis og gjenta samme prosess. Dette er gjort for alle fliser rundt midtflisen til blokken.

Kontrollerer gyldige operasjoner

Mens du beveger eller roterer blokken, må vi sjekke om det er en gyldig operasjon. For eksempel kan vi ikke flytte eller rotere blokken hvis flisene det må okkupert er allerede opptatt. Også, vi kan ikke flytte blokken utenfor vårt todimensjonale rutenett. Vi må også sjekke om blokken kan gå videre, noe som vil avgjøre om vi må sementere blokken eller ikke. 

For alle disse bruker jeg en metode canMove (i, j), som returnerer en boolesk indikerer om blokkering plasseres på i, j er et gyldig trekk. For hver operasjon, før du faktisk endrer levelData verdier, kontrollerer vi om den nye posisjonen for blokken er en gyldig posisjon ved hjelp av denne metoden.

funksjon canMove (iVal, jVal) var validMove = true; var butikk = klokkeWise; var newBlockMidPoint = ny Phaser.Point (blockMidRowValue + iVal, blockMidColumnValue + jVal); med urviseren = true; hvis (! validAndEmpty (newBlockMidPoint.x, newBlockMidPoint.y)) // sjekk midt, alltid 1 validMove = false;  var rotatingTile = ny Phaser.Point (newBlockMidPoint.x-1, newBlockMidPoint.y); hvis (currentBlock.tBlock == 1) if (! validAndEmpty (roterendeTile.x, roterendeTile.y)) // sjekke topp validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); hvis (currentBlock.trBlock == 1) if (! validAndEmpty (roterendeTile.x, roterendeTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); hvis (currentBlock.brBlock == 1) if (! validAndEmpty (roterendeTile.x, roterendeTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); hvis (currentBlock.bBlock == 1) if (! validAndEmpty (roterendeTile.x, roterendeTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); hvis (currentBlock.blBlock == 1) if (! validAndEmpty (roterendeTile.x, roterendeTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); hvis (currentBlock.tlBlock == 1) if (! validAndEmpty (roterendeTile.x, roterendeTile.y)) validMove = false;  clockWise = store; returnere validMove;  funksjon gyldigAndEmpty (iVal, jVal) if (! validIndexes (iVal, jVal)) return false;  annet hvis (levelData [iVal] [jVal]> 1) // occuppied return false;  returnere sann; 

Prosessen her er den samme som paintBlock, men i stedet for å endre noen verdier, returnerer dette bare en boolean som indikerer et gyldig trekk. Selv om jeg bruker rotasjon rundt en midtflis logikk for å finne naboene, er det enklere og ganske effektive alternativet å benytte de direkte koordinatverdiene til naboene, som lett kan bestemmes ut fra de midterste flisekoordinatene.

Rendering spillet

Spillnivået er visuelt representert av a RenderTexture oppkalt gameScene. I matrisen levelData, en ubebodd flis ville ha en verdi av 0, og en okkupert flis ville ha en verdi av 2 eller høyere. 

En sementert blokk er betegnet med en verdi på 2, og en verdi på 5 Betegner en flis som må fjernes som den er en del av en fullført rad. En verdi på 1 betyr at flisen er en del av blokken. Etter hvert spilltilstandsendring, gjør vi nivået ved hjelp av informasjonen i levelData, som vist under.

// ... hexSprite.tint = '0xffffff'; hvis (levelData [i] [j]> - 1) axialPoint = offsetToAxial (axialPoint); cubicZ = calculateCubicZ (axialPoint); hvis (levelData [i] [j] == 1) hexSprite.tint = '0xff0000';  annet hvis (levelData [i] [j] == 2) hexSprite.tint = '0x0000ff';  annet hvis (levelData [i] [j]> 2) hexSprite.tint = '0x00ff00';  gameScene.renderXY (hexSprite, startX, startY, false);  // ... 

Derav en verdi av 0 er gjengitt uten noen fargetone, en verdi på 1 er gjengitt med rød tint, en verdi på 2 er gjengitt med blå fargetone, og en verdi på 5 er gjengitt med grønn fargetone.

5. Det fullførte spillet

Når vi tar alt sammen, får vi det ferdige sekskantede Tetris-spillet. Vennligst gå gjennom kildekoden for å forstå den fullstendige implementeringen. Du vil merke at vi bruker både offsetkoordinater og kubiske koordinater til forskjellige formål. For å finne ut om en rad er fullført, bruker vi for eksempel offsetkoordinater og sjekker levelData rader.

Konklusjon

Dette avsluttes første del av serien. Vi har skapt et sekskantet Tetris-spill med en kombinasjon av offsetkoordinater, aksiale koordinater og kubekoordinater. 

I den avsluttende delen av serien lærer vi om tegnbevegelse ved hjelp av de nye koordinatene på et horisontalt justert sekskantet rutenett.