In dieser Übung lernst du, wie du mit Node.js einfache Spiele programmieren kannst.
Du brauchst für diese Übung die gleichen Tools (Visual Studio Code, Node.js, npm), die schon in der Übung Ein Webserver mit Node.js erwähnt sind. Falls du die Übung schon gemacht hast, hast du schon alle notwendigen Tools. Falls nicht, schau dort bitte nach.
Du kannst das Beispiel unter Windows, Linux oder MacOS bauen. Alle verwendeten Tools sind plattformunabhängig.
Als erstes brauchen wir einen Ordern, in dem wir das Spiel erstellen können. Lege im Dateiexplorer einen Ordner an und öffne dann diesen Pfad in der Eingabeaufforderung. Dort rufst du den Befehl npm init
auf. Damit wird in deinem Ordner eine Datei package.json
erzeugt, in der wir zusätzlich benötigte Pakete speichern können.
Wir brauchen für unser Spiel die beiden Pakete ansi
und keypress
. Diese können wir durch die folgenden beiden Befehle in der Eingabeaufforderung laden:
npm install ansi
npm install keypress
Nachdem du die beiden Pakete installiert hast, kannst du in deinem Ordner eine neue Datei für dein Spiel anlegen (z.B. game.js). Zum Starten geben wir den Text “MY GAME” aus und malen ein paar farbige Punkte:
/*jslint node: true, for: true */
'use strict';
var ansi = require('ansi');
var cursor = ansi(process.stdout);
try {
// cursor.bg.... setzt die Hintergrundfarbe, so können wir mit dem Leerzeichen farbige Flächen malen
cursor.bg.red();
cursor.goto(5, 5).write(' ');
cursor.goto(6, 5).write(' ');
cursor.goto(7, 5).write(' ');
// mit reset setzt du die Hintergrundfarbe wieder zurück
cursor.bg.reset();
// cursor..... setzt die Textfarbe
cursor.yellow();
cursor.goto(9, 5).write('MY GAME');
cursor.reset();
cursor.bg.red();
cursor.goto(17, 5).write(' ');
cursor.goto(18, 5).write(' ');
cursor.goto(19, 5).write(' ');
cursor.bg.reset();
} catch (ex) {
// hier werden Fehler erkannt und ausgegeben
console.log(ex.toString());
} finally {
// zum Schluss müssen wir das Spiel beenden
quitGame();
}
function quitGame() {
cursor.reset();
cursor.bg.reset();
cursor.goto(1, 10);
process.exit();
}
Du kannst das Programm ausführen, indem du in der Eingabeaufforderung folgendes Kommando eingibst:
node game.js
Wenn du das Spiel nun laufen lässt, werden zuerst drei Leerzeichen mit roten Hintergrund gezeichnet, dann wird der Text “MY GAME” in gelb ausgegeben, und dann kommen wieder drei Leerzeichen mit rotem Hintergrund.
Als nächstes wollen wir ein Spielfeld zeichnen:
Dazu definieren wir global die Größe des Spielfelds mit var width = 40;
und var height = 20;
. Den Code im try
Block ändern wir, um die vier Linien zu zeichnen. Damit wir nicht jeden Punkt der Linie einzeln zeichnen müssen, bauen wir dafür die Funktionen drawHorizontalLine
und drawVerticalLine
.
In den neuen Funktionen verwenden wir sogeannten Schleifen (for
) um einen Befehl mehrmals zu wiederholen. Zu Beginn der for-Schleife wird der Wert i auf 0 gesetzt. Bei jedem Schleifendurchlauf wird geprüft, ob i < length erfüllt wird. Wenn nicht wird die Schleife abgebrochen, sonst wird der Code ausgeführt. Zum Schluss wird noch i++
ausgeführt - also i um 1 erhöht.
/*jslint node: true, for: true */
'use strict';
var ansi = require('ansi');
var cursor = ansi(process.stdout);
var width = 40;
var height = 20;
try {
// draw game area
cursor.bg.grey();
drawHorizontalLine(1, 1, width);
drawHorizontalLine(1, height, width);
drawVerticalLine(1, 1, height);
drawVerticalLine(width, 1, height);
cursor.bg.reset();
} catch (ex) {
console.log(ex.toString());
} finally {
quitGame();
}
...
function drawHorizontalLine(col, row, length) {
for (var i = 0; i < length; i++) {
cursor.goto(col + i, row).write(' ');
}
}
function drawVerticalLine(col, row, length) {
for (var i = 0; i < length; i++) {
cursor.goto(col, row + i).write(' ');
}
}
Die Schlange stellen wir als grünen Punkt dar. Dazu erweitern wir die globalen Variablen zu Beginn des Codes um posX
und posY
für die aktuelle Position der Schlange und um dirX
und dirY
für die Richtung der Schlange.
Variable | Wert | Beschreibung |
---|---|---|
posX |
-1 | nach links bewegen |
posX |
0 | Bewegung nach oben oder unten durch posY |
posX |
1 | nach rechts bewegen |
posY |
-1 | nach oben bewegen |
posY |
0 | Bewegung nach links oder rechts durch posX |
posY |
1 | nach unten bewegen |
Zu Beginn des try
Blocks ist noch Code enthalten, der den Cursor im Spiel ausblendet. Dieser muss in quitGame
wieder eingeblendet werden.
Nach dem Malen des Spielfelds wird die initiale Position der Schlange gesetzt und dann wird die Game Loop gestartet. Das heißt, die Funktion gameLoop
wird immer wieder aufgerufen, bis die Schlange den Rand berührt.
Achtung: hier brauchen wir das Spiel nicht im Block finally
zu beenden, da das Spiel mit der Game Loop solange weiter laufen soll, bis die Schlange den Rand berührt.
/*jslint node: true, for: true */
'use strict';
var ansi = require('ansi');
var keypress = require('keypress');
var cursor = ansi(process.stdout);
var width = 40;
var height = 20;
var posX = 0;
var posY = 0;
var dirX = 1;
var dirY = 0;
try {
// clear output
process.stdout.write('\x1Bc');
// hide cursor
process.stderr.write('\x1B[?25l');
// draw game area
cursor.bg.grey();
drawHorizontalLine(1, 1, width);
drawHorizontalLine(1, height, width);
drawVerticalLine(1, 1, height);
drawVerticalLine(width, 1, height);
cursor.bg.reset();
// set initial position of snake
posX = Math.floor(width / 2);
posY = Math.floor(height / 2);
// start game loop
gameLoop();
} catch (ex) {
console.log(ex.toString());
quitGame();
}
function gameLoop() {
// remove snake at old position
removeSnake(posX, posY);
// set new position
posX = posX + dirX;
posY = posY + dirY;
// check new position
if (posX == 1 || posX == width || posY == 1 || posY == height) {
cursor.red();
cursor.bg.white();
setText(width / 2 - 6, height / 2, " GAME OVER ");
quitGame();
}
// draw snake at new position
drawSnake();
// call gameLoop
setTimeout(gameLoop, 500);
}
function quitGame() {
cursor.reset();
cursor.bg.reset();
process.stderr.write('\x1B[?25h');
cursor.goto(1, height + 4);
process.exit();
}
function removeSnake() {
cursor.bg.black();
drawPoint(posX, posY);
cursor.bg.reset();
}
function drawSnake() {
cursor.bg.green();
drawPoint(posX, posY);
cursor.bg.reset();
}
function drawPoint(col, row, char) {
cursor.goto(col, row).write(' ');
}
function drawHorizontalLine(col, row, length) {
for (var i = 0; i < length; i++) {
cursor.goto(col + i, row).write(' ');
}
}
function drawVerticalLine(col, row, length) {
for (var i = 0; i < length; i++) {
cursor.goto(col, row + i).write(' ');
}
}
function setText(col, row, text) {
cursor.goto(col, row).write(text);
}
Damit wir die Schlange mit den Cursortasten steuern können müssen wir auf das Event keypress
reagieren. Dazu fügen wir vor dem initalen Setzen der Position der Schlange eine neue Zeile ein.
// handle key press events
process.stdin.on('keypress', handleInput);
// set initial position of snake
posX = Math.floor(width / 2);
posY = Math.floor(height / 2);
Dann müssen wir auch die Funktion handleInput
definieren:
function handleInput(chunk, key) {
if (key.name == 'q') {
quitGame();
} else if (key.name == 'right') {
dirX = 1;
dirY = 0;
} else if (key.name == 'left') {
dirX = -1;
dirY = 0;
} else if (key.name == 'up') {
dirX = 0;
dirY = -1;
} else if (key.name == 'down') {
dirX = 0;
dirY = 1;
}
}
Zusätzlich zu den Cursortasten behandeln wir auch die Taste q
. Sie führt dazu, dass das Spiel abgebrochen wird.
Nun brauchen wir noch den Apfel, der von der Schlange gefressen werden soll. Dazu brauchen wir noch einige Erweiterungen.
Bei den globalen Variablen brauchen wir die aktuelle Position des Apfels.
var applePosX = 0;
var applePosY = 0;
Vor dem Start der Game Loop muss der erste Apfel gezeichnet werden:
// draw first apple
drawApple();
// start game loop
gameLoop();
Die Funktion drawApple
sieht so aus:
function drawApple() {
applePosX = Math.ceil(Math.random() * (width - 2)) + 1;
applePosY = Math.ceil(Math.random() * (height - 2)) + 1;
cursor.bg.red();
drawPoint(applePosX, applePosY);
cursor.bg.reset();
}
Bevor wir in der Game Loop die Schlange neu zeichnen, müssen wir überprüfen, ob der Apfel berührt wurde. Wenn ja, muss ein neuer Apfel gezeichnet werden.
// check if apple is touched
if (posX == applePosX && posY == applePosY) {
// draw new apple
drawApple();
}
// draw snake at new position
drawSnake();
Damit wir die Anzahl der gefressenen Äpfel ausgeben können und die Geschwindigkeit mit jedem Apfel erhöhen können, fügen wir zwei weitere globale Variablen hinzu:
var points = 0;
var speed = 3; // moves per second
Diese verwenden wir als erstes in der Game Loop an der Stelle, an der wir prüfen, ob der Apfel berührt wird. Wenn ja, erhöhen wir die points
und die speed
:
// check if apple is touched
if (posX == applePosX && posY == applePosY) {
// increase points
points++;
// increase speed
if (speed < 20) {
speed++;
}
// draw new apple
drawApple();
}
Am Ende der Game Loop wird diese erneut aufgerufen. Hier ändern wir die Wartezeit, indem wir 1000 (Millisekunden) / durch die speed
dividieren. Bei speed
= 4 warten wir z.B. 250 ms bis zum nächsten Aufruf der Game Loop.
// call gameLoop
setTimeout(gameLoop, 1000 / speed);
Außerdem fügen wir in der Funktion drawApple
am Ende noch folgende Zeilen zum Ausgeben von points
und speed
ein:
setText(1, height + 2, "Points: " + points.toString());
setText(1, height + 3, "Speed: " + speed.toString());
/*jslint node: true, for: true */
'use strict';
var ansi = require('ansi');
var keypress = require('keypress');
keypress(process.stdin);
process.stdin.setRawMode(true);
process.stdin.resume();
var cursor = ansi(process.stdout);
var width = 40;
var height = 20;
var posX = 0;
var posY = 0;
var applePosX = 0;
var applePosY = 0;
var dirX = 1;
var dirY = 0;
var points = 0;
var speed = 3;
try {
// clear output
process.stdout.write('\x1Bc');
// hide cursor
process.stderr.write('\x1B[?25l');
// draw game area
cursor.bg.grey();
drawHorizontalLine(1, 1, width);
drawHorizontalLine(1, height, width);
drawVerticalLine(1, 1, height);
drawVerticalLine(width, 1, height);
cursor.bg.reset();
// handle key press events
process.stdin.on('keypress', handleInput);
// set initial position of snake
posX = Math.floor(width / 2);
posY = Math.floor(height / 2);
// draw first apple
drawApple();
// start game loop
gameLoop();
} catch (ex) {
console.log(ex.toString());
quitGame();
}
function gameLoop() {
// remove snake at old position
removeSnake(posX, posY);
// set new position
posX = posX + dirX;
posY = posY + dirY;
// check new position
if (posX == 1 || posX == width || posY == 1 || posY == height) {
cursor.red();
cursor.bg.white();
setText(width / 2 - 6, height / 2, " GAME OVER ");
quitGame();
}
// check if apple is touched
if (posX == applePosX && posY == applePosY) {
// increase points
points++;
// increase speed
if (speed < 20) {
speed++;
}
// draw new apple
drawApple();
}
// draw snake at new position
drawSnake();
// call gameLoop
setTimeout(gameLoop, 1000 / speed);
}
function quitGame() {
cursor.reset();
cursor.bg.reset();
process.stderr.write('\x1B[?25h');
cursor.goto(1, height + 4);
process.exit();
}
function handleInput(chunk, key) {
if (key.name == 'q') {
quitGame();
} else if (key.name == 'right') {
dirX = 1;
dirY = 0;
} else if (key.name == 'left') {
dirX = -1;
dirY = 0;
} else if (key.name == 'up') {
dirX = 0;
dirY = -1;
} else if (key.name == 'down') {
dirX = 0;
dirY = 1;
}
}
function drawApple() {
applePosX = Math.ceil(Math.random() * (width - 2)) + 1;
applePosY = Math.ceil(Math.random() * (height - 2)) + 1;
cursor.bg.red();
drawPoint(applePosX, applePosY);
cursor.bg.reset();
setText(1, height + 2, "Points: " + points.toString());
setText(1, height + 3, "Speed: " + speed.toString());
}
function removeSnake() {
cursor.bg.black();
drawPoint(posX, posY);
cursor.bg.reset();
}
function drawSnake() {
cursor.bg.green();
drawPoint(posX, posY);
cursor.bg.reset();
}
function drawPoint(col, row, char) {
cursor.goto(col, row).write(' ');
}
function drawHorizontalLine(col, row, length) {
for (var i = 0; i < length; i++) {
cursor.goto(col + i, row).write(' ');
}
}
function drawVerticalLine(col, row, length) {
for (var i = 0; i < length; i++) {
cursor.goto(col, row + i).write(' ');
}
}
function setText(col, row, text) {
cursor.goto(col, row).write(text);
}