Snake auf dem Arduino

Nachdem wir nun schon einige Anforderungen im Zuge unseres Projekts realisiert haben und zuletzt die Programmierwerkzeuge für den Arduino ausführlich erklärt wurden, geht es nun um die Realisierung von dem bekannten Spiel Snake.

Was ist Snake überhaupt?

Snake_trs-80

Hyper-Wurm auf dem TRS-80 – Quelle: Wikipedia

Snake ist ein Klassiker unter den Spielen. Die Geschichte des Kultspiels reicht bis in das Jahr 1979 zurück, in dem es vermutlich zum ersten Mal von F.Seger für TRS-80 Computer entwickelt wurde. Diese Implementierung trug den Namen “Hyper-Wurm”.

In dem Spiel steuert man eine Schlange, bestehend aus mehreren aneinanderreihenden Pixeln, die man auf einem Spielfeld in verschiedene Richtungen steuern kann. Dabei werden weitere Pixel, oft auch Früchte oder Äpfel genannt, zufällig auf dem Spielfeld verteilt, die der Spieler einsammeln muss. Mit jeder Einsammlung dieser Pixel wird die Schlange länger, wodurch das Spiel so lange läuft, bis die Schlange des Spielers das gesamte Spielfeld bedeckt.

Die Schwierigkeit dabei ist, nicht den Rand des Spielfeldes zu überqueren oder einen Teil der Schlange zu berühren. Geschieht das, ist das Spiel vorbei.

 

Snake Reloaded

Wir entschieden uns, Snake in ähnlicher Form auf dem Arduino zu realisieren. Einige Änderungen nehmen wir jedoch vor. Beispielsweise soll das Spiel nicht vorüber sein, wenn der Rand berührt wird sondern die Schlange soll auf der anderen Seite herauskommen.

 

Implementierung der Spiel-Logik

Zu Beginn wird das Spielfeld in einem zweidimensionalem Array arduino_snakeabgebildet. Dabei bildet die erste Dimension die Spalte (X Achse) und die zweite Dimension die Zeile (Y Achse). Das Array wird komplett mit Nullen initialisiert. Der Wert 0 im Array bedeutet, dass an der Stelle nichts ist. -1 steht für die Frucht und alles > 0 (1, 2, 3, …) repräsentiert die Schlange. Manche mögen jetzt fragen: Wieso fortlaufende Zahlen? Weshalb keine konstante Zahl, wie auch bei der Frucht? Dadurch, dass die Schlange in Bewegung ist, muss der Kopf der Schlange um eins erweitert und das Ende um eins reduziert werden. Das ist technisch insofern noch umsetzbar, wenn es sich dabei um eine gerade Linie handelt aber da die Schlange natürlich auch über Kurven geht, ist dies nicht mehr so einfach. Deshalb wird die Länge der Schlange als fortlaufende Zahl genutzt. Beim nächsten Zug wird das Spielfeld dann einfach um eins vermindert, sofern die Zahl > 0 und ist und so wird dafür gesorgt, dass sich das Ende der Schlange pro Zug um eins vermindert.

Startet das Spiel, wird eine zufällige Position in der 8×8 Matrix generiert und anschließend in das Spielfeld geschrieben. Dann wäre da noch die Frucht, die auch bei der Initialisierung zufällig generiert und Spielfeld platziert wird. Nach weiteren Initialisierungen von Variablen und der Festlegung der initialen Bewegungsrichtung (Oben) ist ein Teil der Logik implementiert.

Nach der Initialisierung greift das Prinzip der sogenannten Game Loop, einem Prozess der sich alle paar Millisekunden wiederholt. Es ist eine endlose Schleife, die so lange wie das Programm läuft.

Während dieser Endlosschleife wird die weitere Spiellogik behandelt. Die Richtung wird entsprechend der gedrückten Richtungstasten geändert, die Schlange wird fortbewegt und wächst ggf. und es wird geprüft, ob das Spiel vorbei ist weil bestimmte Bedingungen (Schlange berührt) eingetroffen sind.

Am Ende folgt die Anzeige eines Schriftzuges, der neben den Worten “Game Over” noch zusätzlich die erreichte Punktzahl, den Score, anzeigt. Die Punktzahl wird berechnet aus der folgenden Formel:

Erreichte Länge der Schlange – Startlänge

Möchte man das Spiel erneut spielen, muss der Reset Button auf dem Arduino betätigt werden.

 

Snake in Action

