Introducere în coordonatele axiale pentru jocurile bazate pe tigla hexagonale

Ce veți crea

Abordarea pe bază de țiglă hexagonală de bază explicată în tutorialul hexagonal minesweeper obține lucrul făcut, dar nu este foarte eficient. Utilizează conversia directă de la datele de nivel bazate pe matrice bidimensionale și de la coordonatele ecranului, ceea ce îl complică în mod inutil de a determina dale tamponate. 

De asemenea, necesitatea de a folosi logica diferită în funcție de rândul / coloana impare sau par ial a unei plăci nu este convenabilă. Această serie de tutori explorează sistemele alternative de coordonate ale ecranului, care ar putea fi utilizate pentru a ușura logica și a face lucrurile mai convenabile. V-aș sugera cu insistență să citiți tutorialul hexagonal minesweeper înainte de a merge mai departe cu acest tutorial, deoarece acesta explică randarea rețelei bazată pe o matrice bidimensională.

1. Coordonate axiale

Metoda implicită utilizată pentru coordonatele ecranului în tutorialul hexagonal minesweeper se numește abordarea coordonatelor offset. Acest lucru se datorează faptului că rândurile sau coloanele alternative sunt compensate de o valoare în timp ce se aliniază grila hexagonală. 

Pentru a vă reîmprospăta memoria, consultați imaginea de mai jos, care arată alinierea orizontală cu valorile coordonatelor offset afișate.

În imaginea de mai sus, un rând cu același eu valoarea este evidențiată în roșu și o coloană cu același j valoarea este evidențiată în verde. Pentru a face totul simplu, nu vom discuta variantele ciudate și chiar echilibrate, ambele fiind moduri diferite de a obține același rezultat. 

Permiteți-mi să introduc o alternativă mai bună a coordonatelor pe ecran, coordonata axială. Conversia unei coordonate offset la o variantă axială este foarte simplă. eu valoarea rămâne aceeași, dar j valoarea este convertită folosind formula axialJ = i - etaj (j / 2). O metodă simplă poate fi folosită pentru a converti un offset Phaser.Point la varianta sa axială, după cum se arată mai jos.

