Opprette en sekskantet Minesweeper

Hva du skal skape

I denne opplæringen vil jeg prøve å introdusere den interessante verdenen av sekskantede fliserbaserte spill ved hjelp av de enkleste tilnærmingene. Du lærer hvordan du konverterer en todimensjonal array-data til et tilsvarende sekskantnivånivå på skjermen og omvendt. Ved å bruke informasjonen som er oppnådd, vil vi lage et sekskantet minesweeper-spill i to forskjellige sekskantede layouter.

Dette vil komme i gang med å utforske enkle sekskantede brettspill og puslespill og vil være et godt utgangspunkt for å lære mer kompliserte tilnærminger som de aksiale eller kubiske sekskantede koordinatsystemene.

1. Sekskantede fliser og layouter

I dagens generasjon av casual gaming ser vi ikke mange spill som bruker en sekskantet fliserbasert tilnærming. De vi kommer over er vanligvis puslespill, brettspill eller strategispill. Også de fleste av våre krav er oppfylt av kvadratnettet tilnærming eller isometrisk tilnærming. Dette fører til det naturlige spørsmålet: "Hvorfor trenger vi en annen og åpenbart komplisert sekskantet tilnærming?" La oss finne det ut.

Fordeler ved den sekskantede tilnærmingen

Så hva gjør den sekskantede fliserbaserte tilnærmingen relevant, siden vi allerede har andre tilnærminger lært og perfeksjonert? La meg liste noen grunner.

  • Mindre antall nabobilder: Sammenlignet med et kvadratisk rutenett, som vil ha åtte nabobilder, vil en sekskantet flis bare ha seks naboer. Dette reduserer beregninger for kompliserte algoritmer.
  • Alle nabofliser er i samme avstand: For et firkantet rutenett er de fire diagonale naboene langt unna sammenlignet med horisontale eller vertikale naboer. Naboer som ligger på like avstander er en stor lettelse når vi beregner heuristikk og reduserer overhead ved å bruke to forskjellige metoder for å beregne noe avhengig av naboen.
  • Unikhet: I disse dager kommer millioner av tilfeldige spill ut og konkurrerer om spillerens tid. Gode ​​spill er ikke i stand til å få et publikum, og en ting som kan garanteres å ta tak i en spillers oppmerksomhet, er unikt. Et spill som bruker en sekskantet tilnærming, vil visuelt skille seg ut fra resten, og spillet vil virke mer interessant for en mengde som kjeder seg med alle de konvensjonelle spillmekanikkene. 

Jeg vil si at den siste grunnen skal være nok til at du skal mestre denne nye tilnærmingen. Å legge til det unike spillelementet over spilllogikken din, kan gjøre hele forskjellen og gjøre deg i stand til å lage et godt spill. 

De andre grunnene er rent teknisk og vil bare tre i kraft når du har problemer med kompliserte algoritmer eller større fliser. Det er også mange andre aspekter som kan listes som fordeler med sekskantet tilnærming, men de fleste av dem vil avhenge av spillerens personlige interesse.

oppsett

En sekskant er et polygon med seks sider, og en sekskant med alle sider som har samme lengde kalles en vanlig sekskant. For teoribaserte formål vil vi vurdere våre sekskantede fliser å være vanlige heksagoner, men de kan bli squashed eller forlenget i praksis. 

Det interessante er at en sekskant kan plasseres på to forskjellige måter: Spisse hjørner kan justeres vertikalt eller horisontalt. Når spisse topper er justert vertikalt, kalles det a horisontal layout, og når de er justert horisontalt, kalles det a vertikal oppsett. Du kan tenke at navnene er misnomerer med hensyn til forklaringen som er oppgitt. Dette er ikke tilfellet som navngivningen ikke er ferdig basert på spisse hjørner, men måten et flis av fliser blir lagt ut. Bildet under viser de forskjellige flisjusteringene og de tilsvarende layoutene.

Valg av layout avhenger helt av spillets visuelle og gameplay. Men ditt valg slutter ikke her, da hver av disse layoutene kunne implementeres på to forskjellige måter.

