Heksagonal karakterbevegelse ved hjelp av aksiale koordinater

Hva du skal skape

I den første delen av serien undersøkte vi de forskjellige koordinatsystemene for sekskantede fliserbaserte spill ved hjelp av et sekskantet Tetris-spill. En ting du kanskje har lagt merke til er at vi fremdeles stoler på offsetkoordinatene for å tegne nivået på skjermen ved hjelp av levelData matrise. 

Du kan også være nysgjerrig på hvordan vi kunne bestemme de aksiale koordinatene til en sekskantet flise fra pikselkoordinatene på skjermen. Metoden som brukes i sekskantet minesweeper-opplæringen er basert på offsetkoordinatene, og er ikke en enkel løsning. Når vi har funnet ut dette, vil vi fortsette å skape løsninger for sekskantet tegnbevegelse og pathfinding.

1. Konvertering av koordinater mellom piksel og aksial

Dette vil innebære litt matte. Vi bruker det horisontale oppsettet for hele opplæringen. La oss starte med å finne et svært nyttig forhold mellom den vanlige sekskantens bredde og høyde. Vennligst se bildet nedenfor.

Tenk på den blå vanlige sekskanten på venstre side av bildet. Vi vet allerede at alle sidene er like lange. Alle innvendige vinkler er 120 grader hver. Ved å koble hvert hjørne til midten av sekskanten, får du seks trekanter, hvorav en er vist med røde linjer. Denne trekanten har alle de indre vinklene lik 60 grader. 

Som den røde linjen deler de to hjørnesvinklene i midten, får vi det 120/2 = 60. Den tredje vinkelen er 180- (60 + 60) = 60 som summen av alle vinkler i triangelen skal være 180 grader. Således er trekantene i hovedsak en like-sidig trekant, noe som betyr at hver side av trekanten har samme lengde. Så i den blå heksagonen er de to røde linjene, den grønne linjen og hvert blå linjesegment av samme lengde. Fra bildet er det klart at den grønne linjen er hexTileHeight / 2.

Når vi går til sekskanten på høyre side, kan vi se at siden lengden er lik hexTileHeight / 2, høyden på den øverste triangulære delen skal være hexTileHeight / 4 og høyden på den nederste triangulære delen skal være hexTileHeight / 4, som tilsvarer hele sekskantens høyde, hexTileHeight

Overvei nå den lille, rettvinklede trekanten øverst til venstre med en grønn og en blå vinkel. Den blå vinkelen er 60 grader da det er halvparten av hjørnesvinkelen, som igjen betyr at den grønne vinkelen er 30 grader (180- (60 + 90)). Ved å bruke denne informasjonen kommer vi til et forhold mellom høyde og bredde på den vanlige sekskanten.

tan 30 = motsatt side / tilstøtende side; 1 / sqrt (3) = (hexTileHeight / 4) / (hexTileWidth / 2); hexTileWidth = sqrt (3) * hexTileHeight / 2; hexTileHeight = 2 * hexTileWidth / sqrt (3);

Konvertere aksial til pikselkoordinater

Før vi nærmer oss konverteringen, la vi se på bildet av den horisontale sekskantede utformingen der vi har markert rad og kolonne der en av koordinatene forblir den samme.

Med tanke på skjerm y-verdien kan vi se at hver rad har en y-offset for 3 * hexTileHeight / 4, Mens du går ned på den grønne linjen, er den eneste verdien som endrer seg Jeg. Derfor kan vi konkludere med at y-pikselverdien bare avhenger av den aksiale Jeg koordinere.

y = (3 * hexTileHeight / 4) * i; y = 3/2 * s * i;

Hvor s er sidelengden, som ble funnet å være hexTileHeight / 2.

Skjerm x-verdien er litt mer komplisert enn dette. Når du vurderer flisene i en enkelt rad, har hver flis et x-offsett for hexTileWidth, som tydelig bare avhenger av den aksiale j koordinere. Men hver alternativ rad har en ekstra kompensasjon for hexTileWidth / 2 avhengig av aksial Jeg koordinere.

