Ich bin mir sicher, dass es möglich ist, ein Tetris-Spiel mit einem Point-and-Click-Gamedev-Tool zu erstellen, aber ich konnte nie herausfinden, wie das geht. Heute denke ich lieber auf einer höheren Abstraktionsebene, wo das Tetromino, das Sie auf dem Bildschirm sehen, nur ein ist Darstellung was im zugrundeliegenden Spiel vorgeht. In diesem Tutorial zeige ich Ihnen, was ich meine, indem ich demonstriere, wie die Kollisionserkennung in Tetris gehandhabt wird.
Hinweis: Obwohl der Code in diesem Lernprogramm unter Verwendung von AS3 geschrieben wurde, sollten Sie in der Lage sein, in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte zu verwenden.
Ein Standard-Tetris-Spielfeld hat 16 Reihen und 10 Spalten. Wir können dies in einem mehrdimensionalen Array darstellen, das 16 Unterarrays mit 10 Elementen enthält:
Stellen Sie sich vor, das Bild auf der linken Seite ist ein Screenshot des Spiels. So könnte das Spiel für den Spieler aussehen, nachdem ein Tetromino gelandet ist, aber bevor ein anderes erscheint.
Auf der rechten Seite befindet sich eine Array-Darstellung des aktuellen Status des Spiels. Nennen wir es mal gelandet[]
, wie es sich auf alle gelandeten Blöcke bezieht. Ein Element von 0
bedeutet, dass kein Block diesen Raum belegt; 1
bedeutet, dass ein Block in diesem Bereich gelandet ist.
Lassen Sie uns nun ein O-Tetromino in der Mitte oben auf dem Feld erscheinen lassen:
Tetromino.Shape = [[1,1], [1,1]]; tetromino.topLeft = Zeile: 0, Spalte: 4;
Das gestalten
Eigenschaft ist eine weitere mehrdimensionale Arraydarstellung der Form dieses Tetrominos. oben links
gibt die Position des oberen linken Blocks des Tetrominos an: in der oberen Reihe und die fünfte Spalte in.
Wir machen alles. Zuerst zeichnen wir den Hintergrund - dies ist einfach, es handelt sich lediglich um ein statisches Rasterbild.
Als nächstes zeichnen wir jeden Block aus dem gelandet[]
Array:
for (var Zeile = 0; Zeile < landed.length; row++) for (var col = 0; col < landed[row].length; col++) if (landed[row][col] != 0) //draw block at position corresponding to row and col //remember, row gives y-position, col gives x-position
Meine Blockbilder sind 20x20px. Um die Blöcke zu zeichnen, könnte ich einfach ein neues Blockbild einfügen (Spalte * 20, Reihe * 20)
. Die Details spielen keine Rolle.
Als Nächstes zeichnen wir jeden Block im aktuellen Tetromino:
for (var Zeile = 0; Zeile < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) //draw block at position corresponding to //row + topLeft.row, and //col + topLeft.col
Wir können hier denselben Zeichnungscode verwenden, aber wir müssen die Blöcke um etwas versetzen oben links
.
Hier ist das Ergebnis:
Beachten Sie, dass das neue O-Tetromino nicht im angezeigt wird gelandet[]
Array - weil es ja noch nicht gelandet ist.
Angenommen, der Player berührt die Steuerelemente nicht. In regelmäßigen Abständen - etwa jede halbe Sekunde - muss der O-Tetromino eine Reihe herunterfallen.
Es ist verlockend, einfach anzurufen:
tetromino.topLeft.row ++;
… Und dann alles noch einmal rendern, aber es werden keine Überlappungen zwischen dem O-Tetromino und den bereits gelandeten Blöcken festgestellt.
Stattdessen prüfen wir zuerst auf mögliche Kollisionen und bewegen den Tetromino nur dann, wenn er "sicher" ist..
Dafür müssen wir eine definieren Potenzial neue Position für den Tetromino:
tetromino.potentialTopLeft = Zeile: 1, Spalte: 4;
Jetzt prüfen wir auf Kollisionen. Der einfachste Weg, dies zu tun, besteht darin, alle Felder im Raster zu durchlaufen, die der Tetromino an seiner potentiellen neuen Position einnehmen würde, und die Option zu überprüfen gelandet[]
Array, um zu sehen, ob sie bereits belegt sind:
for (var Zeile = 0; Zeile < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (landed[row + tetromino.potentialTopLeft.row] != 0 && landed[col + tetromino.potentialTopLeft.col] != 0) //the space is taken
Lass uns das ausprobieren:
Tetromino.Shape = [[1,1], [1,1]]; tetromino.potentialTopLeft: Zeile: 1, Spalte: 4 --------------------------------- ------- Reihe: 0, Spalte: 0, Tetromino.form [0] [0]: 1, gelandet [0 + 1] [0 + 4]: 0 Reihe: 0, Spalte: 1, Tetromino. Form [0] [1]: 1, gelandet [0 + 1] [1 + 4]: 0 Reihe: 1, Spalte: 0, Tetromino.form [1] [0]: 1, gelandet [1 + 1] [ 0 + 4]: 0 Reihe: 1, Spalte: 1, Tetromino.form [1] [1]: 1, gelandet [1 + 1] [1 + 4]: 0
Alle Nullen! Das bedeutet, dass es keine Kollision gibt, sodass sich der Tetromino bewegen kann.
Legen wir fest:
tetromino.topLeft = tetromino.potentialTopLeft;
… Und dann wieder alles rendern:
Großartig!
Nehmen wir an, der Spieler lässt den Tetromino auf diesen Punkt fallen:
Die linke obere ist um Zeile: 11, Spalte: 4
. Wir können sehen, dass der Tetromino mit den gelandeten Blöcken kollidieren würde, wenn er noch mehr fallen würde - aber kann unser Code das herausfinden? Mal schauen:
Tetromino.Shape = [[1,1], [1,1]]; tetromino.potentialTopLeft: Zeile: 12, Spalte: 4 --------------------------------- ------- Reihe: 0, Spalte: 0, Tetromino.shape [0] [0]: 1, gelandet [0 + 12] [0 + 4]: 0 Reihe: 0, Spalte: 1, Tetromino. Form [0] [1]: 1, gelandet [0 + 12] [1 + 4]: 0 Reihe: 1, Spalte: 0, Tetromino.form [1] [0]: 1, gelandet [1 + 12] [ 0 + 4]: 1 Reihe: 1, Spalte: 1, Tetromino-Form [1] [1]: 1, gelandet [1 + 12] [1 + 4]: 0
Da ist ein 1
, was bedeutet, dass es eine Kollision gibt - insbesondere würde der Tetromino mit dem Block bei kollidieren gelandet [13] [4]
.
Das bedeutet, dass der Tetromino gelandet ist, was bedeutet, dass wir ihn dem hinzufügen müssen gelandet[]
Array. Wir können dies mit einer sehr ähnlichen Schleife tun, die wir zur Überprüfung auf mögliche Kollisionen verwendet haben:
for (var Zeile = 0; Zeile < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) landed[row + tetromino.topLeft.row][col + tetromino.topLeft.col] = tetromino.shape[row][col];
Hier ist das Ergebnis:
So weit, ist es gut. Sie haben vielleicht bemerkt, dass wir uns nicht mit dem Fall befassen, in dem der Tetromino auf dem "Boden" landet - wir befassen uns nur mit Tetrominos, die auf anderen Tetrominos landen.
Es gibt eine ziemlich einfache Lösung: Wenn wir nach möglichen Kollisionen suchen, prüfen wir auch, ob sich die potenzielle neue Position jedes Blocks unter dem unteren Rand des Spielfelds befindet:
for (var Zeile = 0; Zeile < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (row + tetromino.potentialTopLeft.row >= landed.length) // Dieser Block wäre unter dem Spielfeld. else if (gelandet [Reihe + Tetromino.potentialTopLeft.row]]! = 0 && landet [Col + Tetromino.potentialTopLeft.col]! = 0) / / der Speicherplatz ist belegt
Wenn ein Block im Tetromino unter dem Boden des Spielfelds landet, wenn er weiter fällt, machen wir den Tetromino natürlich "landen", als ob jeder Block einen bereits gelandeten Block überlappen würde.
Nun können wir die nächste Runde mit einem neuen Tetromino beginnen.
Lassen Sie uns diesmal ein J-Tetromino laichen:
Tetromino-Form = [[0,1], [0,1], [1,1]]; tetromino.topLeft = Zeile: 0, Spalte: 4;
Render es:
Denken Sie daran, dass der Tetromino jede halbe Sekunde um eine Reihe fällt. Nehmen wir an, der Spieler drückt die linke Taste viermal, bevor eine halbe Sekunde vergeht. Wir wollen den Tetromino jedes Mal um eine Spalte verschieben.
Wie können wir sicherstellen, dass der Tetromino mit keinem der gelandeten Blöcke kollidiert? Wir können tatsächlich den gleichen Code von vorher verwenden!
Zunächst ändern wir die potenzielle neue Position:
tetromino.potentialTopLeft = Zeile: tetromino.topLeft, col: tetromino.topLeft - 1;
Jetzt prüfen wir, ob sich die Blöcke im Tetromino mit den gelandeten Blöcken überschneiden, und verwenden dabei die gleiche Grundprüfung wie zuvor (ohne zu prüfen, ob ein Block das Spielfeld unterschritten hat):
for (var Zeile = 0; Zeile < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (landed[row + tetromino.potentialTopLeft.row] != 0 && landed[col + tetromino.potentialTopLeft.col] != 0) //the space is taken
Führen Sie die gleichen Überprüfungen durch, die wir normalerweise durchführen, und Sie werden feststellen, dass dies gut funktioniert. Der große Unterschied ist, dass wir uns daran erinnern müssen nicht um die Blöcke des Tetrominos in die gelandet[]
Array bei einer möglichen Kollision - stattdessen sollten wir den Wert von einfach nicht ändern tetromino.topLeft
.
Jedes Mal, wenn der Spieler den Tetromino bewegt, sollten wir alles neu rendern. Hier ist das Endergebnis:
Was passiert, wenn der Spieler noch einmal schlägt? Wenn wir das nennen:
tetromino.potentialTopLeft = Zeile: tetromino.topLeft, col: tetromino.topLeft - 1;
... wir werden am Ende versuchen zu setzen tetromino.potentialTopLeft.col
zu -1
- und das wird später zu allen möglichen Problemen führen.
Lassen Sie uns unsere bestehende Kollisionsprüfung ändern, um dies zu berücksichtigen:
for (var Zeile = 0; Zeile < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (col + tetromino.potentialTopLeft.col < 0) //this block would be to the left of the playing field if (landed[row + tetromino.potentialTopLeft.row] != 0 && landed[col + tetromino.potentialTopLeft.col] != 0) //the space is taken
Einfach - es ist dieselbe Idee wie bei der Überprüfung, ob einer der Blöcke unter das Spielfeld fallen würde.
Lassen Sie uns auch mit der rechten Seite umgehen:
for (var Zeile = 0; Zeile < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (col + tetromino.potentialTopLeft.col < 0) //this block would be to the left of the playing field if (col + tetromino.potentialTopLeft.col >= gelandet [0] .length) // Dieser Block befindet sich rechts vom Spielfeld, wenn (gelandet [Reihe + tetromino.potentialTopLeft.row]]! = 0 && landet [col + tetromino.potentialTopLeft.col]! = 0) // der Platz ist belegt
Wenn sich das Tetromino außerhalb des Spielfelds bewegt, ändern wir es einfach nicht tetromino.topLeft
- keine Notwendigkeit, etwas anderes zu tun.
Okay, eine halbe Sekunde muss schon vergangen sein, also lassen wir das Tetromino eine Reihe fallen:
Tetromino-Form = [[0,1], [0,1], [1,1]]; tetromino.topLeft = Zeile: 1, Spalte: 0;
Nehmen wir an, der Spieler drückt die Taste, damit sich der Tetromino im Uhrzeigersinn dreht. Das ist eigentlich ziemlich einfach - wir ändern nur tetromino.shape
, ohne zu ändern tetromino.topLeft
:
tetromino.shape = [[1,0,0], [1,1,1]]; tetromino.topLeft = Zeile: 1, Spalte: 0;
Wir könnte Verwenden Sie ein paar Berechnungen, um den Inhalt des Block-Arrays zu drehen. Es ist jedoch viel einfacher, die vier möglichen Rotationen jedes Tetrominos irgendwo zu speichern:
jTetromino.rotations = [[[0,1], [0,1], [1,1]], [[1,0,0], [1,1,1]], [[1,1], [1,0], [1,0]], [[1,1,1], [0,0,1]]];
(Ich lasse Sie herausfinden, wo Sie das am besten in Ihrem Code speichern können!)
Wie auch immer, sobald wir alles wieder gerendert haben, sieht es so aus:
Wir können es wieder drehen (und nehmen wir an, dass wir diese beiden Drehungen innerhalb einer halben Sekunde ausführen):
tetromino.shape = [[1,1], [1,0], [1,0]]; tetromino.topLeft = Zeile: 1, Spalte: 0;
Wieder rendern:
Wunderbar Lassen Sie uns noch ein paar Zeilen fallen, bis wir diesen Zustand erreichen:
tetromino.shape = [[1,1], [1,0], [1,0]]; tetromino.topLeft = Zeile: 10, Spalte: 0;
Plötzlich drückt der Spieler die Schaltfläche "Im Uhrzeigersinn drehen" ohne ersichtlichen Grund erneut. Wenn Sie das Bild betrachten, können wir erkennen, dass dies nichts zulassen sollte. Wir haben jedoch noch keine Überprüfungen eingerichtet, um dies zu verhindern.
Sie können wahrscheinlich raten, wie wir das lösen werden. Wir stellen ein tetromino.potentialShape
, Setzen Sie ihn auf die Form des gedrehten Tetrominos und suchen Sie nach möglichen Überlappungen mit bereits gelandeten Blöcken.
tetromino.shape = [[1,1], [1,0], [1,0]]; tetromino.topLeft = Zeile: 10, Spalte: 0; tetromino.potentialShape = [[1,1,1], [0,0,1]];
for (var Zeile = 0; Zeile < tetromino.potentialShape.length; row++) for (var col = 0; col < tetromino.potentialShape[row].length; col++) if (tetromino.potentialShape[row][col] != 0) if (col + tetromino.topLeft.col < 0) //this block would be to the left of the playing field if (col + tetromino.topLeft.col >= landed [0] .length) // Dieser Block würde sich rechts vom Spielfeld befinden if (row + tetromino.topLeft.row> = landed.length) // dieser Block wäre unter dem Spielfeld if (gelandet [Zeile + tetromino.topLeft.row]! = 0 && landet [col + tetromino.topLeft.col]! = 0) // das Leerzeichen ist belegt
Wenn es eine Überlappung gibt (oder wenn die gedrehte Form teilweise außerhalb der Grenzen liegt), lassen wir den Block einfach nicht drehen. So kann es eine halbe Sekunde später in Position geraten und dem hinzugefügt werden gelandet[]
Array:
Ausgezeichnet.
Um klar zu sein, haben wir jetzt drei separate Überprüfungen.
Die erste Prüfung betrifft den Fall, wenn ein Tetromino fällt, und wird jede halbe Sekunde aufgerufen:
// setze tetromino.potentialTopLeft auf eine Zeile unterhalb von tetromino.topLeft, dann: for (var row = 0; row) < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (row + tetromino.potentialTopLeft.row >= landed.length) // Dieser Block wäre unter dem Spielfeld. else if (gelandet [Reihe + Tetromino.potentialTopLeft.row]]! = 0 && landet [Col + Tetromino.potentialTopLeft.col]! = 0) / / der Speicherplatz ist belegt
Wenn alle Prüfungen bestanden sind, werden wir gesetzt tetromino.topLeft
zu tetromino.potentialTopLeft
.
Wenn eine der Prüfungen fehlschlägt, machen wir das Tetromino wie folgt:
for (var Zeile = 0; Zeile < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) landed[row + tetromino.topLeft.row][col + tetromino.topLeft.col] = tetromino.shape[row][col];
Die zweite Prüfung ist für den Fall, dass der Spieler versucht, den Tetromino nach links oder rechts zu bewegen, und wird aufgerufen, wenn der Spieler die Bewegungstaste drückt:
// setze tetromino.potentialTopLeft auf eine Spalte rechts oder links // von tetromino.topLeft, und dann: for (var row = 0; row < tetromino.shape.length; row++) for (var col = 0; col < tetromino.shape[row].length; col++) if (tetromino.shape[row][col] != 0) if (col + tetromino.potentialTopLeft.col < 0) //this block would be to the left of the playing field if (col + tetromino.potentialTopLeft.col >= gelandet [0] .length) // Dieser Block befindet sich rechts vom Spielfeld, wenn (gelandet [Reihe + tetromino.potentialTopLeft.row]]! = 0 && landet [col + tetromino.potentialTopLeft.col]! = 0) // der Platz ist belegt
Wenn (und nur dann) alle diese Prüfungen bestehen, setzen wir tetromino.topLeft
zu tetromino.potentialTopLeft
.
Die dritte Prüfung ist für den Fall, dass der Spieler versucht, den Tetromino im oder gegen den Uhrzeigersinn zu drehen, und wird aufgerufen, wenn der Spieler die Taste drückt, um dies zu tun:
// setze tetromino.potentialShape auf die gedrehte Version von tetromino.shape // (im oder gegen den Uhrzeigersinn) und dann für (var row = 0; row) < tetromino.potentialShape.length; row++) for (var col = 0; col < tetromino.potentialShape[row].length; col++) if (tetromino.potentialShape[row][col] != 0) if (col + tetromino.topLeft.col < 0) //this block would be to the left of the playing field if (col + tetromino.topLeft.col >= landed [0] .length) // Dieser Block würde sich rechts vom Spielfeld befinden if (row + tetromino.topLeft.row> = landed.length) // dieser Block wäre unter dem Spielfeld if (gelandet [Zeile + tetromino.topLeft.row]! = 0 && landet [col + tetromino.topLeft.col]! = 0) // das Leerzeichen ist belegt
Wenn (und nur dann) alle diese Prüfungen bestehen, setzen wir tetromino.shape
zu tetromino.potentialShape
.
Vergleichen Sie diese drei Überprüfungen - es ist leicht, sie durcheinander zu bringen, da der Code sehr ähnlich ist.
Bisher habe ich verschiedene Größen von Arrays verwendet, um die verschiedenen Formen von Tetrominos (und die unterschiedlichen Rotationen dieser Formen) darzustellen: Der O-Tetromino verwendete ein 2x2-Array und der J-Tetromino einen 3x2- oder 2x3-Array.
Aus Gründen der Konsistenz empfehle ich, für alle Tetrominos (und deren Rotationen) dieselbe Arraygröße zu verwenden. Wenn Sie davon ausgehen, dass Sie bei den sieben Standard-Tetrominos bleiben, können Sie dies mit einem 4x4-Array tun.
Es gibt verschiedene Möglichkeiten, die Rotationen in diesem 4x4-Quadrat anzuordnen. Werfen Sie einen Blick in das Tetris-Wiki, um weitere Informationen darüber zu erhalten, welche unterschiedlichen Spiele verwendet werden.
Angenommen, Sie repräsentieren ein vertikales I-Tetromino wie folgt:
[[0,1,0,0], [0,1,0,0], [0,1,0,0], [0,1,0,0]];
… Und Sie repräsentieren seine Rotation folgendermaßen:
[[0,0,0,0], [0,0,0,0], [1,1,1,1], [0,0,0,0]];
Angenommen, ein vertikaler I-Tetromino wird wie folgt an eine Wand gedrückt:
Was passiert, wenn der Spieler die Dreh-Taste drückt?
Nun, mit unserem aktuellen Kollisionserkennungscode passiert nichts - der ganz linke Block des horizontalen I-Tetrominos wäre außerhalb der Grenzen.
Das ist wohl in Ordnung - so hat es in der NES-Version von Tetris funktioniert - aber es gibt eine Alternative: Drehen Sie den Tetromino und bewegen Sie ihn einmal um ein Stück nach rechts.
Ich lasse Sie die Details herausfinden, aber im Wesentlichen müssen Sie prüfen, ob das Tetromino durch Drehen bewegt wird, und falls dies der Fall ist, verschieben Sie es nach Bedarf um ein oder zwei Leerzeichen. Sie müssen jedoch daran denken, nach dem Anwenden der Drehung auf mögliche Kollisionen mit anderen Blöcken zu prüfen und die Bewegung!
Ich habe in diesem Tutorial Blöcke mit der gleichen Farbe verwendet, um die Dinge einfach zu halten, aber es ist einfach, die Farben zu ändern.
Wählen Sie für jede Farbe eine Zahl, um sie darzustellen. Verwenden Sie diese Zahlen in Ihrem gestalten[]
und gelandet[]
Arrays; Ändern Sie dann Ihren Rendering-Code, um die Blöcke anhand ihrer Nummern zu färben.
Das Ergebnis könnte ungefähr so aussehen:
Die visuelle Darstellung eines Objekts im Spiel von seinen Daten zu trennen, ist ein wirklich wichtiger zu verstehender Begriff. In anderen Spielen kommt es immer wieder vor, insbesondere bei der Kollisionserkennung.
In meinem nächsten Beitrag werden wir uns ansehen, wie wir die andere Kernfunktion von Tetris implementieren können: Linien entfernen, wenn sie gefüllt sind. Danke fürs Lesen!