funcția offsetToAxial (offsetPoint) offsetPoint.y = (offsetPoint.y- (Math.floor (offsetPoint.x / 2)); returnează offsetPoint; 

Conversia inversă ar fi prezentată mai jos.

funcția axialToOffset (axialPoint) axialPoint.y = (axialPoint.y + (Math.floor (axialPoint.x / 2)); retur axialPoint; 

Aici X valoarea este eu valoare, și y valoarea este j valoare pentru matricea bidimensională. După conversie, noile valori ar arăta ca imaginea de mai jos.

Observați că linia verde în cazul în care j valoarea rămâne aceeași nu mai zigzagă, ci mai degrabă este acum diagonală față de grila hexagonală.

Pentru grila hexagonală aliniată vertical, coordonatele de offset sunt afișate în imaginea de mai jos.

Conversia la coordonatele axiale urmează aceleași ecuații, cu diferența că păstrăm j apreciați aceeași valoare și modificați-o eu valoare. Metoda de mai jos arată conversia.

funcția offsetToAxial (offsetPoint) offsetPoint.x = (offsetPoint.x- (Math.floor (offsetPoint.y / 2))); returnează offsetPoint; 

Rezultatul este după cum se arată mai jos.

Înainte de a utiliza noile coordonate pentru a rezolva problemele, permiteți-mi să vă prezint rapid o altă alternativă de coordonate a ecranului: coordonatele cubului.

2. Cubul sau coordonatele cubice

Îndreptarea zig-zagului în sine a rezolvat potențial majoritatea inconvenientelor pe care le-am avut cu sistemul de coordonate offset. Cubul sau coordonatele cubice ne-ar ajuta în simplificarea logicii complicate cum ar fi euristica sau rotirea în jurul unei celule hexagonale. 

După cum probabil ați ghicit din denumire, sistemul cubic are trei valori. Al treilea k sau z valoarea este derivată din ecuație x + y + z = 0, Unde X și y sunt coordonatele axiale. Aceasta ne conduce la această metodă simplă pentru a calcula z valoare.

funcția calculateCubicZ (axialPoint) return -axialPoint.x-axialPoint.y; 

Ecuația x + y + z = 0 este de fapt un plan 3D care trece prin diagonala unei grila cuburi tridimensionala. Afișarea tuturor celor trei valori pentru grila va duce la următoarele imagini pentru diferitele alinieri hexagonale.

Linia albastră indică plăcile în care se află z valoarea rămâne aceeași. 

3. Avantajele noului sistem de coordonate

S-ar putea să te întrebi cum ne ajută aceste noi sisteme de coordonate cu logică hexagonală. Voi explica câteva avantaje înainte de a trece la crearea unui Tetris hexagonal folosind noile noastre cunoștințe.

Circulaţie

Să luăm în considerare țigla de mijloc din imaginea de mai sus, care are valori de coordonate cubice de 3,6, -9. Am observat că o valoare de coordonate rămâne aceeași pentru plăcile de pe liniile colorate. Mai mult, vedem că coordonatele rămase fie cresc sau scad cu 1 în timp ce urmărește oricare dintre liniile colorate. De exemplu, dacă X valoarea rămâne aceeași și y valoarea crește cu 1 de-a lungul unei direcții, z valoarea scade cu 1 pentru a satisface ecuația noastră de guvernare x + y + z = 0. Această caracteristică facilitează mișcarea de control. Vom folosi acest lucru în a doua parte a seriei.

Vecini

Prin aceeași logică, este ușor să găsim vecinii pentru țiglă x, y, z. Prin păstrarea X la fel, avem doi vecini diagonali, x, y-1, z + 1 și x, y + 1, z-1. Dacă păstrăm același lucru, obținem doi vecini verticali, x-1, y, z + 1 și x + 1, y, z-1. Prin păstrarea z, vom obține cei doi vecini diagonali rămași, x + 1, y-1, z și x-1, y + 1, z. Imaginea de mai jos ilustrează acest lucru pentru o țiglă de la origine.

Este mult mai ușor acum că nu trebuie să folosim logică diferită bazată pe rânduri sau coloane ciudate sau ciudate.

Mutarea în jurul unei plăci

Un lucru interesant de observat în imaginea de mai sus este un fel de simetrie ciclică pentru toate plăcile din jurul plăcii roșii. Dacă luăm coordonatele oricărei plăci vecine, coordonatele plăcii vecine imediate pot fi obținute prin deplasarea valorilor coordonatelor la stânga sau la dreapta și apoi înmulțirea cu -1. 

De exemplu, vecinul de sus are o valoare de -1,0,1, care pe dreapta rotativă devine o dată 1, -1,0 și după înmulțirea cu -1 devine -1,1,0, care este coordonatul vecinului drept. Rotirea stânga și înmulțirea cu -1 randamente 0, -1,1, care este coordonatul vecinului stâng. Repetând acest lucru, putem sărim între toate plăcile vecine din jurul plăcii centrale. Aceasta este o caracteristică foarte interesantă care ar putea ajuta la logică și algoritmi. 

Rețineți că acest lucru se întâmplă numai datorită faptului că placa de mijloc este considerată a fi la origine. Am putea face cu ușurință orice țiglă x, y, z să fie la origine scăzând valorile  X, y și z din ea și toate celelalte plăci.

Euristici

Calculul euristicilor eficiente este esențial atunci când vine vorba de algoritmi de căutare de tip pathfinding sau de algoritmi asemănători. Coordonatele cubice facilitează găsirea unor euristici simple pentru rețelele hexagonale din cauza aspectelor menționate mai sus. Vom discuta acest lucru în detaliu în a doua parte a acestei serii.

Acestea sunt câteva dintre avantajele noului sistem de coordonate. Am putea folosi o combinație a diferitelor sisteme de coordonate în implementările noastre practice. De exemplu, matricea bidimensională este încă cea mai bună modalitate de a salva datele de nivel, ale căror coordonate sunt coordonatele offset. 

Să încercăm să creăm o versiune hexagonală a celebrului joc Tetris folosind aceste noi cunoștințe.

4. Crearea unui Tetris hexagonal

Am jucat cu toții Tetris, iar dacă sunteți un dezvoltator de jocuri, este posibil să vă creați propria versiune. Tetris este unul dintre cele mai simple jocuri pe bază de țiglă, pe care le puteți implementa, în afară de tic tac toe sau dame, folosind o matrice simplă bidimensională. Să prezentăm mai întâi caracteristicile Tetris.

  • Începe cu o grilă goală bidimensională.
  • Diferite blocuri apar în partea de sus și se deplasează în jos o singură țiglă, la un moment dat până când ajung în partea de jos.
  • Odată ce ajung în partea de jos, se cimentă acolo sau devin non-interactive. Practic, ei devin parte a rețelei.
  • În timp ce coborâm, blocul poate fi mutat lateral, rotit în sensul acelor de ceasornic / în sens invers acelor de ceasornic, și a scăzut.
  • Obiectivul este de a umple toate plăcile în orice rând, pe care dispare întregul rând, prăbușind restul grilajului umplut pe el.
  • Jocul se termină atunci când nu mai există plăci libere în partea superioară pentru ca un nou bloc să intre în grila.

Reprezentarea diferitelor blocuri

Deoarece jocul are blocuri care coboară vertical, vom folosi o grilă hexagonală aliniată vertical. Aceasta înseamnă că mișcarea în lateral îi va face să se miște în mod zig-zag. Un rând complet în rețea constă dintr-un set de plăci în ordine zig-zag. Din acest moment, puteți începe să vă referiți la codul sursă furnizat împreună cu acest tutorial. 

Datele de nivel sunt stocate într-o matrice bidimensională denumită levelData, iar redarea se face folosind coordonatele offset, așa cum se explică în tutorialul hexagonal minesweeper. Consultați-o dacă aveți dificultăți în a urma codul. 

Elementul interactiv din secțiunea următoare arată diferitele blocuri pe care le vom folosi. Există încă un bloc suplimentar, care constă din trei plăci pline aliniate vertical ca un stâlp. BlockData este folosit pentru a crea diferite blocuri. 

funcția 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; 

Un șablon blanc bloc este un set de șapte dale formate dintr-o țiglă mijlocie înconjurată de cei șase vecini. Pentru orice bloc Tetris, țigla de mijloc este întotdeauna umplută, indicată cu o valoare de 1, în timp ce o țiglă goală ar fi notată cu o valoare de 0. Diferitele blocuri sunt create prin popularea dalelor BlockData ca mai jos.

var bloc1 = blocData nou (1,1,0,0,0,1); var bloc2 = blocData nou (0,1,0,0,0,1); var bloc3 = blocData nou (1,1,0,0,0,0); var bloc4 = blocData nou (1,1,0,1,0,0); var bloc5 = blocData nou (1,0,0,1,0,1); var bloc6 = blocData nou (0,1,1,0,1,1); var bloc7 = blocData nou (1,0,0,1,0,0);

Avem un total de șapte blocuri diferite.

Rotirea blocurilor

Permiteți-mi să vă arăt cum se rotesc blocurile folosind elementul interactiv de mai jos. Atingeți și mențineți apăsat pentru a roti blocurile și apăsați X pentru a schimba direcția de rotație.

Pentru a roti blocul, trebuie să găsim toate plăcile care au o valoare de 1, setați valoarea la 0, rotiți o dată în jurul țiglei de mijloc pentru a găsi țigla vecină și setați valoarea acesteia 1. Pentru a roti o țiglă în jurul unei alte plăci, putem folosi logica explicată în se deplasează în jurul unei plăci de mai sus. Sosim la metoda de mai jos pentru acest scop.

(), pentru a obține o valoare maximă a valorii anchorTileZ = calculateCubicZ (tileToRotate); // convertiți în axial var tileToRotateZ = calculateCubicZ (tileToRotate); // găsiți valoarea z anchorTile = offsetToAxial (anchorTile); anchorTile); / / find z value tileToRotate.x = tileToRotate.x-anchorTile.x; // găsi x diferență tileToRotate.y = tileToRotate.y-anchorTile.y; // găsi y diferență tileToRotateZ = tileToRotateZ-anchorTileZ; // găsiți diferența var punctArr = [tileToRotate.x, tileToRotate.y, tileToRotateZ]; // arborele populate pentru a roti pointArr = arrayRotate (pointArr, clockWise); // rotire array, true pentru turaToRotate.x = (- 1 * pointArr [0]) + anchorTile.x; // multiplicați cu -1 și eliminați diferența x tileToRotate.y = (- 1 * pointArr [1]) + anchorTile.y; // multiplicați cu -1 și eliminați diferența y tileToRotate = axialToOffset (tileToRotate); // convertiți la decalajul de întoarcere tileToRotate;  // ... array functionRotate (arr, invers) // metoda de rotire a elementelor matricei daca (invers) arr.unshift (arr.pop ()) alt arr.push (arr.shift ()) return arr 

Variabila contrar acelor de ceasornic este utilizat pentru a se roti în sensul acelor de ceasornic sau în sens invers acelor de ceasornic, ceea ce se realizează prin mutarea valorilor matricei în direcții opuse în arrayRotate.

Mutarea blocului

Urmărim eu și j coordonatele offset pentru plăcile de mijloc ale blocului folosind variabilele blockMidRowValue și blockMidColumnValue respectiv. Pentru a muta blocul, creștem sau diminuăm aceste valori. Actualizăm valorile corespunzătoare în levelData cu valorile blocului folosind paintBlock metodă. Actualizată levelData este folosit pentru a face scena după fiecare schimbare de stat.

var blockMidRowValue; var blockMidColumnValue; // ... funcția moveLeft () blockMidColumnValue--;  funcția moveRight () blockMidColumnValue ++;  funcția dropDown () paintBlock (true); blockMidRowValue ++;  funcția paintBlock () clockWise = true; var val = 1; changeLevelData (blockMidRowValue, blockMidColumnValue, val); var rotationTile = nou Phaser.Point (blocMidRowValue-1, blockMidColumnValue); dacă (currentBlock.tBlock == 1) changeLevelData (rotațieTile.x, rotațieTil.y, val * currentBlock.tBlock);  var midpoint = nou Phaser.Point (blocMidRowValue, blockMidColumnValue); rotatingTile = rotateTileAroundTile (rotatingTile, Midpoint); dacă (currentBlock.trBlock == 1) changeLevelData (rotațieTile.x, rotativăTil.y, val * currentBlock.trBlock);  midpoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotatingTile = rotateTileAroundTile (rotatingTile, Midpoint); dacă (currentBlock.brBlock == 1) changeLevelData (rotațieTile.x, rotațieTile.y, val * currentBlock.brBlock);  midpoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotatingTile = rotateTileAroundTile (rotatingTile, Midpoint); dacă (currentBlock.bBlock == 1) changeLevelData (rotațieTile.x, rotațieTile.y, val * currentBlock.bBlock);  midpoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotatingTile = rotateTileAroundTile (rotatingTile, Midpoint); dacă (currentBlock.blBlock == 1) changeLevelData (rotațieTile.x, rotativăTil.y, val * currentBlock.blBlock);  midpoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotatingTile = rotateTileAroundTile (rotatingTile, Midpoint); dacă (currentBlock.tlBlock == 1) changeLevelData (rotațieTile.x, rotativăTil.y, val * currentBlock.tlBlock);  funcția changeLevelData (iVal, jVal, newValue, șterge) if (! validIndexes (iVal, jVal)) retur; dacă (ștergeți) if (nivelData [iVal] [jVal] == 1) levelData [iVal] [jVal] = 0;  altceva levelData [iVal] [jVal] = newValue;  funcția validIndexes (iVal, jVal) if (iVal<0 || jVal<0 || iVal>= levelData.length || jVal> = nivelData [0] .length) return false;  return true;  

Aici, currentBlock arată spre blockData în scenă. În paintBlock, mai întâi am setat levelData valoare pentru plăcile din mijloc ale blocului 1 așa cum este întotdeauna 1 pentru toate blocurile. Indicele punctului intermediar este blockMidRowValueblockMidColumnValue

Apoi ne mutăm la levelData indicele țiglei deasupra plăcii de mijloc  blockMidRowValue-1,  blockMidColumnValue, și puneți-o la dispoziție 1 dacă blocul are această placă 1. Apoi rotim în sensul acelor de ceasornic o dată în jurul țiglei de mijloc pentru a obține următoarea țiglă și repeta același proces. Acest lucru se face pentru toate plăcile din jurul plăcii de mijloc pentru bloc.

Verificarea operațiunilor valide

În timp ce deplasăm sau rotim blocul, trebuie să verificăm dacă aceasta este o operație validă. De exemplu, nu putem muta sau roti blocul dacă dalele pe care trebuie să le ocupă sunt deja ocupate. De asemenea, nu putem muta blocul în afara rețelei noastre bidimensionale. De asemenea, trebuie să verificăm dacă blocul poate cădea mai departe, ceea ce ar determina dacă trebuie să cimentăm blocul sau nu. 

Pentru toate acestea, folosesc o metodă canMove (i, j), care returnează un boolean care indică dacă a pus blocul la i, j este o mișcare valabilă. Pentru fiecare operație, înainte de a schimba efectiv levelData , verificăm dacă noua poziție pentru bloc este o poziție validă utilizând această metodă.

funcția canMove (iVal, jVal) var validMove = true; var magazin = ClockWise; var newBlockMidPoint = Phaser.Point nou (blockMidRowValue + iVal, blockMidColumnValue + jVal); = true acelor de ceasornic; dacă (! validAndEmpty (newBlockMidPoint.x, newBlockMidPoint.y)) // verificați mijlocul, întotdeauna 1 validMove = false;  var rotationTile = nou Phaser.Point (newBlockMidPoint.x-1, newBlockMidPoint.y); dacă (currentBlock.tBlock == 1) if (! validAndEmpty (rotațieTile.x, rotațieTile.y)) // verificați top validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); dacă (currentBlock.trBlock == 1) if (! validAndEmpty (rotationTile.x, rotațieTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); dacă (actualBlock.brBlock == 1) if (! validAndEmpty (rotațieTile.x, rotațieTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); dacă (currentBlock.bBlock == 1) if (! validAndEmpty (rotativeTile.x, rotațieTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); dacă (currentBlock.blBlock == 1) if (! validAndEmpty (rotativeTile.x, rotațieTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); dacă (currentBlock.tlBlock == 1) if (! validAndEmpty (rotațieTile.x, rotațieTile.y)) validMove = false;  clockWise = magazin; returna validMove;  funcția validAndEmpty (iVal, jVal) if (! validIndexes (iVal, jVal)) return false;  altfel dacă (levelData [iVal] [jVal]> 1) // returul ocupat returnat;  return true; 

Procesul de aici este același paintBlock, dar în loc de a modifica orice valoare, aceasta returnează un boolean care indică o mișcare valabilă. Deși folosesc rotație în jurul unei plăci de mijloc logica pentru a găsi vecinii, alternativa mai ușoară și destul de eficientă este de a utiliza valorile directe ale coordonatelor vecinilor, care pot fi ușor determinate de coordonatele țiglelor de mijloc.

Rendering the Game

Nivelul jocului este reprezentat vizual de a RenderTexture numit gameScene. În matrice levelData, o țiglă neocupată ar avea o valoare 0, iar o placă ocupată ar avea o valoare de 2 sau mai sus. 

Un bloc cimentat este notat cu o valoare de 2, și o valoare de 5 desemnează o țiglă care trebuie îndepărtată deoarece face parte dintr-un rând completat. O valoare de 1 înseamnă că piesa face parte din bloc. După fiecare schimbare de stare a jocului, facem nivelul folosind informațiile din levelData, așa cum se arată mai jos.

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

Prin urmare, o valoare de 0 este redat fără nici o tentă, o valoare de 1 este redat cu nuanță roșie, o valoare de 2 este redat cu nuanță albastră și o valoare de 5 este redat cu nuanță verde.

5. Jocul finalizat

Punând totul împreună, obținem jocul Tetris hexagonal completat. Accesați codul sursă pentru a înțelege implementarea completă. Veți observa că folosim atât coordonate offset cât și coordonate cubice pentru scopuri diferite. De exemplu, pentru a afla dacă un rând este completat, folosim coordonatele offset și verificăm levelData rânduri.

Concluzie

Aceasta incheie prima parte a seriei. Am creat cu succes un joc Tetris hexagonal folosind o combinație de coordonate offset, coordonate axiale și coordonate cub. 

În partea finală a seriei, vom afla despre mișcarea caracterului utilizând noile coordonate pe o grilă hexagonală aliniată orizontal.