JavaScript-Tutorial: Snake

Kurzbeschreibung des Spiels

Durch die Mobiltelefone von Nokia in den 90er Jahren wurde Snake einer breiten Masse von Menschen bekannt. Die Spielfigur, welche eine Schlange darstellt, kann mittels der Pfeiltasten über das Spielfeld gesteuert werden und muss versuchen, Äpfel einzusammeln, was zu einer Verlängerung der Schlange um ein zusätzliches Glied führt. Wird ein Apfel aufgesammelt, erscheint ein neuer an einer anderen Stelle im Spiefeld.

Das Ziel des Spieles ist es, dass die Schlange möglichst viele Äpfel einsammelt. Das Spiel ist vorbei, wenn die Schlange entweder eine Außenkante des Spielfeldes berührt oder die Schlange mit sich selbst kollidiert.

In der folgenden Abbildung ist die in diesem Tutorial durchgeführte Umsetzung des Spiele-Klassikers in JavaScript zu sehen. Die Schlange ist rot dargestellt, während der grüne Punkt einer der zu sammelnden Äpfel darstellt.

../../../_images/snake-overview.png

Konzept des Spiels

Dem Spiel Snake liegt ein einfaches Raster zugrunde, wie in der folgenden Abbildung zu sehen ist.

../../../_images/snake-grid.png

Das gesamte Spielfeld besteht aus einer Aneinderreihung aus Kacheln, die dieselbe Breite und Höhe aufweisen. Um die Größe des Spielfelds festzulegen, wird eine Anzahl von Kacheln in X-Richtung sowie in Y-Richtung, sowie die Größe dieser Kacheln definiert.

Ein Apfel stellt genau eine Kachel des gesamten Spielfeldes da. Die Schlange erstreckt sich über mehrere Kacheln. Wenn sich die Schlange bewegt, wird sie immer um eine Kachel weiter verschoben.

Im Hintergrund wird sowohl für den Apfel als auch für alle Schlangen-Fragmente gespeichert, an welcher Position im Spielfeld sie sich befinden (z.B. in der vierten Kachel in x-Richtung und der achten Kachel in y-Richtung).

Die Position des Apfels am Spielfeld ist zufällig, d.h. über Zufallszahlen festgelegt. Der Schlange ist eine Bewegungsrichtung vorgegeben (Up, Right, Down, Left). Diese Richtung kann durch die Verwendung der Pfeiltasten geändert werden.

Umsetzung

Wie in der Softwareentwicklung üblich werden wir Snake schrittweise erstellen, d.h. wir versuchen immer kleine in sich geschlossene Probleme/Aufgaben zu lösen/implementieren.

Schritt 1 - HTML-Struktur aufbauen

Die Snake-Version dieses Tutorials ist als Browser-Spiel konzipiert. Dies bedeutet, dass der JavaScript-Code in eine HTML-Datei eingebettet wird.

Dafür muss zunächst eine neue Datei mit dem Namen snake.html angelegt werden. In diese wird die HTML-Grundstruktur, die unten abgebildet ist, eingefügt.

<!-- Basic HTML Structure -->
<!Doctype html>
<html>
    <!-- Head of the HTML file -->
    <head>
        <title>JavaScript Snake</title>
        <style type="text/css">
            /* Within the style-tag you can write CSS */
        </style>
    </head>

    <!-- Body of the HTML file -->
    <body>
        <script type="text/javascript">
            // Within the script-tag you can write JavaScript
        </script>
    </body>
</html>

Um das Spiel darzustellen, wird ein Canvas verwendet, das nichts anderes als eine Zeichenfläche darstellt, auf die die einzelnen Komponenten des Spiel gezeichnet werden. Die Bewegung des Spiels kommt dadurch zustande, dass das Canvas in gewissen Abständen wieder gelöscht und mit neuen Werten neu gezeichnet wird.

Das Canvas wird eingebunden, indem im body-Tag der HTML-Struktur ein canvas-Tag eingefügt wird. Das Canvas benötigt eine ID, über die auf das HTML-Element zugegriffen werden kann. Im CSS wird zusätzlich eine Hintergrundfarbe für das Canvas gesetzt, damit es im weißen Browser-Fenster zu sehen ist.

<!-- Basic HTML Structure -->
<!Doctype html>
<html>
    <!-- Head of the HTML file -->
    <head>
        <title>JavaScript Snake</title>
        <style type="text/css">
            /* Within the style-tag you can write CSS */
            #myCanvas {                             /* ADDED */
                background-color: lightgrey;        /* ADDED */
            }                                       /* ADDED */
        </style>
    </head>

    <!-- Body of the HTML file -->
    <body>
        <canvas id="myCanvas"></canvas>  <!-- ADDED -->

        <script type="text/javascript">
            // Within the script-tag you can write Javascript
        </script>
    </body>
</html>

Wird die HTML-Datei durch Doppelklick im Browser ausgeführt, ist ein kleines Canvas mit hellgrauem Hintergrund zu erkennen.