La oss vurdere et horisontalt sekskantet gridoppsett. Alternative rader i rutenettet må være horisontalt kompensert av hexTileWidth / 2. Dette betyr at vi kan velge å kompensere enten de ulike radene eller de jevne radene. Hvis vi også viser tilsvarende rad, kolonne verdier, vil disse variantene se ut som bildet nedenfor.

På samme måte kan det vertikale oppsettet implementeres i to variasjoner mens alternative kolonner erstattes av hexTileHeight / 2 som vist under.

2. Implementere sekskantede layouter

Fra og med, vennligst begynn å referere til kilden som følger med denne opplæringen for bedre forståelse

Bildene over, med rader og kolonner som vises, gjør det lettere å visualisere en direkte korrelasjon med et todimensjonalt array som lagrer nivådataene. La oss si at vi har en enkel todimensjonal matrise levelData som Nedenfor.

var levelData = [[0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0], [0,0,0,0,0 ], [0,0,0,0,0]]

For å gjøre det lettere å visualisere, vil jeg vise det påtatte resultatet her både i vertikale og horisontale variasjoner.

La oss starte med horisontal layout, som er bildet på venstre side. I hver rad, hvis de tas individuelt, er nabobladerne horisontalt kompensert av hexTileWidth. Alternative rader er horisontalt kompensert med en verdi på hexTileWidth / 2. Den vertikale høydeforskjellen mellom hver rad er hexTileHeight * på 3/4

For å forstå hvordan vi kom fram til en slik verdi for høydeforskyvningen, må vi vurdere det faktum at topp og bunn trekantede deler av en horisontalt lagt ut sekskant er nøyaktig hexTileHeight / 4

Dette betyr at sekskanten har en rektangulær hexTileHeight / 2 del i midten, en trekantet hexTileHeight / 4 porsjon på toppen og en omvendt trekantet hexTileHeight / 4 porsjon på bunnen. Denne informasjonen er nok til å opprette koden som er nødvendig for å legge ut sekskantet rutenett på skjermen.

var verticalOffset = hexTileHeight * 3/4; var horizontalOffset = hexTileWidth; var startX; var starty; var startXInit = hexTileWidth / 2; var startYInit = hexTileHeight / 2; var hexTile; for (var i = 0; i < levelData.length; i++)  if(i%2!==0) startX=2*startXInit; else startX=startXInit;  startY=startYInit+(i*verticalOffset); for (var j = 0; j < levelData[0].length; j++)  if(levelData[i][j]!=-1) hexTile= new HexTile(game, startX, startY, 'hex',false,i,j,levelData[i][j]); hexGrid.add(hexTile);  startX+=horizontalOffset;  

Med HexTile prototype, har jeg lagt til noen ekstra funksjonaliteter til Phaser.Sprite prototype som gjør at den kan vise Jeg og j verdier. Koden legger i hovedsak en ny sekskantet flis Spritestartx og starty. Denne koden kan endres for å vise utjevningsvarianten bare ved å fjerne en operatør i hvis tilstand slik: if (i% 2 === 0).

For en vertikal layout (bildet på høyre halvdel) er naboblader i hver kolonne vertikalt kompensert av hexTileHeight. Hver alternativ kolonne er vertikalt kompensert av hexTileHeight / 2. Ved å bruke logikken som vi søkte for vertikal offset for det horisontale oppsettet, kan vi se at den horisontale forskyvningen for det vertikale oppsettet mellom nabofliser på rad er hexTileWidth * på 3/4. Tilsvarende kode er under.

var verticalOffset = hexTileHeight; var horizontalOffset = hexTileWidth * 3/4; var startX; var starty; var startXInit = hexTileWidth / 2; var startYInit = hexTileHeight / 2; var hexTile; for (var i = 0; i < levelData.length; i++)  startX=startXInit; startY=2*startYInit+(i*verticalOffset); for (var j = 0; j < levelData[0].length; j++)  if(j%2!==0) startY=startY+startYInit; else startY=startY-startYInit;  if(levelData[i][j]!=-1) hexTile= new HexTile(game, startX, startY, 'hex', true,i,j,levelData[i][j]); hexGrid.add(hexTile);  startX+=horizontalOffset;  

