Introductie tot axiale coördinaten voor zeshoekige tegel-gebaseerde spellen

Wat je gaat creëren

De basis hexagonale tegel-gebaseerde aanpak uitgelegd in de hexagonale mijnenveger tutorial krijgt het werk gedaan, maar is niet erg efficiënt. Het maakt gebruik van directe conversie van de tweedimensionale array-gebaseerde niveaudata en de schermcoördinaten, waardoor het onnodig ingewikkeld wordt om getapte tegels te bepalen. 

Ook is de behoefte om verschillende logica te gebruiken afhankelijk van de oneven of even rij / kolom van een tegel niet handig. Deze zelfstudiereeks verkent de alternatieve schermcoördinatenstelsels die kunnen worden gebruikt om de logica te vereenvoudigen en dingen handiger te maken. Ik zou sterk aanraden dat je de hexagonale mijnenveger-zelfstudie leest voordat je verder gaat met deze tutorial, omdat degene die de grid-rendering uitlegt, gebaseerd is op een tweedimensionale array.

1. Axiale coördinaten

De standaardbenadering voor schermcoördinaten in de hexagonale mijnenveger-zelfstudie wordt de offset-coördinaatbenadering genoemd. Dit komt omdat de alternatieve rijen of kolommen worden verschoven door een waarde tijdens het uitlijnen van het hexagonale raster. 

Om uw geheugen te vernieuwen, raadpleegt u de afbeelding hieronder, die de horizontale uitlijning toont met de waarden van de offsetcoördinaten die worden weergegeven.

In de bovenstaande afbeelding een rij met dezelfde ik waarde is rood gemarkeerd en een kolom met dezelfde j waarde is groen gemarkeerd. Om alles eenvoudig te maken, zullen we niet de oneven en even offset-varianten bespreken, omdat beide slechts verschillende manieren zijn om hetzelfde resultaat te krijgen. 

Laat me een beter schermcoördinatenalternatief introduceren, de axiale coördinaat. Het converteren van een offsetcoördinaat naar een axiale variant is heel eenvoudig. De ik waarde blijft hetzelfde, maar de j waarde wordt geconverteerd met behulp van de formule axialJ = i - vloer (j / 2). Een eenvoudige methode kan worden gebruikt om een ​​offset te converteren Phaser.Point naar zijn axiale variant, zoals hieronder getoond.

functie offsetToAxial (offsetPoint) offsetPoint.y = (offsetPoint.y- (Math.floor (offsetPoint.x / 2))); return offsetPoint; 

De omgekeerde conversie zou zijn zoals hieronder getoond.

functie axialToOffset (axialPoint) axialPoint.y = (axialPoint.y + (Math.floor (axialPoint.x / 2))); retourneer axialPoint; 

Hier de X waarde is het ik waarde, en Y waarde is het j waarde voor de tweedimensionale array. Na de conversie lijken de nieuwe waarden op de onderstaande afbeelding.

Merk op dat de groene lijn waar de j waarde blijft hetzelfde, zigzagt niet meer, maar is eerder diagonaal ten opzichte van ons zeshoekige raster.

Voor het verticaal uitgelijnde hexagonale raster worden de offset-coördinaten weergegeven in de onderstaande afbeelding.

De conversie naar axiale coördinaten volgt dezelfde vergelijkingen, met het verschil dat we de j waarde hetzelfde en verander de ik waarde. De onderstaande methode toont de conversie.

functie offsetToAxial (offsetPoint) offsetPoint.x = (offsetPoint.x- (Math.floor (offsetPoint.y / 2))); return offsetPoint; 

Het resultaat is zoals hieronder getoond.

Voordat ik de nieuwe coördinaten gebruik om problemen op te lossen, laat ik u snel een ander schermcoördinatenalternatief voorstellen: kubuscoordinaten.

2. Kubus of kubieke coördinaten

Het rechtzetten van de zigzag zelf heeft mogelijk de meeste ongemakken opgelost die we hadden met het offset-coördinatenstelsel. Kubus of kubieke coördinaten zouden ons verder helpen bij het vereenvoudigen van ingewikkelde logica zoals heuristieken of roteren rond een zeshoekige cel. 

Zoals je misschien al geraden hebt uit de naam, heeft het kubieke systeem drie waarden. De derde k of z waarde is afgeleid van de vergelijking x + y + z = 0, waar X en Y zijn de axiale coördinaten. Dit leidt ons naar deze eenvoudige methode om het te berekenen z waarde.