../../../_images/snake-step1.png

Schritt 2 - Canvas für das Spiel vorbereiten

In Schritt 1 wurde bereits der ganze HTML- und CSS-Code abgedeckt, der für die Umsetzung von Snake notwendig ist.

Die restliche Implementierung wird in JavaScript gemacht. Der gesamte folgende Code wird in den script-Tag geschrieben.

Am Beginn wird das Canvas für das Spiel vorbereitet. Dies bedeutet, dass Variablen angelegt werden, über die die Größe der Kacheln (tiles), sowie die Anzahl der Kacheln in X- und Y-Richtung definiert werden.

var tileSize = 15;
var nrOfTilesInX = 50;
var nrOfTilesInY = 30;

Im Anschluss konfigurieren wir das Canvas bzw. den Zugriff auf das Canvas. Dafür muss mit der folgenden von JavaScript zur Verfügung gestellten Funktion und der zuvor definierten ID das Canvas in eine Variable gespeichert werden.

var canvas = document.getElementById("myCanvas");

Um die Größe des Canvas festzulegen oder darauf zu zeichnen, muss der sogenannte 2D-Context des Canvas verwendet werden. Dieser kann folgendermaßen in eine eigene Variable gespeichert werden, welchen wir gleichfalls in eine Variable für die wiederkehrende Verwendung speichern.

var ctx = canvas.getContext("2d");

Der letzte Schritt, um das Canvas vorzubereiten, ist die Festlegung dessen Größe. Diese wird abhängig von der Anzahl der Kacheln und der Größe der Kacheln ermittelt.

ctx.canvas.width = nrOfTilesInX * tileSize;
ctx.canvas.height = nrOfTilesInY * tileSize;

Zusammengefasst führen folgende Codezeilen im script-Tag zur Konfiguration der Spiel-/Zeichenfläche.

<script type="text/javascript">
    // Within the script-tag you have to write Javascript

    /*******************************************************
    * GAME LOGIC VARIABLES
    *******************************************************/

    var tileSize = 15;
    var nrOfTilesInX = 50;
    var nrOfTilesInY = 30;

    /*******************************************************
    * CANVAS VARIABLES
    *******************************************************/

    var canvas = document.getElementById("myCanvas");
    var ctx = canvas.getContext("2d");

    ctx.canvas.width = nrOfTilesInX * tileSize;
    ctx.canvas.height = nrOfTilesInY * tileSize;
</script>

Wird die HTML-Datei durch Doppelklick oder Neuladen im Browser ausgeführt, ist nun ein Canvas mit der gewünschten Größe sichtbar.

../../../_images/snake-step2.png

Schritt 3 - Game-Loop erzeugen

Ein Spiel besteht immer aus einem Game-Loop (dt. Spiel-Schleife), die dafür zuständig ist, die Darstellung des Spiels in regelmäßigen Abständen zu aktualisieren. Dadurch kommen Bewegungen im Spiel zustande.

Um in JavaScript einen Game-Loop zu erzeugen, der in fix definierten Abständen das Canvas aktualisiert (neu zeichnet), muss zunächst eine Funktion erzeugt werden, die alles beinhaltet, was immer wieder ausgeführt werden muss, also die gesamte Spielelogik und das Zeichnen auf das Canvas. Da zu diesem Zeitpunkt noch nicht bekannt ist, was dies genau für Aktionen sind, wird eine leere Funktion mit dem Namen gameLoop erstellt.

function gameLoop() {

}

Nun muss noch definiert werden, dass diese Funktion in regelmäßigen Zeitabständen ausgeführt wird. Dies wird mit der von JavaScript bereitgestellten Funktion setInterval gemacht, der der Name einer Funktion und eine Zeitspanne in Millisekunden übergeben wird. Mit der folgenden Zeile wird definiert, dass die Funktion gameLoop alle 100 Millisekunden ausgeführt wird. setInterval gibt einen Wert zurück, dieser wird in einer Variablen gespeichert, damit die Ausführung des Game-Loops später auch wieder beendet werden kann.

var gameLoopInterval = setInterval(gameLoop, 100);

Folglich müssen die folgenden Zeilen Code im Script-Tag unter der Definition der Variablen hinzugefügt werden:

// ... Game Logic Variables
// ... Canvas Variables

/*******************************************************
* GAME LOOP
*******************************************************/

function gameLoop() {

}

var gameLoopInterval = setInterval(gameLoop, 100);

Unmittelbar ist bei einer erneuten Ausführung der Datei im Browser noch keine Änderung sichtbar.

Schritt 4 - Schlange zeichnen

Der nächste Schritt ist das Zeichnen der Schlange. In der ersten Version des Spiels wird die Schlange nur eine Kachel groß sein und auch nicht länger werden, wenn sie Äpfel aufsammelt. Dies hat damit zu tun, dass sowohl die Bewegung einer längeren Schlange als auch das Hinzufügen eines Fragments komplizierter sind und darum in dieser Grundversion keine Integration findet.

