Der im Hexagonal Minesweeper-Tutorial beschriebene grundlegende Ansatz mit hexagonalen Kacheln erledigt die Arbeit zwar, ist aber nicht sehr effizient. Es verwendet die direkte Konvertierung aus den zweidimensionalen Array-basierten Pegeldaten und den Bildschirmkoordinaten, wodurch die Ermittlung der getappten Kacheln unnötig kompliziert wird.
Die Notwendigkeit, unterschiedliche Logik in Abhängigkeit von der ungeraden oder geraden Zeile / Spalte einer Kachel zu verwenden, ist ebenfalls nicht zweckmäßig. In dieser Lernreihe werden alternative Bildschirmkoordinatensysteme untersucht, mit denen die Logik vereinfacht und die Bedienung komfortabler gestaltet werden kann. Ich würde dringend empfehlen, dass Sie das Hexagonal Minesweeper-Tutorial lesen, bevor Sie mit diesem Tutorial fortfahren, da dieses die Rasterwiedergabe basierend auf einem zweidimensionalen Array erklärt.
Der Standardansatz für Bildschirmkoordinaten im Hexagonal Minesweeper-Lernprogramm wird als Versatzkoordinatenansatz bezeichnet. Dies liegt daran, dass die alternativen Zeilen oder Spalten beim Ausrichten des hexagonalen Gitters um einen Wert versetzt werden.
Um Ihr Gedächtnis aufzufrischen, beziehen Sie sich bitte auf das Bild unten, das die horizontale Ausrichtung mit angezeigten Koordinatenwerten zeigt.
Im obigen Bild eine Zeile mit der gleichen ich
Der Wert wird rot hervorgehoben und eine Spalte mit demselben Wert j
Der Wert wird grün hervorgehoben. Der Einfachheit halber werden wir die ungeraden und ungeraden Varianten nicht diskutieren, da beide nur unterschiedliche Wege sind, um das gleiche Ergebnis zu erzielen.
Lassen Sie mich eine bessere Bildschirmkoordinatenalternative vorstellen, die axiale Koordinate. Die Umwandlung einer Versatzkoordinate in eine axiale Variante ist sehr einfach. Das ich
Wert bleibt derselbe, aber der j
Der Wert wird mithilfe der Formel konvertiert axialJ = i - Boden (j / 2)
. Eine einfache Methode kann zum Konvertieren eines Offsets verwendet werden Phaser.Point
zu seiner axialen Variante, wie unten gezeigt.
function offsetToAxial (offsetPoint) offsetPoint.y = (offsetPoint.y- (Math.floor (offsetPoint.x / 2))); return offsetPoint;
Die umgekehrte Umwandlung würde wie folgt aussehen.
Funktion axialToOffset (axialPoint) axialPoint.y = (axialPoint.y + (Math.floor (axialPoint.x / 2))); return axialPoint;
Hier die x
Wert ist der ich
Wert und y
Wert ist der j
Wert für das zweidimensionale Array. Nach der Konvertierung würden die neuen Werte wie im Bild unten aussehen.
Beachten Sie, dass die grüne Linie dort ist j
Der Wert bleibt derselbe, der nicht mehr im Zickzack verläuft, sondern ist jetzt eine Diagonale zu unserem sechseckigen Gitter.
Für das vertikal ausgerichtete Sechseckraster werden die Versatzkoordinaten im Bild unten angezeigt.
Die Umrechnung in axiale Koordinaten erfolgt nach den gleichen Gleichungen, mit dem Unterschied, dass wir die beibehalten j
Wert gleich und ändern Sie die ich
Wert. Die Methode unten zeigt die Konvertierung.
function offsetToAxial (offsetPoint) offsetPoint.x = (offsetPoint.x- (Math.floor (offsetPoint.y / 2))); return offsetPoint;
Das Ergebnis ist wie unten gezeigt.
Bevor wir die neuen Koordinaten verwenden, um Probleme zu lösen, möchte ich Sie kurz mit einer anderen Bildschirmkoordinaten-Alternative vertraut machen: Würfelkoordinaten.
Durch das Aufrichten des Zickzacks selbst wurden möglicherweise die meisten Unannehmlichkeiten gelöst, die wir mit dem Offset-Koordinatensystem hatten. Würfel- oder Kubikkoordinaten würden uns weiter dabei helfen, komplizierte Logik wie Heuristiken oder das Drehen um eine hexagonale Zelle zu vereinfachen.
Wie Sie vielleicht anhand des Namens erraten haben, hat das kubische System drei Werte. Der dritte k
oder z
Wert wird aus der Gleichung abgeleitet x + y + z = 0
, woher x
und y
sind die axialen Koordinaten. Dies führt uns zu dieser einfachen Methode zur Berechnung der z
Wert.
Funktion berechneCubicZ (axialPoint) return -axialPoint.x-axialPoint.y;
Die gleichung x + y + z = 0
ist eigentlich eine 3D-Ebene, die durch die Diagonale eines dreidimensionalen Würfelgitters verläuft. Wenn Sie alle drei Werte für das Raster anzeigen, werden die folgenden Bilder für die verschiedenen hexagonalen Ausrichtungen angezeigt.
Die blaue Linie zeigt die Kacheln an, an denen die z
Wert bleibt gleich.
Sie fragen sich vielleicht, wie uns diese neuen Koordinatensysteme bei der hexagonalen Logik unterstützen. Ich werde einige Vorteile erläutern, bevor wir mit unserem neuen Wissen einen sechseckigen Tetris erstellen.
Betrachten wir die mittlere Kachel im Bild oben, die kubische Koordinatenwerte von hat 3,6, -9
. Wir haben festgestellt, dass ein Koordinatenwert für die Kacheln auf den farbigen Linien gleich bleibt. Außerdem können wir sehen, dass die verbleibenden Koordinaten um 1 steigen oder sinken, während Sie eine der farbigen Linien verfolgen. Zum Beispiel, wenn die x
Wert bleibt derselbe und der y
Wert steigt entlang einer Richtung um 1, der z
Der Wert sinkt um 1, um unsere herrschende Gleichung zu erfüllen x + y + z = 0
. Diese Funktion erleichtert das Steuern der Bewegung. Wir werden dies im zweiten Teil der Serie einsetzen.
Nach der gleichen Logik ist es einfach, die Nachbarn für die Fliese zu finden x, y, z
. Durch das Behalten x
Das gleiche gilt für zwei diagonale Nachbarn, x, y-1, z + 1
und x, y + 1, z-1
. Indem wir dasselbe beibehalten, bekommen wir zwei vertikale Nachbarn, x-1, y, z + 1
und x + 1, y, z-1
. Wenn wir z gleich halten, erhalten wir die restlichen zwei diagonalen Nachbarn, x + 1, y-1, z
und x-1, y + 1, z
. Das Bild unten zeigt dies für eine Kachel am Ursprung.
Es ist jetzt so viel einfacher, dass wir keine andere Logik verwenden müssen, die auf geraden oder ungeraden Zeilen / Spalten basiert.
Eine interessante Sache, die im obigen Bild zu beachten ist, ist eine Art zyklischer Symmetrie für alle Kacheln um die rote Kachel. Wenn wir die Koordinaten eines benachbarten Kachels nehmen, können die Koordinaten des unmittelbar benachbarten Kachels erhalten werden, indem die Koordinatenwerte entweder nach links oder nach rechts verschoben und dann mit -1 multipliziert werden.
Zum Beispiel hat der oberste Nachbar einen Wert von -1,0,1
, die auf rechts drehen wird einmal 1, -1,0
und nach Multiplikation mit -1 wird -1,1,0
, Das ist die Koordinate des rechten Nachbarn. Nach links drehen und mit -1 multiplizieren 0, -1,1
, Das ist die Koordinate des linken Nachbarn. Indem wir dies wiederholen, können wir zwischen allen benachbarten Plättchen um das mittlere Plättchen springen. Dies ist eine sehr interessante Funktion, die Logik und Algorithmen unterstützen kann.
Beachten Sie, dass dies nur dadurch geschieht, dass die mittlere Kachel als Ursprung betrachtet wird. Wir könnten leicht jede Fliese machen x, y, z
durch Abzug der Werte am Ursprung zu sein x
, y
und z
von ihm und allen anderen Fliesen.
Die Berechnung effizienter Heuristiken ist der Schlüssel, wenn es um Wegfindungs-oder ähnliche Algorithmen geht. Kubische Koordinaten erleichtern das Auffinden einfacher Heuristiken für Sechseckraster aufgrund der oben genannten Aspekte. Wir werden dies im zweiten Teil dieser Serie ausführlich besprechen.
Dies sind einige der Vorteile des neuen Koordinatensystems. In unseren praktischen Implementierungen könnten wir eine Mischung der verschiedenen Koordinatensysteme verwenden. Beispielsweise ist das zweidimensionale Array immer noch der beste Weg, um die Ebenendaten zu speichern, deren Koordinaten die Versatzkoordinaten sind.
Versuchen wir mit diesem neuen Wissen eine sechseckige Version des berühmten Tetris-Spiels zu erstellen.
Wir haben alle Tetris gespielt, und wenn Sie Spieleentwickler sind, haben Sie möglicherweise auch eine eigene Version erstellt. Tetris ist eines der einfachsten Kachel-basierten Spiele, die Sie - abgesehen von Tic Tac Toe oder Checkers - mithilfe eines einfachen zweidimensionalen Arrays implementieren können. Lassen Sie uns zunächst die Funktionen von Tetris auflisten.
Da das Spiel Blöcke vertikal fallen lässt, verwenden wir ein vertikal ausgerichtetes hexagonales Gitter. Wenn Sie sie seitwärts bewegen, bewegen Sie sich im Zickzack. Eine vollständige Reihe im Raster besteht aus einer Reihe von Fliesen in Zickzack-Reihenfolge. Von diesem Punkt an können Sie mit dem Quellcode beginnen, der zusammen mit diesem Tutorial bereitgestellt wird.
Die Pegeldaten werden in einem zweidimensionalen Array mit dem Namen gespeichert levelData
, und das Rendern erfolgt unter Verwendung der Versatzkoordinaten, wie im Hexagonal Minesweeper-Tutorial erläutert. Bitte beziehen Sie sich darauf, wenn Sie Schwierigkeiten haben, den Code zu befolgen.
Das interaktive Element im nächsten Abschnitt zeigt die verschiedenen Blöcke, die wir verwenden werden. Es gibt einen weiteren zusätzlichen Block, der aus drei gefüllten Fliesen besteht, die vertikal wie eine Säule ausgerichtet sind. BlockData
wird verwendet, um die verschiedenen Blöcke zu erstellen.
function 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;
Eine leere Blockvorlage besteht aus sieben Kacheln, die aus einer mittleren Kachel bestehen, die von sechs Nachbarn umgeben ist. Bei jedem Tetris-Block wird die mittlere Kachel immer mit einem Wert von gefüllt 1
, Eine leere Kachel würde dagegen mit einem Wert von bezeichnet werden 0
. Die verschiedenen Blöcke werden durch Auffüllen der Kacheln von erstellt BlockData
wie nachstehend.
var block1 = neue BlockData (1,1,0,0,0,1); var block2 = neue BlockData (0,1,0,0,0,1); var block3 = neue BlockData (1,1,0,0,0,0); var block4 = new BlockData (1,1,0,1,0,0); var block5 = neue BlockData (1,0,0,1,0,1); var block6 = neue BlockData (0,1,1,0,1,1); var block7 = neue BlockData (1,0,0,1,0,0);
Wir haben insgesamt sieben verschiedene Blöcke.
Lassen Sie mich Ihnen zeigen, wie sich die Blöcke drehen, indem Sie das interaktive Element verwenden. Tippen und halten Sie, um die Blöcke zu drehen, und tippen Sie auf x
die Drehrichtung ändern.
Um den Block zu drehen, müssen wir alle Kacheln finden, die einen Wert von haben 1
, setze den Wert auf 0
, Drehen Sie einmal um die mittlere Kachel, um die benachbarte Kachel zu finden, und legen Sie deren Wert auf fest 1
. Um eine Kachel um eine andere Kachel zu drehen, können wir die in der sich um eine Fliese bewegen Abschnitt oben. Zu diesem Zweck kommen wir zu der unten angegebenen Methode.
function rotateTileAroundTile (tileToRotate, anchorTile) tileToRotate = offsetToAxial (tileToRotate); // in axiale Variable konvertieren tileToRotateZ = berechneCubicZ (tileToRotate); anchorTile); // z-Wert finden tileToRotate.x = tileToRotate.x-anchorTile.x; // find x Unterschied tileToRotate.y = tileToRotate.y-anchorTile.y; // find y Unterschied tileToRotateZ = tileToRotateZ-anchorTileZ; // z-Unterschied finden var pointArr = [tileToRotate.x, tileToRotate.y, tileToRotateZ]; // Array zum Drehen von pointArr = arrayRotate (pointArr, clockWise) füllen; // Array drehen, true für clockwise tileToRotate.x = (- 1 * pointArr) [0]) + anchorTile.x; // Multiplizieren mit -1 und Entfernen der x-Differenz tileToRotate.y = (- 1 * pointArr [1]) + anchorTile.y; // Multiplizieren mit -1 und Entfernen der y-Differenz tileToRotate = axialToOffset (tileToRotate); // in Offset konvertieren return tileToRotate; //… Funktion arrayRotate (arr, reverse) // einfache Methode zum Drehen von Arrayelementen if (reverse) arr.unshift (arr.pop ()) else arr.push (arr.shift ()) return arr
Die Variable im Uhrzeigersinn
wird verwendet, um im Uhrzeigersinn oder gegen den Uhrzeigersinn zu drehen. Dies geschieht durch Verschieben der Array-Werte in entgegengesetzte Richtungen arrayRotate
.
Wir behalten den Überblick ich
und j
Versatzkoordinaten für die mittlere Kachel des Blocks unter Verwendung der Variablen blockMidRowValue
und blockMidColumnValue
beziehungsweise. Um den Block zu verschieben, erhöhen oder verringern wir diese Werte. Wir aktualisieren die entsprechenden Werte in levelData
mit den Blockwerten mit der paintBlock
Methode. Die aktualisiert levelData
wird verwendet, um die Szene nach jeder Statusänderung zu rendern.
var blockMidRowValue; var blockMidColumnValue; //… function moveLeft () blockMidColumnValue--; function moveRight () blockMidColumnValue ++; function dropDown () paintBlock (true); blockMidRowValue ++; Funktion paintBlock () clockWise = true; var val = 1; changeLevelData (blockMidRowValue, blockMidColumnValue, val); var rotaryTile = new Phaser.Point (blockMidRowValue-1, blockMidColumnValue); if (currentBlock.tBlock == 1) changeLevelData (rotierendeTile.x, rotierendeTile.y, val * currentBlock.tBlock); var midPoint = new Phaser.Point (blockMidRowValue, blockMidColumnValue); rotaryTile = rotateTileAroundTile (rotierendeTile, midPoint); if (currentBlock.trBlock == 1) changeLevelData (rotierendeTile.x, rotierendeTile.y, val * currentBlock.trBlock); midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotaryTile = rotateTileAroundTile (rotierendeTile, midPoint); if (currentBlock.brBlock == 1) changeLevelData (rotierendeTile.x, rotierendeTile.y, val * currentBlock.brBlock); midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotaryTile = rotateTileAroundTile (rotierendeTile, midPoint); if (currentBlock.bBlock == 1) changeLevelData (rotierendeTile.x, rotierendeTile.y, val * currentBlock.bBlock); midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotaryTile = rotateTileAroundTile (rotierendeTile, midPoint); if (currentBlock.blBlock == 1) changeLevelData (rotierendeTile.x, rotierendeTile.y, val * currentBlock.blBlock); midPoint.x = blockMidRowValue; midPoint.y = blockMidColumnValue; rotaryTile = rotateTileAroundTile (rotierendeTile, midPoint); if (currentBlock.tlBlock == 1) changeLevelData (rotierendeTile.x, rotierendeTile.y, val * aktuelleblock.tlBlock); function changeLevelData (iVal, jVal, newValue, Erase) if (! validIndexes (iVal, jVal)) return; if (löschen) if (levelData [iVal] [jVal] == 1) levelData [iVal] [jVal] = 0; else levelData [iVal] [jVal] = neuer Wert; Funktion validIndexes (iVal, jVal) if (iVal<0 || jVal<0 || iVal>= levelData.length || jVal> = levelData [0] .length) return false; return true;
Hier, currentBlock
zeigt auf die blockData
in der Szene Im paintBlock
, Zuerst setzen wir die levelData
Wert für die mittlere Kachel des Blocks bis 1
wie es immer ist 1
für alle Blöcke. Der Index des Mittelpunkts ist blockMidRowValue
, blockMidColumnValue
.
Dann gehen wir zum levelData
Index der Kachel oben auf der mittleren Kachel blockMidRowValue-1
, blockMidColumnValue
, und setze es auf 1
wenn der Block diese Kachel als hat 1
. Dann drehen wir uns einmal im Uhrzeigersinn um die mittlere Kachel, um die nächste Kachel zu erhalten und wiederholen den gleichen Vorgang. Dies wird für alle Kacheln um die mittlere Kachel des Blocks ausgeführt.
Beim Verschieben oder Drehen des Blocks müssen wir prüfen, ob dies eine gültige Operation ist. Zum Beispiel können wir den Block nicht bewegen oder drehen, wenn die zu besetzenden Kacheln bereits belegt sind. Außerdem können wir den Block nicht außerhalb unseres zweidimensionalen Gitters verschieben. Wir müssen auch prüfen, ob der Block weiter fallen kann. Dies würde bestimmen, ob wir den Block zementieren müssen oder nicht.
Für alle diese benutze ich eine Methode canMove (i, j)
, was einen booleschen Wert zurückgibt, der angibt, ob der Block auf gesetzt wird ich, j
ist ein gültiger Zug. Bei jeder Operation vor dem eigentlichen Wechsel levelData
Werte überprüfen wir mit dieser Methode, ob die neue Position für den Block eine gültige Position ist.
Funktion canMove (iVal, jVal) var validMove = true; var store = clockWise; var newBlockMidPoint = new Phaser.Point (blockMidRowValue + iVal, blockMidColumnValue + jVal); clockWise = true; if (! validAndEmpty (newBlockMidPoint.x, newBlockMidPoint.y)) // check mid, immer 1 validMove = false; var rotaryTile = new Phaser.Point (newBlockMidPoint.x-1, newBlockMidPoint.y); if (currentBlock.tBlock == 1) if (! validAndEmpty (rotierendeTile.x, rotierendeTile.y)) // check top validMove = false; newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotaryTile = rotateTileAroundTile (rotierendeTile, newBlockMidPoint); if (currentBlock.trBlock == 1) if (! validAndEmpty (rotierendeTile.x, rotierendeTile.y)) validMove = false; newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotaryTile = rotateTileAroundTile (rotierendeTile, newBlockMidPoint); if (currentBlock.brBlock == 1) if (! validAndEmpty (rotierendeTile.x, rotierendeTile.y)) validMove = false; newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotaryTile = rotateTileAroundTile (rotierendeTile, newBlockMidPoint); if (currentBlock.bBlock == 1) if (! validAndEmpty (rotierendeTile.x, rotierendeTile.y)) validMove = false; newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotaryTile = rotateTileAroundTile (rotierendeTile, newBlockMidPoint); if (currentBlock.blBlock == 1) if (! validAndEmpty (rotierendeTile.x, rotierendeTile.y)) validMove = false; newBlockMidPoint.x = blockMidRowValue + iVal; newBlockMidPoint.y = blockMidColumnValue + jVal; rotaryTile = rotateTileAroundTile (rotierendeTile, newBlockMidPoint); if (currentBlock.tlBlock == 1) if (! validAndEmpty (rotierendeTile.x, rotierendeTile.y)) validMove = false; clockWise = speichern; return validMove zurückgeben; Funktion validAndEmpty (iVal, jVal) if (! validIndexes (iVal, jVal)) return false; else if (levelData [iVal] [jVal]> 1) // occuppied return false; return true;
Der Prozess hier ist derselbe wie paintBlock
, Anstatt jedoch Werte zu ändern, wird nur ein boolescher Wert zurückgegeben, der eine gültige Bewegung anzeigt. Obwohl ich das benutze Drehung um eine mittlere Fliese Um die Nachbarn zu finden, besteht die einfachere und effizientere Alternative darin, die direkten Koordinatenwerte der Nachbarn zu verwenden, die leicht aus den mittleren Kachelkoordinaten bestimmt werden können.
Das Spielniveau wird visuell durch a dargestellt RenderTexture
genannt gameScene
. Im Array levelData
, ein nicht besetztes Plättchen hätte einen Wert von 0
, und ein besetztes Plättchen hätte einen Wert von 2
oder höher.
Ein zementierter Block wird mit einem Wert von bezeichnet 2
, und einen Wert von 5
bezeichnet eine Kachel, die entfernt werden muss, da sie Teil einer abgeschlossenen Reihe ist. Ein Wert von 1
bedeutet, dass die Kachel Teil des Blocks ist. Nach jeder Änderung des Spielstatus rendern wir das Level anhand der Informationen in levelData
, Wie nachfolgend dargestellt.
//… hexSprite.tint = '0xffffff'; if (levelData [i] [j]> - 1) AxialPoint = offsetToAxial (AxialPoint); cubicZ = berechneCubicZ (axialer Punkt); 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); //…
Daher ein Wert von 0
wird ohne Tönung gerendert, ein Wert von 1
wird mit roter Tönung dargestellt, ein Wert von 2
wird mit blauem Farbton und einem Wert von 5
wird mit grüner Tönung dargestellt.
Wenn wir alles zusammenfügen, bekommen wir das sechseckige Tetris-Spiel. Bitte gehen Sie den Quellcode durch, um die vollständige Implementierung zu verstehen. Sie werden feststellen, dass wir für unterschiedliche Zwecke sowohl Offset-Koordinaten als auch kubische Koordinaten verwenden. Um beispielsweise zu ermitteln, ob eine Zeile abgeschlossen ist, verwenden wir Offsetkoordinaten und überprüfen die levelData
Reihen.
Damit ist der erste Teil der Serie abgeschlossen. Wir haben erfolgreich ein sechseckiges Tetris-Spiel mit einer Kombination aus Versatzkoordinaten, Axialkoordinaten und Würfelkoordinaten erstellt.
Im abschließenden Teil der Serie lernen wir die Bewegung von Charakteren anhand der neuen Koordinaten in einem horizontal ausgerichteten Sechseckraster.