På samme måte som med det horisontale oppsettet, kan vi bytte til even-off varianten bare ved å fjerne ! operatør i toppen hvis tilstand. Jeg bruker en Phaser Gruppe å samle alle hexTiles oppkalt hexGrid. For enkelhets skyld bruker jeg midtpunktet på sekskantet fliser bildet som et anker, ellers måtte vi også vurdere bildesettene. 

En ting å merke seg er at flisens bredde og flisens høydeverdier i det horisontale oppsettet ikke er lik flisens bredde og flisens høydeverdier i vertikal layout. Men når du bruker det samme bildet for begge layoutene, kan vi bare rotere flisbildet 90 grader og bytte verdiene av flisbredde og flisens høyde.

3. Finne Array-indeksen av en sekskantet flis

Plasseringen til skjermplasseringslogikken var interessant grei, men omvendt er ikke så lett. Tenk på at vi må finne arrayindeksen for den sekskantede flisen som vi har tappet på. Koden for å oppnå dette er ikke pen, og det er vanligvis ankommet av noen forsøk og feil. 

Hvis vi vurderer det horisontale oppsettet, kan det virke som om den midterste rektangulære delen av sekskantet flis lett kan hjelpe oss å finne ut j verdi som det bare handler om å dele x verdi av hexTileWidth og tar heltallverdien. Men med mindre vi kjenner Jeg verdi, vi vet ikke om vi er på en merkelig eller jevn rad. En omtrentlig verdi av Jeg kan bli funnet ved å dele y-verdien med hexTileHeight * på 3/4

Nå kommer de kompliserte delene av sekskantet flis: topp og bunn trekantede deler. Bildet nedenfor hjelper oss med å forstå problemet ved hånden.

Regionene 2, 3, 5, 6, 8 og 9 danner sammen en flis. Den mest kompliserte delen er å finne om den tappede posisjonen er i 1/2 eller 3/4 eller 7/8 eller 9/10. For dette må vi vurdere alle de enkelte trekantede områdene og sjekke mot dem ved hjelp av skråkantens skråning. 

Denne skråningen kan bli funnet fra høyden og bredden av hver triangulær region, som henholdsvis er hexTileHeight / 4 og hexTileWidth / 2. La meg vise deg hvilken funksjon som gjør dette.

funksjon findHexTile () var pos = game.input.activePointer.position; pos.x- = hexGrid.x; pos.y- = hexGrid.y; var xVal = Math.floor ((pos.x) / hexTileWidth); var yVal = Math.floor ((pos.y) / (hexTileHeight * 3/4)); var dX = (pos.x)% hexTileWidth; var dY = (pos.y)% (hexTileHeight * 3/4); var helling = (hexTileHeight / 4) / (hexTileWidth / 2); var caldY = dX * helling; var delta = hexTileHeight / 4-caldY; hvis (yVal% 2 === 0) // korreksjon må skje i trekantede deler og forskyvningsrader hvis (Math.abs (delta)> dY) hvis (delta> 0) // merkelig rad nederst til høyre halv xVal--; yVal--;  ellers // odd rad nederst venstre halvdel yVal--;  ellers hvis (dX> hexTileWidth / 2) // tilgjengelige verdier virker ikke for jevn rad nederst til høyre hvis (dY<((hexTileHeight/2)-caldY))//even row bottom right half yVal--;  else if(dY>caldY) // odd rad øverst til høyre og midt høyre halvdel xVal--;  ellers // selv rad nederst venstre halvdel yVal--;  pos.x = yVal; pos.y = XVerd; returnere pos; 

Først finner vi XVerdi og yVal På samme måte som vi ville gjøre for et firkantet rutenett. Da finner vi de resterende horisontale (dX) og vertikal (dY) verdier etter fjerning av flis multiplikator offset. Ved å bruke disse verdiene, prøver vi å finne ut om poenget er innenfor noen av de kompliserte triangulære områdene. 

Hvis funnet, gjør vi tilsvarende endringer til de opprinnelige verdiene for XVerdi og yVal. Som jeg har sagt tidligere, er koden ikke pen og ikke grei. Den enkleste måten å forstå dette ville være å ringe findHexTile på musen flytte, og deretter sette console.log innenfor hver av disse forholdene og flytte musen over ulike regioner innenfor en sekskantet flis. På denne måten kan du se hvordan hver intra-hexagonal region håndteres.