Wie in der Konzept-Beschreibung erklärt, muss das Programm sich merken, in welcher Kachel in X- und Y-Richtung sich die Schlange zu jeder Zeit befindet. Dies wird gelöst, in dem zwei neue Variablen angelegt werden, in die gespeichert wird, in der wievielten Kachel in beide Richtungen (x,y) sich die Schlange aktuell befindet. Zudem wird eine Variable erzeugt, die sich die Farbe der Schlange merkt.

/*******************************************************
* GAME LOGIC VARIABLES
*******************************************************/

// ...

var snakeColor = "red";
var snakeX = 2;
var snakeY = 2;

Da nun die initiale Position der Schlange definiert ist, kann diese gezeichnet werden. Dafür wird eine neue Funktion erzeugt mit dem Name fileTile, die dafür zuständig ist, eine der Kacheln im Spielfeld mit einer bestimmten Farbe zu füllen. fillTile bekommt als Eingabe-Parameter die X- und die Y-Position der Kachel, sowie die Farbe, mit der sie gefüllt werden soll.

** ToDo **

Beim Canvas liegt der Nullpunkt in der linken oberen Ecke. Außerdem muss beachtet werden, dass das Zählen der Kacheln mit 0 beginnt. Das bedeutet, die Kachel in der linken oberen Ecke liegt an der 0-ten Stelle in X- sowie Y-Richtung (X=0 und Y=0). Liegt eine Kachel z.B. an der Position X=5 und Y=2, ist somit die sechste Kachel von links oben nach rechts hin und die dritte Kachel von links oben nach unten hin gemeint.

// ... Game Logic Variables
// ... Canvas Variables
// ... Game Loop

/*******************************************************
* DRAWING FUNCTIONS
*******************************************************/

function fillTile(x, y, color) {
    ctx.beginPath();
    ctx.rect(x*tileSize, y*tileSize, tileSize, tileSize);
    ctx.fillStyle = color;
    ctx.fill();
    ctx.closePath();
}

Das Zeichnen der Schlange soll Teil des Game-Loops sein, da es notwendig ist, die Schlange immer wieder an die geänderte im Hintergrund gespeicherte Position anzupassen. Dies bedeutet, dass die Funktion fileTile innerhalb der Funktion gameLoop aufgerufen wird mit den zuvor definierten Variablen, die die Position und die Farbe der Schlange beschreiben.

// ... Game Logic Variables
// ... Canvas Variables

/*******************************************************
* GAME LOOP
*******************************************************/

function gameLoop() {
    // draw snake
    fillTile(snakeX, snakeY, snakeColor);
}

// ...

// ... Drawing Functions

Im Browser ist jetzt die Schlange zu sehen.

../../../_images/snake-step4.png

Schritt 5 - Apfel zeichnen

Im Gegensatz zur Schlange wird die Position des Apfels zu Beginn des Spiels zufällig erzeugt. Wenn die Schlange einen Apfel einsammelt, soll ein neuer Apfel ebenfalls an einer zufälligen Position erzeugt werden. Dies macht es notwendig, mit Hilfe von JavaScript Zufallszahlen zu erzeugen. Damit derselbe Code öfters verwendet werden kann, wird dafür die folgende Funktion definiert. Die Funktion kann eine Zufallszahl in einem gewissen Bereich erzeugen, welcher über die Parameter start und end für den Start-Wert und den End-Wert begrenzt werden kann.

// ... Game Logic Variables
// ... Canvas Variables
// ... Game Loop
// ... Drawing Functions

/*******************************************************
* HELPER FUNCTIONS
*******************************************************/

function getRandomNumber(start, end) {
    return Math.floor(Math.random() * (end - start + 1) + start);
}

Um später erkennen zu können, ob die Schlange einen Apfel eingesammelt hat oder nicht, muss immer die Position des aktuellen Apfels gespeichert werden. Dies wird, wie auch bei der Schlange, mit zwei Variablen gelöst. Das Besondere ist jetzt jedoch, dass keine fixen Werte definiert werden, sondern die zuvor erzeugte Funktion getRandomNumber verwendet wird, um Zufallszahlen für die X- und Y-Position des Apfels zu erzeugen. Für die X-Position soll eine Zufallszahl zwischen 0 und der Anzahl der Kacheln in X-Richtung minus eins und für die Y-Position eine Zufallszahl zwischen 0 und der Anzahl der Kacheln in Y-Richtung minus eins erzeugt werden.

Wie zuvor erwähnt, beginnt das Zählen der Kacheln bei 0. Das -1 ist also notwendig, da es sonst passieren kann, dass eine Zahl außerhalb des Spielfelds erzeugt wird.

Zusätzlich wird wiederum die Farbe des Apfels in einer Variablen gespeichert.

/*******************************************************
* GAME LOGIC VARIABLES
*******************************************************/

// ...

