In het eerste deel van de serie hebben we de verschillende coördinatensystemen voor hexagonale op tegels gebaseerde spellen onderzocht met behulp van een hexagonaal Tetris-spel. Een ding dat je misschien hebt gemerkt, is dat we nog steeds vertrouwen op de offsetcoördinaten voor het tekenen van het niveau op het scherm met behulp van de levelData
rangschikking.
Je zou ook nieuwsgierig kunnen zijn om te weten hoe we de axiale coördinaten van een zeshoekige tegel van de pixelcoördinaten op het scherm kunnen bepalen. De methode die wordt gebruikt in de hexagonale mijnenveger-zelfstudie is gebaseerd op de offsetcoördinaten en is geen eenvoudige oplossing. Zodra we dit uitzoeken, zullen we doorgaan met het creëren van oplossingen voor zeshoekige bewegingen en verplaatsing van karakters.
Dit zal wat wiskunde inhouden. We zullen de horizontale lay-out gebruiken voor de hele tutorial. Laten we beginnen met het vinden van een zeer nuttige relatie tussen de breedte en hoogte van de regelmatige zeshoek. Raadpleeg de afbeelding hieronder.
Overweeg de blauwe regelmatige zeshoek aan de linkerkant van de afbeelding. We weten al dat alle zijden even lang zijn. Alle binnenhoeken zijn elk 120 graden. Elke hoek verbinden met het midden van de zeshoek levert zes driehoeken op, waarvan er één wordt weergegeven met rode lijnen. Deze driehoek heeft alle interne hoeken gelijk aan 60 graden.
Terwijl de rode lijn de twee hoekhoeken in het midden splitst, krijgen we 120/2 = 60
. De derde hoek is 180- (60 + 60) = 60
omdat de som van alle hoeken binnen de driehoek 180 graden moet zijn. Dus in wezen is de driehoek een gelijkzijdige driehoek, wat verder betekent dat elke zijde van de driehoek dezelfde lengte heeft. Dus in de blauwe zeshoek hebben de twee rode lijnen, de groene lijn en elk blauw lijnsegment dezelfde lengte. Uit de afbeelding is het duidelijk dat de groene lijn is hexTileHeight / 2
.
Ga naar de zeshoek aan de rechterkant, we kunnen zien dat de lengte van de zijkant gelijk is aan hexTileHeight / 2
, de hoogte van het driehoekige bovengedeelte moet zijn hexTileHeight / 4
en de hoogte van het driehoekige bodemgedeelte moet zijn hexTileHeight / 4
, die tot de volledige hoogte van de zeshoek komt, hexTileHeight
.
Overweeg nu de kleine rechthoekige driehoek linksboven met een groene en een blauwe hoek. De blauwe hoek is 60 graden omdat het de helft van de hoek is, wat op zijn beurt betekent dat de groene hoek 30 graden is (180- (60 + 90)
). Met behulp van deze informatie komen we tot een relatie tussen de hoogte en de breedte van de regelmatige zeshoek.
tan 30 = tegenoverliggende zijde / aangrenzende zijde; 1 / sqrt (3) = (hexTileHeight / 4) / (hexTileWidth / 2); hexTileWidth = sqrt (3) * hexTileHeight / 2; hexTileHeight = 2 * hexTileWidth / sqrt (3);
Voordat we de conversie benaderen, gaan we terug naar de afbeelding van de horizontale hexagonale lay-out waarin we de rij en kolom hebben gemarkeerd waarin een van de coördinaten hetzelfde blijft.
Gezien de scherm-y-waarde, kunnen we zien dat elke rij een y-offset heeft van 3 * hexTileHeight / 4
, terwijl je op de groene lijn gaat, is de enige waarde die verandert ik
. Daarom kunnen we concluderen dat de y-pixelwaarde alleen afhangt van de axiale waarde ik
coördineren.
y = (3 * hexTileHeight / 4) * i; y = 3/2 * s * i;
Waar s
is de lengte van de zijkant, die bleek te zijn hexTileHeight / 2
.
De scherm x-waarde is een beetje ingewikkelder dan deze. Bij het beschouwen van de tegels binnen een enkele rij, heeft elke tegel een x-offset van hexTileWidth
, wat duidelijk alleen afhangt van het axiale j
coördineren. Maar elke alternatieve rij heeft een extra offset van hexTileWidth / 2
afhankelijk van de axiaal ik
coördineren.
Nogmaals, als we de groene lijn overwegen, als we ons voorstellen dat het een vierkant raster was, dan zou de lijn verticaal zijn geweest, wat aan de vergelijking voldoet x = j * hexTileWidth
. Als de enige coördinaat die langs de groene lijn verandert is ik
, de offset hangt ervan af. Dit leidt ons naar de volgende vergelijking.
x = j * hexTileWidth + (i * hexTileWidth / 2); = j * sqrt (3) * hexTileHeight / 2 + i * sqrt (3) * hexTileHeight / 4; = sqrt (3) * s * (j + (i / 2));
Dus hier hebben we ze: de vergelijkingen om axiale coördinaten naar schermcoördinaten om te zetten. De bijbehorende conversiefunctie is zoals hieronder.
var rootThree = Math.sqrt (3); var sideLength = hexTileHeight / 2; functie axialToScreen (axialPoint) var tileX = rootThree * sideLength * (axialPoint.y + (axialPoint.x / 2)); var tileY = 3 * sideLength / 2 * axialPoint.x; axialPoint.x = Tilex; axialPoint.y = tiley; retourneer axialPoint;
De herziene code voor het tekenen van het hexagonale raster is als volgt.
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);
Het omkeren van die vergelijkingen met de eenvoudige vervanging van één variabele leidt ons naar de scherm naar axiale conversievergelijkingen.
i = y / (3/2 * s); j = (x- (y / sqrt (3))) / s * sqrt (3);
Hoewel de vereiste axiale coördinaten gehele getallen zijn, resulteren de vergelijkingen in getallen met drijvende komma. We moeten ze dus afronden en enkele correcties toepassen, afhankelijk van onze belangrijkste vergelijking x + y + z = 0
. De conversiefunctie is zoals hieronder.
function 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 = calculationCubicZ (axialPoint); var round_x = Math.round (axialPoint.x); var round_y = Math.round (axialPoint.y); var round_z = Math.round (cubicZ); if (round_x + round_y + round_z === 0) screenPoint.x = round_x; screenPoint.y = round_y; else 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); if (delta_x> delta_y && delta_x> delta_z) screenPoint.x = -round_y-round_z; screenPoint.y = round_y; else if (delta_y> delta_x && delta_y> delta_z) screenPoint.x = round_x; screenPoint.y = -round_x-round_z; else if (delta_z> delta_x && delta_z> delta_y) screenPoint.x = round_x screenPoint.y = round_y; retourneer screenPoint;
Bekijk het interactieve element, dat deze methoden gebruikt om tegels weer te geven en tikken te detecteren.
Het kernconcept van de personagebeweging in elk raster is vergelijkbaar. We pollen voor gebruikersinvoer, bepalen de richting, vinden de resulterende positie, controleren of de resulterende positie binnen een muur in het raster valt, anders verplaats het personage naar die positie. U kunt verwijzen naar mijn isometrische bewegingshandleiding voor karakters om dit in actie te zien met betrekking tot de omzetting van isometrische coördinaten.
De enige dingen die hier anders zijn, zijn de coördinatenconversie en de bewegingsrichtingen. Voor een horizontaal uitgelijnd zeshoekig raster zijn er zes beschikbare bewegingsrichtingen. We zouden de klaviertoetsen kunnen gebruiken EEN
, w
, E
, D
, X
, en Z
voor het regelen van elke richting. De standaard toetsenbordindeling komt perfect overeen met de richtingen en de gerelateerde functies zijn zoals hieronder.
function moveLeft () movementVector.x = movementVector.y = 0; movementVector.x = -1 * snelheid; CheckCollisionAndMove (); function moveRight () movementVector.x = movementVector.y = 0; movementVector.x = snelheid; CheckCollisionAndMove (); functie moveTopLeft () movementVector.x = -0.5 * speed; // Cos60 movementVector.y = -0.866 * speed; // sine60 CheckCollisionAndMove (); functie moveTopRight () movementVector.x = 0.5 * speed; // Cos60 movementVector.y = -0.866 * speed; // sine60 CheckCollisionAndMove (); functie moveBottomRight () movementVector.x = 0.5 * speed; // Cos60 movementVector.y = 0.866 * speed; // sine60 CheckCollisionAndMove (); functie moveBottomLeft () movementVector.x = -0.5 * speed; // Cos60 movementVector.y = 0.866 * speed; // sine60 CheckCollisionAndMove ();
De diagonale bewegingsrichtingen maken een hoek van 60 graden met de horizontale richting. We kunnen dus direct de nieuwe positie berekenen met behulp van trigonometrie door te gebruiken Cos 60
en Sine 60
. Van dit movementVector
, we ontdekken de nieuwe resulterende positie en controleren of deze binnen een muur in het rooster valt, zoals hieronder.
function CheckCollisionAndMove () var tempPos = new Phaser.Point (); tempPos.x = hero.x + movementVector.x; tempPos.y = hero.y + movementVector.y; var corner = new Phaser.Point (); // check tl corner.x = tempPos.x-heroSize / 2; corner.y = tempPos.y-heroSize / 2; if (checkCorner (hoek)) return; // check tr corner.x = tempPos.x + heroSize / 2; corner.y = tempPos.y-heroSize / 2; if (checkCorner (hoek)) return; // check bl corner.x = tempPos.x-heroSize / 2; corner.y = tempPos.y + heroSize / 2; if (checkCorner (hoek)) return; // check br corner.x = tempPos.x + heroSize / 2; corner.y = tempPos.y + heroSize / 2; if (checkCorner (hoek)) return; hero.x = tempPos.x; hero.y = tempPos.y; function checkCorner (corner) corner = screenToAxial (corner); corner = axialToOffset (hoek); if (checkForOccuppancy (corner.x, corner.y)) return true; return false;
We voegen het toe movementVector
naar de hero-positie-vector om de nieuwe positie voor het centrum van de hero sprite te krijgen. Vervolgens vinden we de positie van de vier hoeken van de heldensprite en controleren of deze botsen. Als er geen botsingen zijn, stellen we de nieuwe positie in op de heldensprite. Laten we dat in actie zien.
Meestal is dit soort vrij vloeiende beweging niet toegestaan in een op rasters gebaseerde game. Typisch verplaatsen tekens zich van tegel naar tegel, dat wil zeggen van tegelmidden naar tegelmidden, op basis van opdrachten of tik. Ik vertrouw erop dat je de oplossing zelf kunt uitvinden.
Dus hier hebben we het over pathfinding, een heel eng onderwerp voor sommigen. In mijn vorige tutorials heb ik nooit geprobeerd nieuwe oplossingen voor pathfinding te maken, maar ik heb altijd de voorkeur gegeven aan gemakkelijk beschikbare oplossingen die op gevechten zijn getest.
Deze keer maak ik een uitzondering en zal ik het wiel opnieuw uitvinden, vooral omdat er verschillende spelmechanismen mogelijk zijn en geen enkele oplossing ten goede zou komen. Het is dus handig om te weten hoe het allemaal is om je eigen oplossingen voor je spelmonteur te vinden.
Het meest eenvoudige algoritme dat wordt gebruikt voor het vinden van paden in rasters is Het algoritme van Dijkstra. We starten bij het eerste knooppunt en berekenen de kosten voor het verplaatsen naar alle mogelijke buurknooppunten. We sluiten het eerste knooppunt en gaan naar het buurknooppunt met de laagste kosten. Dit wordt herhaald voor alle niet-gesloten knooppunten totdat we de bestemming hebben bereikt. Een variant hiervan is de Een * algoritme, waarbij we naast de kosten ook een heuristiek gebruiken.
Een heuristiek wordt gebruikt om de geschatte afstand van het huidige knooppunt tot het bestemmingsknooppunt te berekenen. Omdat we het pad niet echt kennen, is deze afstandsberekening altijd een benadering. Dus een betere heuristiek levert altijd een beter pad op. Dat gezegd hebbende hoeft de beste oplossing niet de beste oplossing te zijn, want we moeten ook rekening houden met het gebruik van hulpbronnen en de prestaties van het algoritme, wanneer alle berekeningen in realtime of eenmaal per update moeten worden uitgevoerd. lus.
De eenvoudigste en eenvoudigste heuristiek is de Heuristische Manhattan
of Manhattan afstand
. In een 2D-raster is dit eigenlijk de afstand tussen het startknooppunt en het eindknooppunt in vogelvlucht, of het aantal blokken dat we moeten lopen.
Voor ons hexagonale raster moeten we een variant vinden voor de heuristiek van Manhattan om de afstand te benaderen. Terwijl we op de zeshoekige tegels lopen, is het de bedoeling om het aantal tegels te vinden dat we nodig hebben om naar de bestemming te lopen. Ik zal u eerst de oplossing laten zien. Beweeg de muis over het interactieve element hieronder om te zien hoe ver de andere tegels zich van de tegel onder de muis bevinden.
In het bovenstaande voorbeeld vinden we de tegel onder de muis en vinden we de afstand van alle andere tegels. De logica is om het verschil te vinden ik
en j
axiale coördinaten van beide tegels eerst, zeg di
en dj
. Zoek de absolute waarden van deze verschillen, ABSI
en absj
, omdat afstanden altijd positief zijn.
Dat merken we wanneer beide di
en dj
zijn positief en wanneer beide di
en dj
zijn negatief, de afstand is ABSI + absj
. Wanneer di
en dj
zijn van tegengestelde tekens, de afstand is de grotere waarde onder ABSI
en absj
. Dit leidt tot de heuristische berekeningsfunctie getHeuristic
zoals hieronder.
getHeuristic = functie (i, j) j = (j- (Math.floor (i / 2))); var di = i-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; if (si! = sj) this.heuristic = Math.max (absi, absj); else this.heuristic = (absi + absj);
Een ding om op te letten is dat we niet overwegen of het pad echt beloopbaar is of niet; we nemen gewoon aan dat het beloopbaar is en stel de afstandswaarde in.
Laten we doorgaan met het zoeken naar routines voor ons hexagonale raster met de nieuw gevonden heuristische methode. Omdat we recursie zullen gebruiken, zal het gemakkelijker te begrijpen zijn als we de kernlogica van onze aanpak hebben afgebroken. Elke zeshoekige tegel heeft een heuristische afstand en een bijbehorende kostenwaarde.
FindPath (tegel)
, die één hexagonale tegel in beslag neemt, wat de huidige tegel is. In eerste instantie is dit de starttegel.Gesloten
.kosten
naar huidige tegel kost + 10. We plaatsen de buurtegel als bezocht. We plaatsen de buurtegels vorige tegel
als de huidige tegel. We doen dit ook voor een eerder bezochte buurman als de kostprijs van de huidige tegel + 10 lager is dan die van de buurman.FindPath
op die buurtegel.Er is een duidelijke falingstoestand in de logica wanneer meer dan één tegel voldoet aan de voorwaarden. Een beter algoritme zal alle verschillende paden vinden en die met de kortste lengte selecteren, maar we zullen dat hier niet doen. Controleer de verplaatsing in actie hieronder.
Voor dit voorbeeld bereken ik buren anders dan in het Tetris-voorbeeld. Bij gebruik van axiale coördinaten hebben de aangrenzende tegels coördinaten die hoger of lager zijn met een waarde van 1.
functie getNeighbors (i, j) // coördinaten zijn in axiale var tempArray = []; var axialPoint = nieuwe Phaser.Point (i, j); var neighbourPoint = nieuwe 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); return tempArray;
De FindPath
de recursieve functie is zoals hieronder.
function findPath (tile) // past in een hexTileNode if (Phaser.Point.equals (tile, endTile)) // succes, bestemming bereikt console.log ('succes'); // Schilder nu het pad. paintPath (tegel); else // vind alle buren var buren = getNeighbors (tile.originali, tile.convertedj); var newPt = nieuwe Phaser.Point (); var hexTile; var totalCost = 0; var currentLowestCost = 100000; var nextTile; // vind heuristieken & kosten voor alle buren terwijl (neighbors.length) newPt = neighbors.shift (); Hextile = hexGrid.getByName ( "tile" + newPt.x + "_" + newPt.y); if (! hexTile.nodeClosed) // als knooppunt nog niet was berekend als ((hexTile.nodeVisited && (tile.cost + 10)Het kan verder en meervoudig lezen vereisen om goed te begrijpen wat er gaande is, maar geloof me, het is de moeite waard. Dit is slechts een heel eenvoudige oplossing en kan veel worden verbeterd. Om het personage langs het berekende pad te verplaatsen, kunt u mijn isometrische pad volgen via de zelfstudie.
Het markeren van het pad gebeurt met een andere eenvoudige recursieve functie,
paintPath (tegel)
, die eerst wordt aangeroepen met de eindtegel. We markeren hetpreviousNode
van de tegel, indien aanwezig.functie paintPath (tile) tile.markDirty (); if (tile.previousNode! == null) paintPath (tile.previousNode);Conclusie
Met de hulp van alle drie de hexagonale tutorials die ik heb gedeeld, zou je aan de slag kunnen gaan met je volgende geweldige zeshoekige tegel-gebaseerde game.
Houd er rekening mee dat er ook andere benaderingen zijn, en er is nog veel meer te lezen als u er klaar voor bent. Laat het me alsjeblieft weten via de reacties als je nog meer wilt ontdekken in verband met zeshoekige tegel-gebaseerde spellen.