Kodendringene for vertikal layout vises nedenfor.

funksjon findHexTile () var pos = game.input.activePointer.position; pos.x- = hexGrid.x; pos.y- = hexGrid.y; var xVal = Math.floor ((pos.x) / (hexTileWidth * 3/4)); var yVal = Math.floor ((pos.y) / (hexTileHeight)); var dX = (pos.x)% (hexTileWidth * 3/4); var dY = (pos.y)% (hexTileHeight); var helling = (hexTileHeight / 2) / (hexTileWidth / 4); var caldX = dY / skråning; var delta = hexTileWidth / 4-caldX; hvis (xVal% 2 === 0) hvis (dX> Math.abs (delta)) // selv igjen annet // ulikt rett hvis (delta> 0) // ulikt høyre bunn xVal--; yVal--;  else // odd right top xVal--;  ellers hvis (delta> 0) hvis (dX

4. Finne naboer

Nå som vi har funnet flisen vi har tappet på, la oss finne alle seks naboblader. Dette er et veldig enkelt problem å løse når vi visuelt analyserer rutenettet. La oss se på det horisontale oppsettet.

Bildet ovenfor viser merkelig og jevn rader av et horisontalt lagt ut sekskantet rutenett når en midtflis har verdien av 0 for begge Jeg og j. Fra bildet blir det klart at hvis rækken er merkelig, så for en flis på i, j naboene er jeg, j-1, i-1, j-1, i-1, j, i, j + 1, i + 1, j, og i + 1, j-1. Når raden er jevn, så for en flis på i, j naboene er jeg, j-1i-1, ji-1, j + 1i, j + 1i + 1, j + 1, og i + 1, j. Dette kan enkelt beregnes manuelt.

La oss analysere et lignende bilde for det merkelige og jevne kolonner av et vertikalt rettet sekskantet rutenett.

Når vi har en merkelig kolonne, en flis på i, j vil ha i, j-1i-1, j-1, i-1, j, i-1, j + 1, i, j + 1, og i + 1, j som naboer. På samme måte, for en jevn kolonne, er naboene i + 1, j-1, i, j-1, i-1, j, i, j + 1, i + 1, j + 1, og i + 1, j

5. Sekskantet Minesweeper

Med ovennevnte kunnskap kan vi prøve å lage et sekskantet minesweeper-spill i de to forskjellige layoutene. La oss bryte ned funksjonene til et minesweeper-spill.

  1. Det vil være N antall gruver gjemt inne i rutenettet.
  2. Hvis vi trykker på en flis med en gruve, er spillet over.
  3. Hvis vi trykker på en flis som har en nærliggende gruve, vil den vise antall gruver umiddelbart rundt den.
  4. Hvis vi trykker på en gruve uten nærliggende gruver, vil det føre til avsløring av alle de tilkoblede fliser som ikke har gruver.
  5. Vi kan trykke og hold for å markere en flis som en gruve.
  6. Spillet er ferdig når vi avslører alle fliser uten gruver.

Vi kan enkelt lagre en verdi i levelData array for å indikere en gruve. Den samme metoden kan brukes til å fylle verdien av nærliggende gruver på den nærliggende flisens arrayindeks. 

Ved start av spillet vil vi tilfeldigvis fylle levelData array med N antall gruver. Etter dette vil vi oppdatere verdiene for alle nabofliser. Vi vil bruke en rekursiv metode for å kjede avsløre alle tilkoblede tomme fliser når spilleren tapper på en flis som ikke har en gruve som nabo.

Nivådata

Vi må lage et fint utseende sekskantet rutenett, som vist på bildet nedenfor.

Dette kan gjøres ved kun å vise en del av levelData array. Hvis vi bruker -1 som verdien for en ikke-brukbar fliser og 0 som verdien for en brukbar flis, så vår levelData for å oppnå det ovennevnte resultatet vil se slik ut.