var foodColor = "green";
var foodX = getRandomNumber(0, nrOfTilesInX-1);
var foodY = getRandomNumber(0, nrOfTilesInY-1);

// ... Canvas Variables
// ... Game Loop
// ... Drawing Functions
// ... Helper Functions

Wie auch bei der Schlange muss jetzt noch die Funktion fillTile in der Game-Loop aufgerufen werden mit den Variablen für die X- und Y-Position sowie der Farbe der Schlange, um die Kachel an der entsprechenden Position einzufärben.

/*******************************************************
* GAME LOOP
*******************************************************/

function gameLoop() {
    // draw snake
    fillTile(snakeX, snakeY, snakeColor);

    // draw food
    fillTile(foodX, foodY, foodColor);
}

// ...

Wird die HTML-Datei im Browser ausgeführt, ist jetzt sowohl die Schlange als auch ein zufällig positionierter Apfel sichtbar.

../../../_images/snake-step5.png

Zwischenstand

Die Datei sieht nun folgendermaßen aus.

<!-- Basic HTML Structure -->
<!Doctype html>
<html>

    <!-- Head of the HTML file -->
    <head>
        <title>JavaScript Snake</title>
        <style type="text/css">
            /* Within the style-tag you have to write CSS */
            #myCanvas {
                background-color: lightgrey;
            }
        </style>
    </head>

    <!-- Body of the HTML file -->
    <body>
        <canvas id="myCanvas"></canvas>

        <script type="text/javascript">
            // Within the script-tag you have to write Javascript

            /*******************************************************
            * GAME LOGIC VARIABLES
            *******************************************************/

            var tileSize = 15;
            var nrOfTilesInX = 50;
            var nrOfTilesInY = 30;

            var snakeColor = "red";
            var snakeX = 2;
            var snakeY = 2;

            var foodColor = "green";
            var foodX = getRandomNumber(0, nrOfTilesInX-1);
            var foodY = getRandomNumber(0, nrOfTilesInY-1);

            /*******************************************************
            * CANVAS VARIABLES
            *******************************************************/

            var canvas = document.getElementById("myCanvas");
            var ctx = canvas.getContext("2d");

            ctx.canvas.width = nrOfTilesInX * tileSize;
            ctx.canvas.height = nrOfTilesInY * tileSize;

            /*******************************************************
            * GAME LOOP
            *******************************************************/

            function gameLoop() {
                // draw snake
                fillTile(snakeX, snakeY, snakeColor);

                // draw food
                fillTile(foodX, foodY, foodColor);
            }

            var gameLoopInterval = setInterval(gameLoop, 100);

            /*******************************************************
            * DRAWING FUNCTIONS
            *******************************************************/

            function fillTile(x, y, color) {
                ctx.beginPath();
                ctx.rect(x*tileSize, y*tileSize, tileSize, tileSize);
                ctx.fillStyle = color;
                ctx.fill();
                ctx.closePath();
            }

            /*******************************************************
            * HELPER FUNCTIONS
            *******************************************************/

            function getRandomNumber(start, end) {
                return Math.ceil(Math.random() * (end - start) + start);
            }

        </script>
    </body>

</html>

Schritt 6 - Schlange automatisch bewegen

Somit wurden schon alle notwendigen Komponenten gezeigt, jetzt ist es Zeit, Bewegung in das Spiel zu bringen.

Die Schlange soll sich automatisch bewegen, ohne dass der Benutzer etwas tun muss. Der Benutzer steuert schlussendlich nur die Richtung, in die sich die Schlange bewegt, dazu aber mehr im nächsten Schritt. Zunächst soll sich die Schlange einfach bewegen, ohne Steuerung des Benutzers.

Dafür wird eine sogenannte Enumeration bzw. Auflistung von Werten eingeführt, welches uns hilft, die Richtung der Schlange zu bestimmen, in der sie sich bewegt. Die Aufzählung der möglichen Bewegungsrichtungen der Schlange wird mit dem Namen Direction festgelegt. Zusätzlich wird eine Variable angelegt, die uns immer eine der soeben definierten Richtungen bzw. die aktuelle Bewegungsrichtung der Schlange speichert. Da wir die Schlange im linken Bereich gezeichnet haben, bewegen wir die Schlange am Beginn nach rechts. Die Bewegungsrichtung kann später über die Pfeiltasten neu festgelegt werden!

/*******************************************************
* GAME LOGIC VARIABLES
*******************************************************/

const Direction = {
    UP: 1,
    RIGHT: 2,
    DOWN: 3,
    LEFT: 4
};

var snakeDirection = Direction.RIGHT;

// ...

// ... Canvas Variables
// ... Game Loop
// ... Drawing Functions
// ... Helper Functions

Als nächstes kann eine Funktion definiert werden, welche die Schlange weiter bewegt. Folglich nennen wird diese moveSnake. Darin wird abhängig von der aktuellen Richtung der Schlange ihre Position verändert. Die Schlange zu bewegen, bedeutet, sie um eine Kachel zu verschieben.