functie calculationCubicZ (axialPoint) return -axialPoint.x-axialPoint.y; 

De vergelijking x + y + z = 0 is eigenlijk een 3D-vlak dat de diagonaal van een driedimensionaal kubusraster passeert. Als u alle drie de waarden voor het raster weergeeft, resulteert dit in de volgende afbeeldingen voor de verschillende zeshoekige uitlijningen.

De blauwe lijn geeft de tegels aan waar de z waarde blijft hetzelfde. 

3. Voordelen van het nieuwe coördinatensysteem

Je vraagt ​​je misschien af ​​hoe deze nieuwe coördinatensystemen ons helpen met hexagonale logica. Ik zal een paar voordelen uitleggen voordat we verder gaan met het creëren van een zeshoekige Tetris met behulp van onze nieuwe kennis.

Beweging

Laten we de middelste tegel in bovenstaande afbeelding bekijken, die kubieke coördinaatwaarden heeft van 3,6, -9. We hebben gemerkt dat één coördinaatwaarde hetzelfde blijft voor de tegels op de gekleurde lijnen. Verder kunnen we zien dat de resterende coördinaten met 1 stijgen of dalen tijdens het volgen van een van de gekleurde lijnen. Bijvoorbeeld, als de X waarde blijft hetzelfde en de Y waarde neemt toe met 1 in een richting, de z de waarde neemt met 1 af om aan onze leidende vergelijking te voldoen x + y + z = 0. Deze functie maakt het besturen van bewegingen veel gemakkelijker. We zullen dit in het tweede deel van de serie gebruiken.

Buren

Volgens dezelfde logica is het eenvoudig om de buren te vinden voor tegels x, y, z. Door te houden X hetzelfde, we krijgen twee diagonale buren, x, y-1, z + 1 en x, y + 1, z-1. Door y hetzelfde te houden, krijgen we twee verticale buren, x-1, y, z + 1 en x + 1, y, z-1. Door z hetzelfde te houden, krijgen we de overgebleven twee diagonale buren, x + 1, y-1, z en x-1, y + 1, z. De onderstaande afbeelding illustreert dit voor een tegel aan de oorsprong.

Het is zoveel gemakkelijker nu we geen andere logica hoeven te gebruiken op basis van even of oneven rijen / kolommen.

Rond een tegel bewegen

Een interessant ding om op te merken in de bovenstaande afbeelding is een soort cyclische symmetrie voor alle tegels rond de rode tegel. Als we de coördinaten van een aangrenzende tegel nemen, kunnen de coördinaten van de aangrenzende tegel worden verkregen door de coördinaatwaarden naar links of rechts te verplaatsen en vervolgens te vermenigvuldigen met -1. 

De bovenste buur heeft bijvoorbeeld de waarde -1,0,1, die bij eenmaal rechts draaien wordt 1, -1,0 en na vermenigvuldiging met -1 wordt -1,1,0, welke de coördinaat is van de rechterbuurman. Linksom draaien en vermenigvuldigen met -1 opbrengsten 0, -1,1, welke de coördinaat is van de linkerbuurman. Door dit te herhalen, kunnen we tussen alle aangrenzende tegels rond de middelste tegel springen. Dit is een zeer interessante functie die kan helpen bij logica en algoritmen. 

Merk op dat dit alleen gebeurt vanwege het feit dat de middelste tegel wordt beschouwd als de oorsprong. We kunnen gemakkelijk elke tegel maken x, y, z om van de oorsprong te zijn door de waarden af ​​te trekken  X, Y en z van het en alle andere tegels.

heuristiek

Het berekenen van efficiënte heuristieken is essentieel als het gaat om pathfinding of vergelijkbare algoritmen. Kubieke coördinaten maken het eenvoudiger om eenvoudige heuristieken voor hexagonale roosters te vinden vanwege de bovengenoemde aspecten. We zullen dit in het tweede deel van deze serie in detail bespreken.

Dit zijn enkele van de voordelen van het nieuwe coördinatensysteem. We zouden een mix van de verschillende coördinatenstelsels kunnen gebruiken in onze praktische implementaties. De tweedimensionale array is bijvoorbeeld nog steeds de beste manier om de niveaudata op te slaan, waarvan de coördinaten de offsetcoördinaten zijn. 

Laten we proberen om met behulp van deze nieuwe kennis een zeshoekige versie van het beroemde Tetris-spel te maken.