// horisontalt flisformet nivå var levelData = [[-1, -1, -1,0,0,0,0,0,0, -1, -1, -1], [-1, -1 , 0,0,0,0,0,0,0,0, -1, -1, -1], [-1, -1,0,0,0,0,0,0,0, 0, -1, -1], [-1,0,0,0,0,0,0,0,0, -1, -1], [-1,0,0,0, 0,0,0,0,0,0,0,0, -1], [0,0,0,0,0,0,0,0,0,0,0, -1], [ 0,0,0,0,0,0,0,0,0,0,0,0,0, [0,0,0,0,0,0,0,0,0,0,0, 0, -1], [-1,0,0,0,0,0,0,0,0,0,0, -1], [-1,0,0,0,0,0, 0,0,0,0,0, -1, -1], [-1, -1,0,0,0,0,0,0,0,0-1,1], [ -1, -1,0,0,0,0,0,0,0, -1, -1, -1], [-1, -1, -1,0,0,0,0, 0,0,0, -1, -1, -1]];

Mens vi løp gjennom matrisen, ville vi bare legge til sekskantede fliser når levelData har en verdi på 0. For den vertikale tilpasningen er det samme levelData kan brukes, men vi trenger å transponere matrisen. Her er en grei metode som kan gjøre dette for deg.

levelData = transponert (levelData); // funksjon transponere (a) return Object.keys (a [0]). Kart (funksjon (c) return a.map (funksjon (r) return r [c];);); 

Legge til miner og oppdatere naboer

Som standard, vår levelData har bare to verdier, -1 og 0, som vi bare ville bruke området med 0. For å indikere at en flis inneholder en gruve, kan vi bruke verdien av 10

En blank sekskantet flis kan ha maksimalt seks miner i nærheten av den som den har seks nabobilder. Vi kan lagre denne informasjonen også i levelData når vi har lagt til alle gruvene. I hovedsak a levelData indeksen har en verdi på 10 har en gruve, og hvis den inneholder noen verdier fra 0 til 6, som indikerer antall nærliggende gruver. Etter å ha fylt gruver og oppdaterer naboer, hvis et arrayelement fortsatt er 0, det indikerer at det er en tom flise uten noen nærliggende gruver. 

Vi kan bruke følgende metoder til vårt formål.

funksjon addMines () var tileType = 0; var tempArray = []; var newPt = ny Phaser.Point (); for (var i = 0; i < levelData.length; i++)  for (var j = 0; j < levelData[0].length; j++)  tileType=levelData[i][j]; if(tileType===0) newPt=new Phaser.Point(); newPt.x=i; newPt.y=j; tempArray.push(newPt);    for (var i = 0; i < numMines; i++)  newPt=Phaser.ArrayUtils.removeRandomItem(tempArray); levelData[newPt.x][newPt.y]=10;//10 is mine updateNeighbors(newPt.x,newPt.y);   function updateNeighbors(i,j)//update neighbors around this mine var tileType=0; var tempArray=getNeighbors(i,j); var tmpPt; for (var k = 0; k < tempArray.length; k++)  tmpPt=tempArray[k]; tileType=levelData[tmpPt.x][tmpPt.y]; levelData[tmpPt.x][tmpPt.y]=tileType+1;  

For hver min lagt til addMines, vi øker arrayverdien som er lagret i alle naboene. De getNeighbors Metoden vil ikke returnere en flis som ligger utenfor vårt effektive område eller om det inneholder en gruve.

Trykk på Logikk

Når spilleren tapper på en flis, må vi finne det tilsvarende arrayelementet ved hjelp av findHexTile metode forklart tidligere. Hvis flisindeksen er innenfor vårt effektive område, sammenligner vi bare verdien på arrayindeksen for å finne om det er en min eller en blank flis.

funksjon onTap () var tile = findHexTile (); hvis (! checkforBoundary (tile.x, tile.y)) if (checkForObout (tile.x, tile.y)) if (levelData [tile.x] [tile.y] == 10) // konsoll .log ( 'bom'); var hexTile = hexGrid.getByName ("fliser" + tile.x + "_" + tile.y); hvis (! hexTile.revealed) hexTile.reveal (); // spill over else var hexTile = hexGrid.getByName ("fliser" + tile.x + "_" + tile.y); hvis (! hexTile.revealed) if (levelData [tile.x] [tile.y] === 0) //console.log('recursive reveal '); recursiveReveal (tile.x, tile.y);  ellers //console.log('reveal '); hexTile.reveal (); revealedTiles ++;  infoTxt.text = 'found' + revealedTiles + 'of' + blankTiles; 