Es darf nicht vergessen werden, dass der Nullpunkt des Canvas in der linken oberen Ecke liegt. Die X-Position erhöhen, bedeutet, weiter nach rechts zu gehen. Die Y-Position zu erhöhen, bedeutet, weiter nach unten zu gehen.

// ... Game Logic Variables
// ... Canvas Variables
// ... Game Loop

/*******************************************************
* GAME LOGIC FUNCTIONS
*******************************************************/

function moveSnake() {
    if (snakeDirection == Direction.UP) {
        snakeY = snakeY - 1;
    } else if (snakeDirection == Direction.RIGHT) {
        snakeX = snakeX + 1;
    } else if (snakeDirection == Direction.DOWN) {
        snakeY = snakeY + 1;
    } else if (snakeDirection == Direction.LEFT) {
        snakeX = snakeX - 1;
    }
}

// ... Drawing Functions
// ... Helper Functions

Nun muss die Funktion moveSnake noch im Game-Loop aufgerufen werden.

// ... Game Logic Variables
// ... Canvas Variables

/*******************************************************
* GAME LOOP
*******************************************************/

function gameLoop() {
    moveSnake();

    // draw snake
    fillTile(snakeX, snakeY, snakeColor);

    // draw food
    fillTile(foodX, foodY, foodColor);
}

// ...

// ... Game Logic Functions
// ... Drawing Functions
// ... Helper Functions

Wird die Datei im Browser ausgeführt, ergibt sich folgendes Problem. Die Schlange wird mit jedem Durchlauf des Game-Loops bewegt und neu gezeichnet, die alten Zeichnungen werden jedoch nie gelöscht, was dazu führt, dass die Schlange einen Schweif bekommt, welcher nicht gewünscht ist.

../../../_images/snake-step6-withoutClear.png

Um dies zu verhindern, muss das Canvas immer wieder gelöscht werden, bevor es neu gezeichnet wird. Dafür wird eine Funktion mit dem Namen clearCanvas erzeugt, die von der linken oberen Ecke bis zur rechten unteren Ecke alles vom Canvas löscht.

// ... Game Logic Variables
// ... Canvas Variables
// ... Game Loop
// ... Game Logic Functions

/*******************************************************
* DRAWING FUNCTIONS
*******************************************************/

// ...

function clearCanvas() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
}

// ... Helper Functions

Diese Funktion muss immer zu Beginn des Game-Loops ausgeführt werden.

// ... Game Logic Variables
// ... Canvas Variables

/*******************************************************
* GAME LOOP
*******************************************************/

function gameLoop() {
    clearCanvas();

    moveSnake();

    // draw snake
    fillTile(snakeX, snakeY, snakeColor);

    // draw food
    fillTile(foodX, foodY, foodColor);
}

// ...

// ... Game Logic Functions
// ... Drawing Functions
// ... Helper Functions

Nun hat die Schlange keinen Schweif mehr.

../../../_images/snake-step6.png

Schritt 7 - Interaktion mit Benutzer/in

Aktuell bewegt sich die Schlange immer in dieselbe Richtung, nämlich in die zu Beginn definierte Richtung. Der nächste Schritt ist es, dem Benutzer zu ermöglichen, die Schlange mit Hilfe der Pfeiltasten zu steuern.

Dafür muss zunächst eine Funktion geschrieben werden, die ein keydown-Event (also das Drücken einer beliebigen Taste) abfängt und überprüft, welche Taste gedrückt wurde. Abhängig von der gedrückten Taste wird die Richtung der Schlange neu definiert.

function keyPressed(event) {
    if (event.key == "ArrowUp") {
        snakeDirection = Direction.UP;
    } else if (event.key == "ArrowRight") {
        snakeDirection = Direction.RIGHT;
    } else if (event.key == "ArrowDown") {
        snakeDirection = Direction.DOWN;
    } else if (event.key == "ArrowLeft") {
        snakeDirection = Direction.LEFT;
    }
}

Für diese Funktion muss jetzt ein Event-Listener registriert werden, der auf das keydown-Event hört. Das kann mit der folgenden Funktion gemacht werden. Hier wird definiert, dass immer, wenn eine Taste gedrückt wird (keydown), die Funktion keyPressed ausgeführt werden soll. Der Eingabe-Parameter event wird vom Event-Listener selbst an die Funktion weitergegeben.

document.addEventListener("keydown", keyPressed);

Diese Schritte führen zu folgendem Code.

// ... Game Logic Variables
// ... Canvas Variables
// ... Game Loop
// ... Game Logic Functions
// ... Drawing Functions

/*******************************************************
* USER INTERACTION FUNCTIONS
*******************************************************/