Und wenn der Quellcode auf den Arduino übertragen wurde, sieht das dann so aus:

Musik: TeknoAXE’s Royalty Free Music – #237-A (WWWWub) 8-bit/Dubstep/Techno auf YouTube

Hier geht es zum nächsten Schritt, bei dem wir die Entwicklung von Tetris, dem zweiten Spiel, dokumentieren.

Quellcode

#include <gfxfont.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_LEDBackpack.h>

// Pinbelegung der Buttons für die Steuerung
#define PIN_BUTTON_RIGHT 2
#define PIN_BUTTON_LEFT 3
#define PIN_BUTTON_TOP 4
#define PIN_BUTTON_BOTTOM 5

// Länge und Breite des Spielfelds
#define MATRIX_HORIZONTAL_LENGTH 8
#define MATRIX_VERTICAL_LENGTH 8 

#define DIRECTION_TOP 0
#define DIRECTION_RIGHT 1
#define DIRECTION_BOTTOM 2
#define DIRECTION_LEFT 3

// Prüft ob der Wert eine Schlange repräsentiert (im Spielfeld)
#define SNAKE(a) (a > 0)

int field[MATRIX_HORIZONTAL_LENGTH][MATRIX_VERTICAL_LENGTH] = { 0 };
int snakeHeadX;
int snakeHeadY;

Adafruit_8x8matrix matrix = Adafruit_8x8matrix();  
int direction = DIRECTION_TOP; 
int snakeLength = 3;                             
unsigned long prevTime = 0;   
unsigned long delayTime = 400;     

int fruitX, fruitY;
unsigned long fruitPrevTime = 0;
unsigned long fruitBlinkTime = 200;
int fruitLed = LED_ON;

boolean gameOverShown = false;
boolean gameOver = false;

// Setup - wird einmal am Start des Programms aufgerufen
void setup() 
{
	// LED Matrix initialisieren
	randomSeed(analogRead(0));
	matrix.begin(0x70);
	matrix.setRotation(3);

	// Spiel initialisieren, Anfangsposition der Schlange zufällig setzen
	snakeHeadX = random(0, MATRIX_HORIZONTAL_LENGTH);
	snakeHeadY = random(0, MATRIX_VERTICAL_LENGTH);
	field[snakeHeadX][snakeHeadY] = snakeLength;

	// Erste Frucht platzieren
	makeFruit();
}

// Prüft ob der Button, der an buttonPin anliegt gedrückt ( = HIGH) wurde
boolean buttonClicked(int buttonPin) 
{
	return digitalRead(buttonPin) == HIGH;
}

// Wird ständig aufgerufen, so lange das Programm läuft
void loop()
{
	// ggf. Richtung der Schlange ändern
	checkButtons();

	// Game Loop verzögern, damit das Game nicht sofort vorrüber ist...
	unsigned long currentTime = millis();
	if (currentTime - prevTime >= delayTime) {
		nextstep();
		prevTime = currentTime;
	}

	// Spielfeld auf Matrix zeichnen
	draw();
}

// Prüft die Richtungstasten und setzt die Bewegungsrichtung der Schlange
// + Validierung um zu prüfen dass Schlange nicht "in sich" hereinläuft
void checkButtons()
{
	int currentDirection = direction;
	if (buttonClicked(PIN_BUTTON_LEFT) && currentDirection != DIRECTION_RIGHT) {
		direction = DIRECTION_LEFT;
	}
	else if (buttonClicked(PIN_BUTTON_RIGHT) && currentDirection != DIRECTION_LEFT) {
		direction = DIRECTION_RIGHT;
	}
	else if (buttonClicked(PIN_BUTTON_TOP) && currentDirection != DIRECTION_BOTTOM) {
		direction = DIRECTION_TOP;
	}
	else if (buttonClicked(PIN_BUTTON_BOTTOM) && currentDirection != DIRECTION_TOP) {
		direction = DIRECTION_BOTTOM;
	}
}