4. Een hexagonale tetris maken

We hebben allemaal Tetris gespeeld en als je een game-ontwikkelaar bent, heb je misschien ook je eigen versie gemaakt. Tetris is een van de eenvoudigste tegelgebaseerde spellen die je kunt implementeren, behalve tic tac toe of dammen, met behulp van een eenvoudige tweedimensionale array. Laten we eerst de functies van Tetris opnoemen.

  • Het begint met een leeg tweedimensionaal raster.
  • Aan de bovenkant verschijnen verschillende blokken en één tegel tegelijk naar beneden totdat ze de bodem bereiken.
  • Zodra ze de bodem bereiken, worden ze daar gecementeerd of worden ze niet-interactief. Kortom, ze worden onderdeel van het netwerk.
  • Terwijl u naar beneden valt, kan het blok zijwaarts worden verschoven, met de klok mee / tegen de klok in worden geroteerd en naar beneden worden gehaald.
  • Het doel is om alle tegels in een rij te vullen, waarna de hele rij verdwijnt, waarbij de rest van het gevulde raster erop wordt neergevouwen.
  • Het spel eindigt wanneer er geen vrije tegels meer op de top staan ​​om een ​​nieuw blok in het rooster te krijgen.

Vertegenwoordigen de verschillende blokken

Omdat het spel blokken bevat die verticaal vallen, gebruiken we een verticaal gericht hexagonaal raster. Dit betekent dat ze zijwaarts bewegen ze op een zigzag manier doen bewegen. Een volledige rij in het raster bestaat uit een reeks tegels in zig-zag volgorde. Vanaf dit punt kunt u beginnen te verwijzen naar de broncode die bij deze zelfstudie is meegeleverd. 

De niveaugegevens worden opgeslagen in een tweedimensionale array met de naam levelData, en de rendering gebeurt met behulp van de offset-coördinaten, zoals uitgelegd in de hexagonale mijnenveger-zelfstudie. Raadpleeg het als u problemen ondervindt bij het volgen van de code. 

Het interactieve element in de volgende sectie toont de verschillende blokken die we gaan gebruiken. Er is nog een extra blok, dat bestaat uit drie gevulde tegels die verticaal zijn uitgelijnd als een pilaar. BlockData wordt gebruikt om de verschillende blokken te maken. 

functie 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; 

Een lege bloksjabloon is een set van zeven tegels bestaande uit een middelste tegel omringd door zijn zes buren. Voor elk Tetris-blok wordt de middelste tegel altijd gevuld, aangegeven met de waarde van 1, terwijl een lege tegel zou worden aangeduid met een waarde van 0. De verschillende blokken worden gemaakt door de tegels van te vullen BlockData zoals hieronder.

var block1 = nieuwe BlockData (1,1,0,0,0,1); var block2 = nieuwe BlockData (0,1,0,0,0,1); var block3 = nieuwe BlockData (1,1,0,0,0,0); var block4 = nieuwe BlockData (1,1,0,1,0,0); var block5 = nieuwe BlockData (1,0,0,1,0,1); var block6 = nieuwe BlockData (0,1,1,0,1,1); var block7 = nieuwe BlockData (1,0,0,1,0,0);

We hebben in totaal zeven verschillende blokken.

De blokken roteren

Laat me je laten zien hoe de blokken roteren met behulp van het interactieve element hieronder. Tik en houd ingedrukt om de blokken te draaien en tik op X om de draairichting te veranderen.

Om het blok te roteren, moeten we alle tegels vinden met een waarde van 1, stel de waarde in op 0, draai een keer rond de middelste tegel om de aangrenzende tegel te vinden en stel de waarde ervan in 1. Om een ​​steen rond een andere tegel te draaien, kunnen we de logica gebruiken die in de rond een tegel bewegen sectie hierboven. Voor dit doel komen we uit op de onderstaande methode.