function keyPressed(event) {
    if (event.key == "ArrowUp") {
        snakeDirection = Direction.UP;
    } else if (event.key == "ArrowRight") {
        snakeDirection = Direction.RIGHT;
    } else if (event.key == "ArrowDown") {
        snakeDirection = Direction.DOWN;
    } else if (event.key == "ArrowLeft") {
        snakeDirection = Direction.LEFT;
    }
}

document.addEventListener("keydown", keyPressed);

// ... Helper Functions

Jetzt kann durch das Drücken der Pfeiltasten die Richtung der Schlange geändert werden.

../../../_images/snake-step7.png

Schritt 8 - Einsammeln der Äpfel

Da jetzt die Schlange gesteuert werden kann, ist der nächste Schritt das Einsammeln der Äpfel.

Wenn die Schlange einen Apfel eingesammelt hat, ist es notwendig, dass ein neuer Apfel an einer neuen Position erzeugt wird. Um das zu erreichen, wird eine Funktion mit dem Namen generateNewFood erzeugt, in welcher eine neue, zufällig generierte Position des Apfels in X- und Y-Richtung erzeugt wird.

// ... Game Logic Variables
// ... Canvas Variables
// ... Game Loop

/*******************************************************
* GAME LOGIC FUNCTIONS
*******************************************************/

// ...

function generateNewFood() {
    foodX = getRandomNumber(0, nrOfTilesInX-1);
    foodY = getRandomNumber(0, nrOfTilesInY-1);
}

// ... Drawing Functions
// ... User Interaction Functions
// ... Helper Functions

Als nächstes muss überprüft werden, ob die Schlange mit dem Apfel kollidiert. Dies kann ganz einfach gemacht werden, indem überprüft wird, ob die Position der Schlange mit der Position des Apfels übereinstimmt, sowohl in X- als auch in Y-Richtung. Wenn das der Fall ist, dann soll eine neue Position für den Apfel erzeugt werden, indem die Funktion generateNewFood aufgerufen wird.

// ... Game Logic Variables
// ... Canvas Variables
// ... Game Loop

/*******************************************************
* GAME LOGIC FUNCTIONS
*******************************************************/

// ...

function checkFoodCollision() {
    if (snakeX == foodX && snakeY == foodY) {
        generateNewFood();
    }
}

function generateNewFood() {
    foodX = getRandomNumber(0, nrOfTilesInX-1);
    foodY = getRandomNumber(0, nrOfTilesInY-1);
}

// ... Drawing Functions
// ... User Interaction Functions
// ... Helper Functions

Die Überprüfung, ob die Schlange mit dem Apfel kollidiert, muss noch im Game-Loop hinzugefügt werden.

// ... Game Logic Variables
// ... Canvas Variables

/*******************************************************
* GAME LOOP
*******************************************************/

function gameLoop() {
    clearCanvas();

    moveSnake();

    checkFoodCollision();

    // draw snake
    fillTile(snakeX, snakeY, snakeColor);

    // draw food
    fillTile(foodX, foodY, foodColor);
}

// ...

// ... Game Logic Functions
// ... Drawing Functions
// ... User Interaction Functions
// ... Helper Functions

Schritt 9 - Game-Over

Nachdem die Schlange gesteuert werden und Äpfel einsammeln kann, muss nun definiert werden, wann das Spiel zu Ende ist. Da die Schlange nicht länger wird und immer nur eine Kachel groß ist, kann sie nicht mit sich selbst kollidieren. Dies bedeutet wiederum, dass das Spiel nur zu Ende ist, wenn eine Außenkante des Spielfelds berührt wird.

Wenn eine Außenkante berührt wird, soll der Game-Loop gestoppt werden. Dies kann mit der von JavaScript bereitgestellten Funktion clearInterval gemacht werden. Dieser muss das Interval übergeben werden, welches gestoppt werden soll, welches in diesem Beispiel in der zuvor erzeugten Variable gameLoopInterval gespeichert ist.

Es wird also eine Funktion mit dem Name gameOver erzeugt, die das Interval stoppt.

// ... Game Logic Variables
// ... Canvas Variables
// ... Game Loop

/*******************************************************
* GAME LOGIC FUNCTIONS
*******************************************************/

// ...

function gameOver() {
    clearInterval(gameLoopInterval);
}

// ... Drawing Functions
// ... User Interaction Functions
// ... Helper Functions

Nun muss überprüft werden, ob die Schlange mit einer Außenkante kollidiert. Das wird gemacht, indem überprüft wird, ob sich die Schlange außerhalb des Spielfeldes befindet. Da dia Anzahl der Kacheln in X- sowie Y-Richtung bekannt und in jeweils einer Variablen gespeichert ist, ist das kein Problem. Wenn sich die Schlange außerhalb des Spielfeldes befindet, soll die Funktion gameOver aufgerufen werden.

// ... Game Logic Variables
// ... Canvas Variables
// ... Game Loop

/*******************************************************
* GAME LOGIC FUNCTIONS
*******************************************************/

// ...

function gameOver() {
    clearInterval(gameLoopInterval);
}