// Zeichnet das Spielfeld (Schlange + Frucht) auf der LED Matrix
void draw() 
{
	matrix.clear();
	// Wenn das Spiel noch läuft...
	if (!gameOver)
	{
		for (int x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++)
		{
			for (int y = 0; y < MATRIX_VERTICAL_LENGTH; y++)
				matrix.drawPixel(x, y, SNAKE(field[x][y]));
		}

		drawFruit();
		
		// überträgt die Änderungen an dem matrix Objekt auf die LED Matrix.
		matrix.writeDisplay();
	}
	// Wenn Game Over...
	else
	{
		// Matrix konfigurieren sodass Schriftzug ausgegeben werden kann
		matrix.setTextSize(1);
		matrix.setTextWrap(false);
		matrix.setTextColor(LED_ON);

		// Schriftzug GAME OVER nur einmal anzeigen
		if (!gameOverShown)
		{
			for (int8_t x = 0; x >= -56; x--) {
				matrix.clear();
				matrix.setCursor(x, 0);
				matrix.print("GAME OVER");
				matrix.writeDisplay();
				delay(100);
			}

			gameOverShown = true;
		}
		// Anschließend immer wieder den erreichten Score anzeigen
		else {
			for (int8_t x = 0; x >= -56; x--) {
				matrix.clear();
				matrix.setCursor(x, 0);
				matrix.print("SCORE: ");
				matrix.print(snakeLength);
				matrix.writeDisplay();
				delay(100);
			}
		}
	}
}

// Zeichnet die Frucht auf die Matrix und implementiert das Blinken des Pixels
void drawFruit() 
{
	if (inPlayField(fruitX, fruitY)) {
		unsigned long currenttime = millis();
		if (currenttime - fruitPrevTime >= fruitBlinkTime) {
			fruitLed = (fruitLed == LED_ON) ? LED_OFF : LED_ON;
			fruitPrevTime = currenttime;
		}
		matrix.drawPixel(fruitX, fruitY, fruitLed);
	}
}

// Gibt zurück ob die angegebenen Koordinaten im Spielfeld liegen
boolean inPlayField(int x, int y) 
{
	return (x >= 0) && (x<MATRIX_HORIZONTAL_LENGTH) && (y >= 0) && (y<MATRIX_VERTICAL_LENGTH);
}

// Schlange verschieben, ggf. verlängern und feststellen ob Spiel vorbei ist
void nextstep() 
{
	int newX = snakeHeadX;
	int newY = snakeHeadY;

	// Basierend auf derzeitiger Richtung die Schlange verschieben
	switch (direction) {
		case DIRECTION_TOP:
			newY--;
			break;
		case DIRECTION_RIGHT:
			newX++;
			break;
		case DIRECTION_BOTTOM:
			newY++;
			break;
		case DIRECTION_LEFT:
			newX--;
			break;
	}

	// Wenn neue Position über die Grenzen der Matrix hinausragt, neue Position auf die andere Seite setzen
	if (newY >= MATRIX_VERTICAL_LENGTH)
		newY = 0;
	else if (newY < 0)
		newY = MATRIX_VERTICAL_LENGTH - 1;

	if (newX >= MATRIX_HORIZONTAL_LENGTH)
		newX = 0;
	else if (newX < 0)
		newX = MATRIX_HORIZONTAL_LENGTH - 1;

	// Wenn der Kopf ein Teil der Schlange berührt, war's das...
	if (isPartOfSnake(newX, newY))
	{
		gameOver = true;
		delay(3000);
		return;
	}

	// Wenn eine Frucht eingesammelt wird, Schlange erweitern und neue Frucht erstellen
	if ((newX == fruitX) && (newY == fruitY)) {
		snakeLength++;
		makeFruit();
	}

	// Spielfeld anpassen sodass Schlange verschoben wird
	for (int x = 0; x < MATRIX_HORIZONTAL_LENGTH; x++)
	{
		for (int y = 0; y < MATRIX_VERTICAL_LENGTH; y++)
		{
			int value = field[x][y];
			if (SNAKE(value))
				field[x][y] = value - 1;
		}
	}

	snakeHeadX = newX;
	snakeHeadY = newY;
	field[newX][newY] = snakeLength;
}

// Erstellt eine Frucht auf einer zufälligen Position innerhalb des Spielfelds
void makeFruit() 
{
	int x, y = 0;

	// Zufällige Position so generieren, dass sie nicht auf der Schlange liegt
	do {
		x = random(0, MATRIX_HORIZONTAL_LENGTH);
		y = random(0, MATRIX_VERTICAL_LENGTH);
	} while (isPartOfSnake(x, y));

	fruitX = x;
	fruitY = y;
}

// Prüft ob die übergebenen Koordinaten (der Punkt) ein Teil der Schlange ist
boolean isPartOfSnake(int x, int y)
{
	return SNAKE(field[x][y]);
}

Ein Gedanke zu „Snake auf dem Arduino

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.