functie rotateTileAroundTile (tileToRotate, anchorTile) tileToRotate = offsetToAxial (tileToRotate); // convert to axial var tileToRotateZ = calculationCubicZ (tileToRotate); // find z value anchorTile = offsetToAxial (anchorTile); // convert to axial var anchorTileZ = calculationCubicZ ( anchorTile); // find z value tileToRotate.x = tileToRotate.x-anchorTile.x; // find x difference tileToRotate.y = tileToRotate.y-anchorTile.y; // find y difference tileToRotateZ = tileToRotateZ-anchorTileZ; // find z difference var pointArr = [tileToRotate.x, tileToRotate.y, tileToRotateZ]; // matrix vullen om te roteren pointArr = arrayRotate (pointArr, clockWise); // array roteren, true voor clockwise tileToRotate.x = (- 1 * pointArr [0]) + anchorTile.x; // vermenigvuldig met -1 & verwijder het x verschil tileToRotate.y = (- 1 * pointArr [1]) + anchorTile.y; // vermenigvuldig met -1 & verwijder het y verschil tileToRotate = axialToOffset (tileToRotate); // convert to offset return tileToRotate;  // ... functie arrayRotate (arr, reverse) // handige methode om array-elementen te roteren als (reverse) arr.unshift (arr.pop ()) else arr.push (arr.shift ()) return arr 

De variabele met de klok mee wordt gebruikt om met de klok mee of tegen de klok in te roteren, wat wordt bereikt door de arraywaarden in tegengestelde richting te verplaatsen arrayRotate.

Het blok verplaatsen

We volgen de ik en j offset coördinaten voor de middelste tegel van het blok met behulp van de variabelen blockMidRowValue en blockMidColumnValue respectievelijk. Om het blok te verplaatsen, verhogen of verlagen we deze waarden. We werken de corresponderende waarden bij in levelData met de blokwaarden met behulp van de paintBlock methode. De bijgewerkte levelData wordt gebruikt om de scène weer te geven na elke statuswijziging.

var blockMidRowValue; var blockMidColumnValue; // ... function moveLeft () blockMidColumnValue--;  functie moveRight () blockMidColumnValue ++;  function dropDown () paintBlock (true); blockMidRowValue ++;  functie paintBlock () clockWise = true; var val = 1; changeLevelData (blockMidRowValue, blockMidColumnValue, val); var rotatingTile = nieuwe Phaser.Point (blockMidRowValue-1, blockMidColumnValue); if (currentBlock.tBlock == 1) changeLevelData (rotatingTile.x, rotatingTile.y, val * currentBlock.tBlock);  var midPoint = nieuwe Phaser.Point (blockMidRowValue, blockMidColumnValue); rotatingTile = rotateTileAroundTile (rotatingTile, middelpunt); if (currentBlock.trBlock == 1) changeLevelData (rotatingTile.x, rotatingTile.y, val * currentBlock.trBlock);  midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotatingTile = rotateTileAroundTile (rotatingTile, middelpunt); if (currentBlock.brBlock == 1) changeLevelData (rotatingTile.x, rotatingTile.y, val * currentBlock.brBlock);  midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotatingTile = rotateTileAroundTile (rotatingTile, middelpunt); if (currentBlock.bBlock == 1) changeLevelData (rotatingTile.x, rotatingTile.y, val * currentBlock.bBlock);  midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotatingTile = rotateTileAroundTile (rotatingTile, middelpunt); if (currentBlock.blBlock == 1) changeLevelData (rotatingTile.x, rotatingTile.y, val * currentBlock.blBlock);  midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotatingTile = rotateTileAroundTile (rotatingTile, middelpunt); if (currentBlock.tlBlock == 1) changeLevelData (rotatingTile.x, rotatingTile.y, val * currentBlock.tlBlock);  functie changeLevelData (iVal, jVal, newValue, erase) if (! validIndexes (iVal, jVal)) terug; if (erase) if (levelData [iVal] [jVal] == 1) levelData [iVal] [jVal] = 0;  else levelData [iVal] [jVal] = newValue;  functie validIndexes (iVal, jVal) if (iVal<0 || jVal<0 || iVal>= levelData.length || jVal> = levelData [0] .length) return false;  return true;  

Hier, currentBlock wijst naar de blockData in de scène. In paintBlock, eerst stellen we de levelData waarde voor de middelste tegel van het blok naar 1 zoals het altijd is 1 voor alle blokken. De index van het middelpunt is blockMidRowValueblockMidColumnValue

Dan gaan we naar de levelData index van de tegel bovenop de middelste tegel  blockMidRowValue-1,  blockMidColumnValue, en zet het op 1 als het blok deze tegel heeft als 1. Vervolgens draaien we eenmaal in de middelste tegel met de klok mee om de volgende tegel te krijgen en hetzelfde proces te herhalen. Dit wordt gedaan voor alle tegels rond de middelste tegel voor het blok.

Valid Operations controleren