function checkWallCollision() {
    // in X direction
    if (snakeX < 0 || snakeX > nrOfTilesInX) {
        gameOver();
    }

    // in Y direction
    if (snakeY < 0 || snakeY > nrOfTilesInY) {
        gameOver();
    }
}

// ... Drawing Functions
// ... User Interaction Functions
// ... Helper Functions

Wie die Kollision der Schlange mit dem Apfel muss auch die Kollision der Schlange mit der Außenkante im Game-Loop überprüft werden.

// ... Game Logic Variables
// ... Canvas Variables

/*******************************************************
* GAME LOOP
*******************************************************/

function gameLoop() {
    clearCanvas();

    moveSnake();

    checkFoodCollision();
    checkWallCollision();

    // draw snake
    fillTile(snakeX, snakeY, snakeColor);

    // draw food
    fillTile(foodX, foodY, foodColor);
}

// ...

// ... Game Logic Functions
// ... Drawing Functions
// ... User Interaction Functions
// ... Helper Functions

Schritt 10 - Punkte zählen und Text darstellen

Zum Schluss möchten wir dem Benutzer Feedback geben, wie gut das Spiel gespielt wurde. Das wird gemacht, indem mitgezählt wird, wie viele Äpfel die Schlange bisher gesammelt hat.

Um das zu erreichen, wird eine neue Variable erstellt, die den aktuellen Score speichert, und mit 0 initialisiert.

/*******************************************************
* GAME LOGIC VARIABLES
*******************************************************/

// ...

var score = 0;

// ... Canvas Variables
// ... Game Loop
// ... Game Logic Functions
// ... Drawing Functions
// ... User Interaction Functions
// ... Helper Functions

Jedes Mal, wenn die Schlange mit einem Apfel kollidiert, was in der Funktion checkFoodCollision überprüft wird, wird der Score um hochgezählt.

// ... Game Logic Variables
// ... Canvas Variables
// ... Game Loop

/*******************************************************
* GAME LOGIC FUNCTIONS
*******************************************************/

// ...

function checkFoodCollision() {
    if (snakeX == foodX && snakeY == foodY) {
        score = score + 1;  // ADDED
        generateNewFood();
    }
}

// ...

// ... Drawing Functions
// ... User Interaction Functions
// ... Helper Functions

Der Score soll im Canvas dargestellt werden, damit der Benutzer jederzeit sieht, wie viele Äpfel schon gesammelt wurden. Um Text auf dem Canvas zu zeichnen, wird eine neue Funktion mit dem Name drawText erzeugt, der übergeben werden kann, welcher Text mit welcher Größe, Schriftart und Farbe an welcher Position gezeichnet werden soll.

// ... Game Logic Variables
// ... Canvas Variables
// ... Game Loop
// ... Game Logic Functions

/*******************************************************
* DRAWING FUNCTIONS
*******************************************************/

// ...

function drawText(text, font, color, x, y) {
    ctx.beginPath();
    ctx.font = font;
    ctx.fillStyle = color;
    ctx.fillText(text, x, y);
    ctx.closePath();
}

// ... User Interaction Functions
// ... Helper Functions

Um den Score nun tatsächlich auf dem Canvas zu zeichnen, muss die Funktion drawText mit den entsprechenden Parametern im Game-Loop aufgerufen werden.

// ... Game Logic Variables
// ... Canvas Variables

/*******************************************************
* GAME LOOP
*******************************************************/

function gameLoop() {
    clearCanvas();

    moveSnake();

    checkFoodCollision();
    checkWallCollision();

    // draw snake
    fillTile(snakeX, snakeY, snakeColor);

    // draw food
    fillTile(foodX, foodY, foodColor);

    drawText("Score: " + score, "20px Arial", "black", 10, canvas.height - 10);
}

// ...

// ... Game Logic Functions
// ... Drawing Functions
// ... User Interaction Functions
// ... Helper Functions

In der HTML-Datei wird nun der Score dargestellt.

../../../_images/snake-step10.png

Die zuvor erstellte Funktion drawText kann auch verwendet werden, um darzustellen, dass das Spiel zu Ende ist, wenn eine Außenkante berührt wurde. Dafür muss drawText in der zuvor definierten Funktion gameOver aufgerufen werden.

// ... Game Logic Variables
// ... Canvas Variables
// ... Game Loop

/*******************************************************
* GAME LOGIC FUNCTIONS
*******************************************************/

// ...

function gameOver() {
    drawText("Game over!", "60px Arial black", "black", canvas.width/2 - 200, canvas.height/2);
    clearInterval(gameLoopInterval);
}

// ...

// ... Drawing Functions
// ... User Interaction Functions
// ... Helper Functions

Das schaut dann folgendermaßen aus.

../../../_images/snake-step10-gameover.png

Ergebnis

Damit wurde das Snake-JavaScript umgesetzt. Im Folgenden ist der gesamte resultierende Code abgebildet.

<!-- Basic HTML Structure -->
<!Doctype html>
<html>

