În prima parte a seriei, am explorat diferitele sisteme de coordonate pentru jocuri pe bază de țiglă hexagonală cu ajutorul unui joc Tetris hexagonal. Un lucru pe care l-ați observat este că ne bazăm în continuare pe coordonatele offset pentru desenarea nivelului pe ecran folosind levelData
mulțime.
De asemenea, s-ar putea să fii curios să știm cum putem determina coordonatele axiale ale unei plăci hexagonale de la coordonatele pixelilor de pe ecran. Metoda folosită în tutorialul hexagonal minesweeper se bazează pe coordonatele offset și nu este o soluție simplă. Odată ce ne dăm seama, vom continua să creăm soluții pentru mișcarea de caractere hexagonale și pentru trasarea drumurilor.
Aceasta va implica unele matematici. Vom folosi planul orizontal pentru întregul tutorial. Să începem prin găsirea unei relații foarte utile între lățimea și înălțimea hexagonului obișnuit. Consultați imaginea de mai jos.
Luați în considerare hexagonul normal albastru din stânga imaginii. Știm deja că toate laturile au aceeași lungime. Toate unghiurile interioare sunt de 120 de grade fiecare. Conectarea fiecărui colț la centrul hexagonului va produce șase triunghiuri, dintre care unul este afișat folosind linii roșii. Acest triunghi are toate unghiurile interne egale cu 60 de grade.
Deoarece linia roșie împarte cele două unghiuri de colț în mijloc, ajungem 120/2 = 60
. Al treilea unghi este 180- (60 + 60) = 60
deoarece suma tuturor unghiurilor din triunghi ar trebui să fie de 180 de grade. Astfel, în esență, triunghiul este un triunghi echilateral, ceea ce înseamnă că fiecare parte a triunghiului are aceeași lungime. Astfel, în hexagonul albastru cele două linii roșii, linia verde și fiecare segment de linie albastră au aceeași lungime. Din imagine, este clar că linia verde este hexTileHeight / 2
.
Continuând cu hexagonul din dreapta, vedem că lungimea laturii este egală cu hexTileHeight / 2
, înălțimea porțiunii triunghiulare superioare ar trebui să fie hexTileHeight / 4
iar înălțimea porțiunii triunghiulare inferioare ar trebui să fie hexTileHeight / 4
, care totalizează la înălțimea completă a hexagonului, hexTileHeight
.
Acum, luați în considerare triunghiul cu unghi drept în partea stângă sus cu un unghi verde și un albastru. Unghiul albastru este de 60 de grade, deoarece este jumătate din unghiul de colț, ceea ce înseamnă, în schimb, că unghiul verde este de 30 de grade (180- (60 + 90)
). Folosind aceste informații, ajungem la o relație între înălțimea și lățimea hexagonului obișnuit.
tan 30 = partea opusă / partea adiacentă; 1 / sqrt (3) = (hexTileHeight / 4) / (hexTileWidth / 2); hexTileWidth = sqrt (3) * hexTileHeight / 2; hexTileHeight = 2 * hexTileWidth / sqrt (3);
Înainte de a ne apropia de conversie, revedem imaginea dispunerii hexagonale orizontale unde am subliniat rândul și coloana în care una dintre coordonate rămâne aceeași.
Având în vedere valoarea ecranului y, putem vedea că fiecare rând are un decalaj y 3 * hexTileHeight / 4
, în timp ce merge pe linia verde, singura valoare care se schimbă eu
. Prin urmare, putem concluziona că valoarea pixelului y depinde doar de axial eu
coordona.
y = (3 x hexilă înălțime / 4) * i; y = 3/2 * s * i;
Unde s
este lungimea laterală, care sa dovedit a fi hexTileHeight / 2
.
Valoarea ecranului x este un pic mai complicată decât asta. Atunci când se iau în considerare plăcile într-un singur rând, fiecare țiglă are un decalaj de x hexTileWidth
, care depinde în mod clar numai de axial j
coordona. Dar fiecare rând alternativ are un offset suplimentar de hexTileWidth / 2
în funcție de axial eu
coordona.
Din nou, considerând linia verde, dacă ne imaginăm că era o rețea pătrată, linia ar fi fost verticală, satisfăcând ecuația x = j * hexTileWidth
. Ca singura coordonată care se schimbă de-a lungul liniei verde este eu
, offsetul va depinde de acesta. Aceasta ne conduce la următoarea ecuație.
x = j * hexTileWidth + (i * hexTileWidth / 2); = j * sqrt (3) * hexTileHeight / 2 * i * sqrt (3) * hexTileHeight / 4; = sqrt (3) * s * (j + (i / 2));
Deci aici le avem: ecuațiile pentru a converti coordonatele axiale la coordonatele ecranului. Funcția de conversie corespunzătoare este cea de mai jos.
var rootThree = Math.sqrt (3); var lateralLength = hexTileHeight / 2; funcția axialToScreen (axialPoint) var tileX = rădăcinăTrei * Lățime laterală * (axialPoint.y + (axialPoint.x / 2)); var tileY = 3 * Lățime laterală / 2 * axialPoint.x; axialPoint.x = tileX; axialPoint.y = Tiley; retur axialPoint;
Codul revizuit pentru desenarea grilajului hexagonal este după cum urmează.
pentru (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);
Reversarea acelor ecuații cu înlocuirea simplă a unei variabile ne va conduce la ecrane la ecuațiile de conversie axiale.
i = y / (3/2 * s); j = (x- (y / sqrt (3))) / s * sqrt (3);
Deși coordonatele axiale cerute sunt numere întregi, ecuațiile vor avea ca rezultat numere în virgulă mobilă. Așadar, va trebui să le rotungem și să aplicăm unele corecții, bazându-ne pe ecuația noastră principală x + y + z = 0
. Funcția de conversie este cea de mai jos.
ecranul funcțieiToAxial (screenPoint) var axialPoint = nou Phaser.Point (); axialPoint.x = screenPoint.y / (1,5 * sideLength); axialPoint.y = (screenPoint.x- (screenPoint.y / rootThree)) / (rootThree * sideLength); var cubicZ = calculCubicZ (axialPoint); var rotund_x = Math.round (axialPoint.x); var rot_y = Math.round (axialPoint.y); var rotund_z = Math.round (cubicZ); dacă (rotund_x + rotund_y + rotund_z === 0) screenPoint.x = rotund_x; screenPoint.y = round_y; altfel var delta_x = Math.abs (axialPoint.x-rotund_x); var delta_y = Math.abs (axialPoint.y-rotund_y); var delta_z = Math.abs (cubicZ-rotund_z); dacă delta_x> delta_y && delta_x> delta_z) screenPoint.x = -round_y-round_z; screenPoint.y = round_y; altfel dacă (delta_y> delta_x && delta_y> delta_z) screenPoint.x = rotundă_x; screenPoint.y = -round_x-round_z; altfel dacă (delta_z> delta_x && delta_z> delta_y) screenPoint.x = rotund_x screenPoint.y = rotund_y; return screenPoint;
Consultați elementul interactiv, care utilizează aceste metode pentru a afișa dale și a detecta robinetele.
Conceptul de bază al mișcării caracterului în orice rețea este similar. Sondăm pentru introducerea utilizatorului, determinăm direcția, găsim poziția rezultată, verificăm dacă poziția rezultată se încadrează în interiorul unui perete din rețea, altfel mutați caracterul în acea poziție. Poți să te referi la tutorialul mișcării izometrice pentru a vedea acest lucru în acțiune în ceea ce privește conversia izometrică a coordonatelor.
Singurele lucruri care sunt diferite aici sunt conversia coordonatelor și direcțiile mișcării. Pentru o rețea hexagonală aliniată orizontal, există șase direcții disponibile pentru mișcare. Am putea folosi cheile de la tastatură A
, W
, E
, D
, X
, și Z
pentru controlul fiecărei direcții. Tastatura implicită a tastaturii se potrivește perfect direcțiilor, iar funcțiile asociate sunt cele de mai jos.
funcția moveLeft () moveVector.x = moveVector.y = 0; movementVector.x = -1 * Viteza; CheckCollisionAndMove (); funcția moveRight () moveVector.x = moveVector.y = 0; movementVector.x = viteza; CheckCollisionAndMove (); funcția moveTopLeft () moveVector.x = -0,5 * viteză; // Cos60 moveVector.y = -0,866 * viteză; // sine60 CheckCollisionAndMove (); functie moveTopRight () moveVector.x = 0.5 * viteza; // Cos60 moveVector.y = -0.866 * viteza; // sine60 CheckCollisionAndMove (); functie moveBottomRight () moveVector.x = 0.5 * viteza; // Cos60 moveVector.y = 0.866 * viteza; // sine60 CheckCollisionAndMove (); functie moveBottomLeft () moveVector.x = -0.5 * viteza; // Cos60 moveVector.y = 0.866 * viteza; // sine60 CheckCollisionAndMove ();
Direcțiile diagonale de mișcare fac un unghi de 60 de grade cu direcția orizontală. Deci, putem calcula direct noua poziție utilizând trigonometria folosind Cos 60
și Sine 60
. Din această movementVector
, aflăm noua poziție care rezultă și verificăm dacă se încadrează în interiorul unui perete din grila de mai jos.
funcția CheckCollisionAndMove () var tempPos = nou Phaser.Point (); tempPos.x = hero.x + movementVector.x; tempPos.y = hero.y + movementVector.y; var colț = nou Phaser.Point (); // verificați tl corner.x = tempPos.x-heroSize / 2; corner.y = tempPos.y-heroSize / 2; în cazul în care (checkCorner (colț)) întoarcere; // verificați tr corner.x = tempPos.x + heroSize / 2; corner.y = tempPos.y-heroSize / 2; în cazul în care (checkCorner (colț)) întoarcere; // verificați bl corner.x = tempPos.x-heroSize / 2; corner.y = tempPos.y + heroSize / 2; în cazul în care (checkCorner (colț)) întoarcere; // verificați br corner.x = tempPos.x + heroSize / 2; corner.y = tempPos.y + heroSize / 2; în cazul în care (checkCorner (colț)) întoarcere; hero.x = tempPos.x; hero.y = tempPos.y; funcția checkCorner (colț) corner = screenToAxial (colț); colt = axialToOffset (colț); dacă (checkForOccuppancy (corner.x, corner.y)) return true; return false;
Adăugăm movementVector
la vectorul de pozitie a eroului pentru a obtine noua pozitie pentru centrul eroului sprite. Apoi găsim poziția celor patru colțuri ale eroului sprite și verificăm dacă acestea se ciocnesc. Dacă nu există coliziuni, atunci vom stabili noua poziție la sprite erou. Să vedem asta în acțiune.
De obicei, acest tip de mișcare liberă nu este permisă într-un joc bazat pe rețea. În mod tipic, caracterele se deplasează de la țiglă la țiglă, adică, centrul țiglelor în centru, bazat pe comenzi sau atingeți. Am încredere că puteți găsi soluția singură.
Deci, aici suntem pe tema traseului de drum, un subiect foarte infricosator pentru unii. În tutorialele mele anterioare nu am încercat niciodată să creez soluții noi de căutare, dar am preferat întotdeauna să folosesc soluții ușor accesibile, care sunt testate în luptă.
De această dată, fac o excepție și voi reinventa roata, în special pentru că există mecanisme de joc diferite și nici o singură soluție nu ar beneficia de toate. Deci, este util să știi cum se face totul pentru a-ți împrumuta propriile soluții personalizate pentru mecanicul tău de joc.
Algoritmul cel mai de bază care este utilizat pentru trasarea în rețea în rețele este Algoritmul lui Dijkstra. Începem la primul nod și calculam costurile implicate în trecerea la toate nodurile vecine posibile. Închidem primul nod și ne mutăm la nodul vecin cu cel mai mic cost implicat. Aceasta se repetă pentru toate nodurile neînchise până când ajungem la destinație. O variantă a acestui lucru este Algoritmul A *, unde folosim, de asemenea, un cost euristic în plus față de cost.
O euristică este folosită pentru a calcula distanța aproximativă de la nodul curent la nodul de destinație. Deoarece nu știm cu adevărat calea, acest calcul al distanței este întotdeauna o aproximare. Deci un euristic mai bun va da întotdeauna o cale mai bună. Acum, după cum sa spus, soluția cea mai bună nu trebuie să fie cea care oferă cea mai bună cale, deoarece trebuie să luăm în considerare utilizarea resurselor și performanța algoritmului, atunci când toate calculele trebuie efectuate în timp real sau o dată pe actualizare buclă.
Cea mai simplă și cea mai simplă euristică este Manhattan euristic
sau Distanța de la Manhattan
. Într-o rețea 2D, aceasta este, de fapt, distanța dintre nodul de început și nodul de sfârșit ca zborul de zbor sau numărul de blocuri de care avem nevoie pentru a merge.
Pentru grila noastră hexagonală, trebuie să găsim o variantă pentru euristica din Manhattan pentru a aproxima distanța. Pe măsură ce mergem pe gresie hexagonală, ideea este să găsim numărul de plăci pe care trebuie să ne plimbăm pentru a ajunge la destinație. Permiteți-mi să vă arăt mai întâi soluția. Deplasați mouse-ul peste elementul interactiv de mai jos pentru a vedea cât de departe sunt celelalte plăci din țiglă sub mouse.
În exemplul de mai sus, găsim țigla sub mouse și găsim distanța tuturor celorlalte plăci din ea. Logica este de a găsi diferența dintre eu
și j
axiale coordonate ale ambelor plăci mai întâi, să zicem di
și dj
. Găsiți valorile absolute ale acestor diferențe, Absi
și absj
, deoarece distanțele sunt întotdeauna pozitive.
Observăm că atunci când amândouă di
și dj
sunt pozitive și când ambele di
și dj
sunt negative, distanța este Absi + absj
. Cand di
și dj
sunt de semne opuse, distanța este cea mai mare valoare printre Absi
și absj
. Aceasta duce la funcția de calcul euristic getHeuristic
ca mai jos.
getHeuristic = funcție (i, j) j = (j- (Math.floor (i / 2))); var di = i-aceasta.originali; var dj = j-this.convertedj; var si = Math.sign (di); var sj = Matematica (dj); var absi = di * si; var absj = dj * sj; dacă (si! = sj) this.heuristic = Math.max (absi, absj); altfel this.heuristic = (absi + absj);
Un lucru de observat este că nu ne gândim dacă calea este cu adevărat vagabond sau nu; presupunem doar că este în mișcare și se stabilește valoarea distanței.
Să continuăm cu modelarea traseului pentru grila hexagonală cu metoda euristică nouă găsită. Pe măsură ce vom folosi recursivitatea, va fi mai ușor de înțeles odată ce defalcam logica de bază a abordării noastre. Fiecare țiglă hexagonală va avea o distanță euristică și o valoare a costurilor asociată cu aceasta.
findPath (tigla)
, care ia o placă hexagonală, care este tigla actuală. Inițial, aceasta va fi țigla de pornire.închis
.cost
la costul curent al plăcii + 10. Am setat țigla vecină vizitat. Am pus țiglele vecine tigla anterioară
ca tigla actuală. Facem acest lucru pentru un vecin vizitat anterior, de asemenea, dacă costul actual al plăcii + 10 este mai mic decât costul vecinului.findPath
pe acea țiglă vecină.Există o condiție evidentă de eșec în logica atunci când mai mult de o țiglă satisface condițiile. Un algoritm mai bun va găsi toate căile diferite și selectați cel cu cea mai scurtă durată, dar nu o vom face aici. Verificați urmărirea traseului în acțiune.
Pentru acest exemplu, calculez vecinii diferit decât în exemplul Tetris. Atunci când se utilizează coordonate axiale, plăcile vecine au coordonate care sunt mai mari sau mai mici cu o valoare de 1.
funcția getNeighbors (i, j) // coordonatele sunt în axial var tempArray = []; var axialPoint = noul Phaser.Point (i, j); var neighbourPoint = nou 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); retur tempArray;
findPath
funcția recursivă este cea descrisă mai jos.
funcția findPath (țiglă) // trece într-un hexTileNode dacă (Phaser.Point.equals (tile, endTile)) // succes, destinația a ajuns la console.log ('succes'); // vopsi acum calea. paintPath (tigla); altfel // găsiți toți vecinii var neighbors = getNeighbors (tile.originali, tile.convertedj); var newPt = nou Phaser.Point (); var hexTile; var totalCost = 0; Var actualLowestCost = 100000; var nextTile; // găsiți euristicile și costurile pentru toți vecinii în timp ce (vecini. lungime) newPt = neighbors.shift (); hexTile = hexGrid.getByName ( "țiglă" + newPt.x + "_" + newPt.y); dacă (! hexTile.nodeClosed) // dacă nodul nu a fost deja calculat dacă (((hexTile.nodeVisited && (tile.cost + 10)Este posibil să fie nevoie de citiri suplimentare și multiple pentru a înțelege corect ceea ce se întâmplă, dar credeți-mă, merită efortul. Aceasta este doar o soluție foarte de bază și ar putea fi îmbunătățită foarte mult. Pentru a deplasa caracterul de-a lungul căii calculate, puteți să consultați calea mea izometrică urmând tutorialul.
Marcarea căii se face folosind o altă funcție recursivă simplă,
paintPath (tigla)
, care este numit mai întâi cu placa de capăt. Pur și simplu notămpreviousNode
a dalei dacă este prezent.funcția paintPath (țiglă) tile.markDirty (); dacă (tile.previousNode! == nulă) paintPath (tile.previousNode);Concluzie
Cu ajutorul celor trei tutoriale hexagonale pe care le-am împărtășit, ar trebui să puteți începe cu următorul joc minunat de șabloane pe bază de țiglă.
Vă rugăm să fiți informat că există și alte abordări, și există o mulțime de lecturi suplimentare acolo dacă sunteți pregătit pentru asta. Vă rog să-mi spuneți prin comentariile dvs. dacă aveți nevoie de ceva mai mult pentru a fi explorat în legătură cu jocurile bazate pe plăci hexagonale.