Igjen vurderer den grønne linjen, hvis vi antar at det var et firkantet rutenett, ville linjen ha vært vertikal og tilfredsstille ligningen x = j * hexTileWidth. Som den eneste koordinaten som endres langs den grønne linjen er Jeg, Avsetningen vil avhenge av den. Dette fører oss til følgende ligning.

x = j * hexTileWidth + (i 'hexTileWidth / 2); = j * sqrt (3) * hexTileHeight / 2 + i * sqrt (3) * hexTileHeight / 4; = sqrt (3) * s * (j + (i / 2));

Så her har vi dem: ligningene for å konvertere aksiale koordinater til skjermkoordinater. Den tilsvarende konverteringsfunksjonen er som nedenfor.

var rootThree = Math.sqrt (3); var sideLength = hexTileHeight / 2; funksjon axialToScreen (axialPoint) var tileX = rootThree * sideLength * (axialPoint.y + (axialPoint.x / 2)); var tileY = 3 * sideLength / 2 * axialPoint.x; axialPoint.x = tilex; axialPoint.y = Tiley; returnere aksialPoint; 

Den reviderte koden for tegning av sekskantet rutenett er som følger.

for (var i = 0; i < levelData.length; i++)  for (var j = 0; j < levelData[0].length; j++)  axialPoint.x=i; axialPoint.y=j; axialPoint=offsetToAxial(axialPoint); screenPoint=axialToScreen(axialPoint); if(levelData[i][j]!=-1) hexTile= new HexTileNode(game, screenPoint.x, screenPoint.y, 'hex', false,i,j,levelData[i][j]); hexGrid.add(hexTile);   

Konvertering av piksel til aksiale koordinater

Omvendt disse likningene med den enkle substitusjonen av en variabel vil føre oss til skjermen til aksiale konverteringsligninger.

i = y / (3/2 * s); j = (x (y / sqrt (3))) / s * sqrt (3);

Selv om de nødvendige aksiale koordinatene er heltall, vil ligningene resultere i flytende punktnumre. Så vi må rulle dem av og bruke noen korreksjoner, avhengig av vår hovedligning x + y + z = 0. Konverteringsfunksjonen er som nedenfor.

funksjon screenToAxial (screenPoint) var axialPoint = new Phaser.Point (); axialPoint.x = screenPoint.y / (1,5 * sidelength); axialPoint.y = (screenPoint.x- (screenPoint.y / rootThree)) / (rootThree * sidelength); var cubicZ = calculateCubicZ (axialPoint); var round_x = Math.round (axialPoint.x); var round_y = Math.round (axialPoint.y); var round_z = Math.round (cubicZ); hvis (round_x + round_y + round_z === 0) screenPoint.x = round_x; screenPoint.y = round_y;  ellers var delta_x = Math.abs (axialPoint.x-round_x); var delta_y = Math.abs (axialPoint.y-round_y); var delta_z = Math.abs (cubicZ-round_z); hvis (delta_x> delta_y && delta_x> delta_z) screenPoint.x = -round_y-round_z; screenPoint.y = round_y;  ellers hvis (delta_y> delta_x && delta_y> delta_z) screenPoint.x = round_x; screenPoint.y = -round_x-round_z;  ellers hvis (delta_z> delta_x && delta_z> delta_y) screenPoint.x = round_x screenPoint.y = round_y;  return screenPoint; 

Sjekk ut det interaktive elementet, som bruker disse metodene til å vise fliser og oppdage kraner.

2. Karakterbevegelse

Kjernekonseptet med tegnbevegelse i et hvilket som helst rutenett er lik. Vi poller for brukerinngang, bestemmer retningen, finner den resulterende posisjonen, kontroller om den resulterende posisjonen faller inn i en vegg i rutenettet, ellers beveger du tegnet til den posisjonen. Du kan referere til min isometriske tegnbevegelsesopplæring for å se dette i aksjon med hensyn til isometrisk koordinatkonvertering. 

De eneste tingene som er forskjellige her er koordinatkonvertering og bevegelsesretninger. For et horisontalt justert sekskantet rutenett er det seks tilgjengelige retninger for bevegelse. Vi kunne bruke tastaturtastene EN, W, E, D, X, og Z for å styre hver retning. Standard tastaturoppsett samsvarer perfekt med retningene, og de tilhørende funksjonene er som nedenfor.

funksjon moveLeft () movementVector.x = movementVector.y = 0; movementVector.x = -1 * hastighet; CheckCollisionAndMove ();  funksjon moveRight () movementVector.x = movementVector.y = 0; movementVector.x = hastighet; CheckCollisionAndMove ();  funksjonen moveTopLeft () movementVector.x = -0.5 * speed; // Cos60 movementVector.y = -0.866 * speed; // sine60 CheckCollisionAndMove ();  funksjonen moveTopRight () movementVector.x = 0.5 * speed; // Cos60 movementVector.y = -0.866 * speed; // sine60 CheckCollisionAndMove ();  funksjonen moveBottomRight () movementVector.x = 0.5 * speed; // Cos60 movementVector.y = 0.866 * speed; // sine60 CheckCollisionAndMove ();  funksjon moveBottomLeft () movementVector.x = -0.5 * speed; // Cos60 movementVector.y = 0.866 * speed; // sine60 CheckCollisionAndMove (); 

De diagonale bevegelsesretningene gjør en vinkel på 60 grader med den horisontale retningen. Så vi kan direkte beregne den nye posisjonen ved hjelp av trigonometri ved å bruke Cos 60 og Sine 60. Fra dette movementVector, Vi finner ut den nye resulterende posisjonen og sjekker om den faller inne i en vegg i rutenett som nedenfor.

funksjon CheckCollisionAndMove () var tempPos = ny Phaser.Point (); tempPos.x = hero.x + movementVector.x; tempPos.y = hero.y + movementVector.y; var hjørne = ny Phaser.Point (); // sjekk tl corner.x = tempPos.x-heroSize / 2; corner.y = tempPos.y-heroSize / 2; if (checkCorner (hjørne)) retur; // sjekk tr corner.x = tempPos.x + heroSize / 2; corner.y = tempPos.y-heroSize / 2; if (checkCorner (hjørne)) retur; // sjekk bl corner.x = tempPos.x-heroSize / 2; corner.y = tempPos.y + heroSize / 2; if (checkCorner (hjørne)) retur; // sjekk br corner.x = tempPos.x + heroSize / 2; corner.y = tempPos.y + heroSize / 2; if (checkCorner (hjørne)) retur; hero.x = tempPos.x; hero.y = tempPos.y;  funksjonskontrollCorner (hjørne) hjørne = skjermToAksialt (hjørne); hjørne = axialToOffset (hjørne); hvis (checkForOccuppancy (corner.x, corner.y)) return true;  returner falsk; 

Vi legger til movementVector til heltenposisjonsvektoren for å få den nye posisjonen til helten sprite senter. Deretter finner vi posisjonen til de fire hjørnene til helten og ser etter om de kolliderer. Hvis det ikke er kollisjoner, setter vi den nye stillingen til heltenes sprite. La oss se det i aksjon.

Vanligvis er denne typen friflytende bevegelse ikke tillatt i et nettbasert spill. Vanligvis flytter tegn fra fliser til fliser, det vil si flisesentre til flisesentre, basert på kommandoer eller trykk. Jeg stoler på at du kan finne løsningen ut av deg selv.

3. pathfinding

Så her er vi på temaet pathfinding, et veldig skummelt tema for noen. I mine tidligere opplæringsprogrammer har jeg aldri prøvd å skape nye pathfinding-løsninger, men alltid foretrukket å bruke lett tilgjengelige løsninger som er slagetestet. 

Denne gangen gjør jeg et unntak og vil gjenoppfinne hjulet, hovedsakelig fordi det er ulike spillmekanikker mulige, og ingen enkelt løsning vil være til nytte for alle. Så det er praktisk å vite hvordan det hele er gjort for å churn ut dine egne tilpassede løsninger for spillmekanikeren din. 

Den mest grunnleggende algoritmen som brukes til opplæring i nett er Dijkstras algoritme. Vi starter ved første node og beregner kostnadene ved å flytte til alle mulige nabo noder. Vi lukker den første noden og flytter til nabo knuten med den laveste kostnaden som er involvert. Dette gjentas for alle ikke-lukkede noder til vi når destinasjonen. En variant av dette er A * -algoritme, hvor vi også bruker en heuristisk i tillegg til kostnaden. 

En heuristisk brukes til å beregne den omtrentlige avstanden fra gjeldende knutepunkt til destinasjonsnoden. Som vi egentlig ikke kjenner banen, er denne avstandsberegningen alltid en tilnærming. Så en bedre heuristisk vil alltid gi en bedre bane. Når det er sagt, må den beste løsningen ikke være den som gir den beste banen, da vi må vurdere ressursbruken og ytelsen til algoritmen også, når alle beregningene må gjøres i sanntid eller en gang per oppdatering sløyfe. 

Den enkleste og enkleste heuristikken er Manhattan heuristisk eller Manhattan avstand. I et 2D-rutenett er dette faktisk avstanden mellom startknutepunktet og sluttknutepunktet som kråken flyr, eller antall blokker vi trenger å gå.

Heksagonal Manhattan Variant

For vårt sekskantede rutenett, må vi finne en variant for Manhattan-heuristikken for å tilnærme avstanden. Når vi går på sekskantede fliser, er ideen å finne antall fliser vi må gå over for å nå destinasjonen. La meg vise deg løsningen først. Vennligst flytt musen over det interaktive elementet nedenfor for å se hvor langt de andre fliser er fra flisen under musen.

I eksemplet ovenfor finner vi flisen under musen og finner avstanden til alle andre fliser fra den. Logikken er å finne forskjellen på Jeg og j aksial koordinater for begge fliser først, si di og dj. Finn de absolutte verdiene til disse forskjellene, absi og absj, som avstander er alltid positive. 

Vi merker at når begge deler di og dj er positive og når begge deler di og dj er negativ, avstanden er absi + absj. Når di og dj er av motsatte tegn, avstanden er større verdi blant absi og absj. Dette fører til den heuristiske beregningsfunksjonen getHeuristic som Nedenfor.

getHeuristic = funksjon (i, j) j = (j- (Math.floor (i / 2))); var di = jeg-this.originali; var dj = j-this.convertedj; var si = Math.sign (di); var sj = Math.sign (dj); var absi = di * si; var absj = dj * sj; hvis (si! = sj) this.heuristic = Math.max (absi, absj);  ellers this.heuristic = (absi + absj); 

En ting å legge merke til er at vi ikke vurderer om banen er virkelig walkable eller ikke; vi antar bare at det er walkable og angi avstandsverdien. 

Finne den sekskantede banen

La oss fortsette med å finne frem til vårt sekskantede rutenett med den nylig funnet heuristiske metoden. Som vi skal bruke rekursjon, blir det lettere å forstå når vi bryter sammen kjernelogikken i vår tilnærming. Hver sekskantet flis vil ha en heuristisk avstand og en kostnadsverdi tilknyttet den.

  • Vi har en rekursiv funksjon, sier findPath (fliser), som tar i en sekskantet flis, som er den nåværende flisen. I utgangspunktet vil dette være startflisen.
  • Hvis flisen er lik slutten flis, slutter rekursjonen og vi har funnet banen. Ellers fortsetter vi med beregningen.
  • Vi finner alle de walkable naboene til flisen. Vi vil løpe gjennom alle nabofliser og søke videre logikk til hver av dem, med mindre de er lukket.
  • Hvis en nabo ikke er tidligere besøkt og ikke lukket, finner vi avstanden til naboflisen til sluttflisen ved hjelp av vår heuristiske. Vi satte naboflisen koste til nåværende flis kostnad + 10. Vi satte naboflisen som besøkt. Vi satte naboflisen tidligere fliser som dagens fliser. Vi gjør dette for en tidligere besøkt nabo også hvis dagens flis kost + 10 er mindre enn naboen koster.
  • Vi beregner den totale kostnaden som summen av naboflisens kostnadsverdi og den heuristiske avstandsverdien. Blant alle naboene velger vi naboen som gir laveste totalpris og samtale findPath på naboflisen.
  • Vi stiller nåværende flis til lukket slik at det ikke blir vurdert lenger.
  • I noen tilfeller vil vi ikke finne noen fliser som tilfredsstiller forholdene, og så lukker vi nåværende flis, åpner forrige flis og gjør om igjen.

Det er en åpenbar feiltilstand i logikken når mer enn en flis tilfredsstiller forholdene. En bedre algoritme vil finne alle de forskjellige veiene og velge den med korteste lengden, men det gjør vi ikke her. Kontroller veiviseren i handlingen nedenfor.

For dette eksempelet beregner jeg naboene annerledes enn i Tetris-eksemplet. Når du bruker aksiale koordinater, har nabofliserne koordinater som er høyere eller lavere med en verdi på 1.

funksjon getNeighbors (i, j) // koordinatene er i aksial var tempArray = []; var axialPoint = ny Phaser.Point (i, j); var neighbourPoint = ny Phaser.Point (); neighbourPoint.x = axialPoint.x-1; // tr neighbourPoint.y = axialPoint.y; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x + 1; // bl neighbourPoint.y = axialPoint.y; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x; // l neighbourPoint.y = axialPoint.y-1; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x; // r neighbourPoint.y = axialPoint.y + 1; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x-1; // tr neighbourPoint.y = axialPoint.y + 1; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); neighbourPoint.x = axialPoint.x + 1; // bl neighbourPoint.y = axialPoint.y-1; populateNeighbor (neighbourPoint.x, neighbourPoint.y, tempArray); returnere tempArray; 

De findPath rekursiv funksjon er som nedenfor.

funn findPath (flis) // passerer i en hexTileNode hvis (Phaser.Point.equals (flis, endTile)) // suksess, destinasjon nådde console.log ('suksess'); // maler nå banen. paintPath (flis);  annet // finn alle naboer var naboer = getNeighbors (tile.originali, tile.convertedj); var newPt = ny Phaser.Point (); var hexTile; var totalCost = 0; var currentLowestCost = 100000; var nextTile; // finne heuristics og kostnad for alle naboer mens (naboer.lengde) newPt = naboer.shift (); hexTile = hexGrid.getByName ( "tile" + newPt.x + "_" + newPt.y); hvis (! hexTile.nodeClosed) // hvis node ikke allerede var beregnet hvis ((hexTile.nodeVisited && (tile.cost + 10)

Det kan kreve flere og flere lesninger for å forstå hva som skjer, men tro meg, det er verdt innsatsen. Dette er bare en veldig grunnleggende løsning og kan forbedres mye. For å flytte tegnet langs den beregnede banen, kan du se på isometrisk banen som følger med opplæringen. 

Markeringen av banen er gjort ved hjelp av en annen enkel rekursiv funksjon, paintPath (fliser), som først kalles med sluttflisen. Vi markerer bare previousNode av flisen hvis den er til stede.

funksjon paintPath (flis) tile.markDirty (); hvis (tile.previousNode! == null) paintPath (tile.previousNode); 

Konklusjon

Ved hjelp av alle de tre sekskantede opplæringsprogrammene jeg har delt, bør du kunne komme i gang med ditt neste fantastiske sekskantede fliserbaserte spill. 

Vær oppmerksom på at det også finnes andre tilnærminger, og det er mye mer å lese der ute hvis du er opptatt av det. Vennligst gi meg beskjed gjennom kommentarene hvis du trenger noe mer å bli utforsket i forhold til sekskantede fliserbaserte spill.