<!-- Head of the HTML file -->
<head>
    <title>JavaScript Snake</title>
    <style type="text/css">
        /* Within the style-tag you have to write CSS */
        #myCanvas {
            background-color: lightgrey;
        }
    </style>
</head>

<!-- Body of the HTML file -->
<body>
    <canvas id="myCanvas"></canvas>

    <script type="text/javascript">
        // Within the script-tag you have to write Javascript


      /*******************************************************
        * GAME LOGIC VARIABLES
        *******************************************************/

        const Direction = {
            UP: 1,
            RIGHT: 2,
            DOWN: 3,
            LEFT: 4
        };

        var tileSize = 15;
        var nrOfTilesInX = 50;
        var nrOfTilesInY = 30;

        var snakeColor = "red";
        var snakeX = 2;
        var snakeY = 2;
        var snakeDirection = Direction.RIGHT;

        var foodColor = "green";
        var foodX = getRandomNumber(0, nrOfTilesInX-1);
        var foodY = getRandomNumber(0, nrOfTilesInY-1);

        var score = 0;


        /*******************************************************
        * CANVAS VARIABLES
        *******************************************************/

        var canvas = document.getElementById("myCanvas");
        var ctx = canvas.getContext("2d");

        ctx.canvas.width = nrOfTilesInX * tileSize;
        ctx.canvas.height = nrOfTilesInY * tileSize;


        /*******************************************************
        * GAME LOOP
        *******************************************************/

        function gameLoop() {
            clearCanvas();

            moveSnake();

            checkFoodCollision();
            checkWallCollision();

            // draw snake
            fillTile(snakeX, snakeY, snakeColor);

            // draw food
            fillTile(foodX, foodY, foodColor);

            drawText("Score: " + score, "20px Arial", "black", 10, canvas.height - 10);
        }

        var gameLoopInterval = setInterval(gameLoop, 100);


        /*******************************************************
        * GAME LOGIC FUNCTIONS
        *******************************************************/

        function moveSnake() {
            if (snakeDirection == Direction.UP) {
                snakeY = snakeY - 1;
            } else if (snakeDirection == Direction.RIGHT) {
                snakeX = snakeX + 1;
            } else if (snakeDirection == Direction.DOWN) {
                snakeY = snakeY + 1;
            } else if (snakeDirection == Direction.LEFT) {
                snakeX = snakeX - 1;
            }
        }

        function checkFoodCollision() {
            if (snakeX == foodX && snakeY == foodY) {
                score = score + 1;
                generateNewFood();
            }
        }

        function generateNewFood() {
            foodX = getRandomNumber(0, nrOfTilesInX-1);
            foodY = getRandomNumber(0, nrOfTilesInY-1);
        }

        function gameOver() {
            drawText(
                "Game over!",
                "60px Arial black",
                "black",
                canvas.width/2 - 200,
                canvas.height/2
            );
            clearInterval(gameLoopInterval);
        }

        function checkWallCollision() {
            // in X direction
            if (snakeX < 0 || snakeX > nrOfTilesInX) {
                gameOver();
            }

            // in Y direction
            if (snakeY < 0 || snakeY > nrOfTilesInY) {
                gameOver();
            }
        }


        /*******************************************************
        * DRAWING FUNCTIONS
        *******************************************************/

        function fillTile(x, y, color) {
            ctx.beginPath();
            ctx.rect(x*tileSize, y*tileSize, tileSize, tileSize);
            ctx.fillStyle = color;
            ctx.fill();
            ctx.closePath();
        }

        function clearCanvas() {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
        }

        function drawText(text, font, color, x, y) {
            ctx.beginPath();
            ctx.font = font;
            ctx.fillStyle = color;
            ctx.fillText(text, x, y);
            ctx.closePath();
        }


      /*******************************************************
        * USER INTERACTION FUNCTIONS
        *******************************************************/

        function keyPressed(event) {
            if (event.key == "ArrowUp") {
                snakeDirection = Direction.UP;
            } else if (event.key == "ArrowRight") {
                snakeDirection = Direction.RIGHT;
            } else if (event.key == "ArrowDown") {
                snakeDirection = Direction.DOWN;
            } else if (event.key == "ArrowLeft") {
                snakeDirection = Direction.LEFT;
            }
        }

        document.addEventListener("keydown", keyPressed);


        /*******************************************************
        * HELPER FUNCTIONS
        *******************************************************/

        function getRandomNumber(start, end) {
            return Math.ceil(Math.random() * (end - start) + start);
        }

    </script>
</body>

</html>

Erweiterungen

Die vorliegende Implementierung kann noch um weitere Features ergänzt werden, wie z.B.

  • das Wachsen/Längerwerden der Schlange
  • mehrere Äpfel auf einmal
  • die Äpfel wechseln nach einer bestimmten Zeit die Position, wenn sie nicht eingesammelt wurden bis dahin
  • Mehrspieler-Unterstützung