Tijdens het verplaatsen of roteren van het blok, moeten we controleren of dat een geldige bewerking is. We kunnen het blok bijvoorbeeld niet verplaatsen of draaien als de tegels die het moet bezetten al bezet zijn. We kunnen het blok ook niet buiten ons tweedimensionale raster verplaatsen. We moeten ook controleren of het blok verder kan vallen, wat zou bepalen of we het blok moeten cementeren of niet. 

Voor al deze, gebruik ik een methode canMove (i, j), die een booleaanse waarde retourneert die aangeeft of het blok wordt geplaatst i, j is een geldige zet. Voor elke bewerking, voordat u daadwerkelijk de levelData waarden, controleren we of de nieuwe positie voor het blok een geldige positie is met behulp van deze methode.

function canMove (iVal, jVal) var validMove = true; var store = clockWise; var newBlockMidPoint = nieuwe Phaser.Point (blockMidRowValue + iVal, blockMidColumnValue + jVal); ClockWise = true; if (! validAndEmpty (newBlockMidPoint.x, newBlockMidPoint.y)) // controleer halverwege, altijd 1 validMove = false;  var rotatingTile = nieuwe Phaser.Point (newBlockMidPoint.x-1, newBlockMidPoint.y); if (currentBlock.tBlock == 1) if (! validAndEmpty (rotatingTile.x, rotatingTile.y)) // controleer top validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); if (currentBlock.trBlock == 1) if (! validAndEmpty (rotatingTile.x, rotatingTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); if (currentBlock.brBlock == 1) if (! validAndEmpty (rotatingTile.x, rotatingTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); if (currentBlock.bBlock == 1) if (! validAndEmpty (rotatingTile.x, rotatingTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); if (currentBlock.blBlock == 1) if (! validAndEmpty (rotatingTile.x, rotatingTile.y)) validMove = false;  newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotatingTile = rotateTileAroundTile (rotatingTile, newBlockMidPoint); if (currentBlock.tlBlock == 1) if (! validAndEmpty (rotatingTile.x, rotatingTile.y)) validMove = false;  clockWise = opslaan; return validMove;  function validAndEmpty (iVal, jVal) if (! validIndexes (iVal, jVal)) return false;  else if (levelData [iVal] [jVal]> 1) // occuppied return false;  return true; 

Het proces is hier hetzelfde als paintBlock, maar in plaats van het veranderen van waarden, geeft dit gewoon een booleaanse waarde die een geldige zet aangeeft. Hoewel ik de rotatie rond een middelste tegel logica om de buren te vinden, het gemakkelijker en vrij efficiënte alternatief is om de directe coördinaatwaarden van de buren te gebruiken, die eenvoudig bepaald kunnen worden uit de middelste tegelcoördinaten.

Het spel weergeven

Het spelniveau wordt visueel weergegeven met een RenderTexture genaamd gameScene. In de array levelData, een onbezette tegel heeft een waarde van 0, en een bezette tegel heeft een waarde van 2 of hoger. 

Een gecementeerd blok wordt aangeduid met een waarde van 2, en een waarde van 5 geeft een tegel aan die moet worden verwijderd omdat deze deel uitmaakt van een voltooide rij. Een waarde van 1 betekent dat de tegel deel uitmaakt van het blok. Na elke wijziging van de spelstatus, maken we het niveau met behulp van de informatie in levelData, zoals hieronder getoond.

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

Vandaar een waarde van 0 wordt weergegeven zonder enige tint, een waarde van 1 wordt weergegeven met een rode tint, een waarde van 2 wordt weergegeven met een blauwe tint en een waarde van 5 wordt weergegeven met groene tint.

5. Het voltooide spel

Alles bij elkaar krijgen we het voltooide hexagonale Tetris-spel. Ga alsjeblieft door de broncode om de volledige implementatie te begrijpen. U zult merken dat we zowel offsetcoördinaten als kubieke coördinaten gebruiken voor verschillende doeleinden. Om bijvoorbeeld te achterhalen of een rij is voltooid, maken we gebruik van offsetcoördinaten en vinkt u het selectievakje aan levelData rijen.

Conclusie

Dit is het eerste deel van de serie. We hebben met succes een hexagonaal Tetris-spel gemaakt met een combinatie van offsetcoördinaten, axiale coördinaten en kubuscoordinaten. 

In het afsluitende deel van de serie leren we over het verplaatsen van personages met behulp van de nieuwe coördinaten op een horizontaal uitgelijnd zeshoekig raster.