Vi holder orden på det totale antall tomme fliser ved hjelp av variabelen blankTiles og antall fliser som ble avslørt ved bruk av revealedTiles. Når de er like, har vi vunnet spillet. 

Når vi klikker på en flis med en array verdi på 0, Vi må rekursivt avsløre regionen med alle de tilkoblede, flislagte fliser. Dette gjøres av funksjonen recursiveReveal, som mottar flisindeksene til den tappede flisen.

funksjon recursiveReveal (i, j) var newPt = new Phaser.Point (i, j); var hexTile; var tempArray = [newPt]; var naboer; mens (tempArray.length) newPt = tempArray [0]; var naboer = getNeighbors (newPt.x, newPt.y); mens (naboer.lengde) newPt = naboer.shift (); hexTile = hexGrid.getByName ( "tile" + newPt.x + "_" + newPt.y); hvis (! hexTile.revealed) hexTile.reveal (); revealedTiles ++; hvis (levelData [newPt.x] [newPt.y] === 0) tempArray.push (newPt);  newPt = tempArray.shift (); // det virket som et punkt uten nabo noen ganger unngår iterasjonen uten å bli avslørt, ta den her hexTile = hexGrid.getByName ("flis" + newPt.x + "_" + newPt.y ); hvis (! hexTile.revealed) hexTile.reveal (); revealedTiles ++; 

I denne funksjonen finner vi naboene til hver flis og avslører den flisens verdi, samtidig som vi legger nabofliser til en matrise. Vi fortsetter å gjenta dette med det neste elementet i matrisen til matrisen er tom. Rekursjonen stopper når vi møter matriseelementer som inneholder en gruve, som sikres av det faktum at getNeighbors vil ikke returnere en flis med en gruve.

Merking og avsløring av fliser

Du må ha lagt merke til at jeg bruker hexTile.reveal (), som er gjort mulig ved å opprette en HexTile prototype som holder de fleste attributter relatert til vår sekskantede fliser. Jeg bruker avsløre funksjon for å vise teglverdietekst og angi flisens farge. På samme måte, toggleMark funksjon brukes til å markere flisen som en gruve når vi trykker og holder nede. HexTile har også a avslørt attributt som sporer om det er tappet og avslørt eller ikke.

HexTile.prototype.reveal = function () this.tileTag.visible = true; this.revealed = true; hvis (this.type == 10) this.tint = '0xcc0000';  else this.tint = '0x00cc00';  HexTile.prototype.toggleMark = function () if (this.marked) this.marked = false; this.tint = '0xFFFFFF';  annet this.marked = true; this.tint = '0x0000cc'; 

Sjekk ut den sekskantede minesveiper med horisontal orientering under. Trykk for å avsløre fliser og trykk på for å markere gruver. Det er ikke noe spill som nå, men hvis du avslører en verdi av 10, da er det hasta la vista baby!

Endringer for den vertikale versjonen

Som jeg bruker det samme bildet av sekskantet fliser for begge retninger, roterer jeg Sprite for den vertikale tilpasningen. Koden nedenfor i HexTile prototype gjør dette.

hvis (isVertical) this.rotation = Math.PI / 2; 

Minesweeper-logikken forblir den samme for det vertikalt justerte sekskantede rutenettet med forskjellen for findHextile og getNeighbors logikk som nå må imøtekomme justeringsforskjellen. Som nevnt tidligere, må vi også bruke transponeringen av nivået array med tilsvarende layout loop.

Sjekk ut den vertikale versjonen nedenfor.

Resten av koden i kilden er enkel og grei. Jeg vil gjerne prøve å legge til manglende gjenoppstart, spillvinning og spill over funksjonalitet.

Konklusjon

Denne tilnærmingen til et hexagonalt fliserbasert spill ved hjelp av et todimensjonalt array er mer av en lekmanns tilnærming. Flere interessante og funksjonelle tilnærminger innebærer å endre koordinatsystemet til forskjellige typer ved å bruke ligninger. 

De viktigste er aksiale koordinater og kubiske koordinater. Det vil bli en oppfølgingsveiledningsserie som drøfter disse tilnærmingene. I mellomtiden vil jeg anbefale å lese Amits utrolig grundige artikkel om sekskantede grid.