Das Spiel Snake auf dem Arduino Gamuino
Die Hardware für die Gamuino getaufte Kombination von Arduino, Funduino Joystick Shield und Nokia 5110 Display habe ich ja bereits beim letzten mal vorgestellt.Da war allerdings nur ein simples Testprogramm dabei, dass die Joystick-Position und Buttons auswertet und anzeigt.
Ich versprach, ein Spiel dafür zu entwickelt, und hier ist es nun. Es lag einfach zu nahe, hier Snake zu wählen, denn:
- ein Nokia Spiel auf einem Nokia Display passt wie Faust auf Auge
- Snake ist Kult und macht einfach Spaß
- Die Hardwareleistung des Arduino sollte für Snake locker ausreichen
- Auch das ein klein wenig träge Display ("Nachleuchtten") sollte mit Snake klarkommen
Gamuino Hardware-Erweiterung
Beim Entwickeln und Testen von der Gamuino Snake Version fehlte mir dann doch ein wenig Sound. Irgendwie vermisste ich das gewohnte "Piep", wenn die Schlange einen Punkt frisst.
Also beschloss ich, noch einen passiven Lautsprecher nachzurüsten. Der musste aber klein sein. Außerdem stellte sich die Frage: Wo anschließen?
Nach ein bisschen Kramen in der Elektronik-Grabbel-Kiste fand ich einen passenden Mikro-Lautsprecher, der noch unter die Abdeckplatte passt, und das ziemlich genau, so dass er der Abdeckplatte sogar noch zusätzlichen Halt gibt.
Die tone()-Funktion für den Arduino unterstützt glücklicherweise jeden Pin, so dass ich den Pin A4 wählen konnte. Praktischerweise gibt es einen Header mit A4 als SDA und daneben gleich Ground unten rechts auf der Platine. Auf einen Stecker musste ich allerdings verzichten, sonst hätte die Abdeckplatte nicht mehr drauf gepasst. Also wurde die Kabel direkt angelötet, auch wenn ich das eigentlich nicht gerne tue.
Snake
Wie ging eigentlich nochmal Snake? Das ist schon einige Zeit her, dass ich das ausgiebig auf meinen Nokias gespielt habe. Es gab eine Zeit, da war Nokia - wie sagt man so schön heute? - alternativlos. Auch ich war Nokia-Fan. Schon mein erstes Handy war ein Nokia 2210i. Später folgten andere Nokia, zwischendrin auch ein Alcatel und danach ein Motorola Razor. Mein letztes Nokia war ein 8210. Als das herauskam, war es Nokias Vorzeigemodell - sehr kompakt und vollgestopft mit Technik. Allerdings auch nicht ganz billig - um die 800 Mark habe ich zahlen müssen.Also das alte Nokia 8210 wieder rausgekramt, Akku wiederbelebt, eingeschaltet und siehe da: 20 Jahre alt und funktioniert immer noch. Und es läuft Snake V.1 darauf. Und ein paar andere Spiele, aber die sind eigentlich in Sachen Spielspaß nicht der Rede wert.
Da das Nokia Snake eigentlich ziemlich perfekt ist, habe ich mich daran orientiert:
- Es gibt 9 (bei mir 10) Geschwindigkeitsstufen. Je schneller, desto mehr Punkte pro gefressenem Punkt.
- An der Wand zerschellt die Schlange. Oder wenn sie sich selbst berührt (in den Schwanz beißt).
- Die Schlange ist durchgehend 3 Pixel breit und wenn man eine Kehrtwende macht, dann bleibt zwischen den Linien eine Pixelreihe frei. Das finde ich sehr übersichtlich und wollte das auch so realisieren
- Soundmäßig gibt es ein "Piep" beim Punkte fressen und ein "brrrrt", wenn man in die Wand oder sich selbst fährt
- Pro Level wird ein Highscore gespeichert.
So macht der Klassiker wieder Spaß! Auch wenn es nur ein simples Spiel mit ganz einfachen Regeln ist, so hat es doch immer noch einen gewissen Suchtfaktor.
Ich habe in folgendem Video noch ein bisschen zum Spieledesign erklärt. Außerdem führe ich das Programm vor. Und ja, das Nokia 8210 hat auch noch eien Gastauftritt auf seine alten Tage:
Source-Code
////////////////////////////////////////////////////////
// (C) 2019 by Oliver Kuhlemann //
// Bei Verwendung freue ich mich über Namensnennung, //
// Quellenangabe und Verlinkung //
// Quelle: http://cool-web.de/arduino/ //
////////////////////////////////////////////////////////
#include <EEPROM.h> // zum Speichern der Highscores im EEPROM
#include <SPI.h> // SPI Interface für Display
#include <Adafruit_GFX.h> // Grafik-Library
#include <Adafruit_PCD8544.h> // für das Nokia 5110 Display
#include "pitches.h" // Noten für die Melodien
// welcher Button hängt an welchem Pin?
#define btnA 2
#define btnB 3
#define btnC 4
#define btnD 5
#define btnE 6
#define btnF 7
#define btnK 8
#define joyX A0
#define joyY A1
#define pinSound A4
#define ButtonForDirection true // auf true setzen, damit die Buttons zur Richtungssteuerung benutzt werden
// Schalter auf 5V
#define center 512 // Wert Joystick Zentrum
#define centerrange 200 // wie groß ist der Mittelbereich, in dem eine Joystick-Bewegung noch nicht gewertet wird?
//Schalter auf 3V3
// #define center 343 // Wert Joystick Zentrum
// #define centerrange 150 // wie groß ist der Mittelbereich, in dem eine Joystick-Bewegung noch nicht gewertet wird?
boolean ba=false; // Zustandsspeicher Buttons
boolean bb=false;
boolean bc=false;
boolean bd=false;
boolean be=false;
boolean bf=false;
boolean bk=false;
int jx=512; // Zustandsspeicher Joystickposition
int jy=512;
boolean dl=false; // Zustandsspeicher Joystick-Direction digital (left, right, up, down)
boolean dr=false;
boolean du=false;
boolean dd=false;
Adafruit_PCD8544 display = Adafruit_PCD8544(9, 10, 11, 13, 12); // Pins für SCLK, DIN, D/C, CS, RST
char sf[20][11]; // Spielfeld
void setup() {
pinMode (btnA, INPUT_PULLUP);
pinMode (btnB, INPUT_PULLUP);
pinMode (btnC, INPUT_PULLUP);
pinMode (btnD, INPUT_PULLUP);
pinMode (btnE, INPUT_PULLUP);
pinMode (btnF, INPUT_PULLUP);
pinMode (btnK, INPUT_PULLUP);
pinMode (joyX, INPUT);
pinMode (joyY, INPUT);
Serial.begin (115200);
display.begin();
display.setContrast(60);
display.clearDisplay();
randomSeed(analogRead(A5));
}
void drawBlock (int x, int y, char d) { // Schlangensekment zeichnen
// fillRect(x,y, width, height, color)
if (d == 'r') display.fillRect(x*4-2,y*4-2, 4,3, BLACK);
if (d == 'l') display.fillRect(x*4-1,y*4-2, 4,3, BLACK);
if (d == 'd') display.fillRect(x*4-1,y*4-3, 3,4, BLACK);
if (d == 'u') display.fillRect(x*4-1,y*4-2, 3,4, BLACK);
}
void drawPoint(int x, int y) { // Fresspunkt zeichnen
display.drawPixel(x*4, y*4-2, BLACK);
display.drawPixel(x*4-1, y*4-1, BLACK);
display.drawPixel(x*4+1, y*4-1, BLACK);
display.drawPixel(x*4, y*4, BLACK);
}
void clearBlock (int x, int y) { // Block auf Display löschen
display.fillRect(x*4-2,y*4-2, 4,4, WHITE);
}
void punktSetzen(){
boolean leer = false;
int x;
int y;
while (!leer) {
x=random(1,20);
y=random(1,11);
if (sf[x][y] == ' ') { //hier ist noch platz
leer = true;
}
}
sf[x][y] = 'o';
drawPoint(x,y);
}
// Arduino Uno: 1kb EEPROM storage.
void saveHighscore (int gameNum, int platz, unsigned int score) {
int addrStart = 0;
int addr;
unsigned int score2=0;
if (gameNum == 0) addrStart = 0; // snake braucht 10 level mal 2 bytes (uint)
else if (gameNum == 1) addrStart = 20;
addr= addrStart + platz;
Serial.print ("Adresse: ");
Serial.println ("Adresse: ");
EEPROM.put(addr, score);
}
unsigned int loadHighscore (int gameNum, int platz) { // lädt Highscore aus dem EEPROM, bleibt auch nach Ausschalten erhalten
int addrStart = 0;
int addr;
unsigned int score=0;
byte b;
if (gameNum == 0) addrStart = 0; // snake braucht 10 level mal 2 bytes (uint)
else if (gameNum == 1) addrStart = 20;
addr= addrStart + platz;
EEPROM.get(addr, score);
return score;
}
void einschaltmelodie() {
// notes in the melody:
int melody[] = {
//NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3, 0, NOTE_B3, NOTE_C4
NOTE_C4, NOTE_C4, NOTE_D4, NOTE_B3 // Ga-mu-i-no
};
// note durations: 4 = quarter note, 8 = eighth note, etc.:
byte noteDurations[] = {
// 4, 8, 8, 4, 4, 4, 4, 4
4, 4, 4, 2
};
playMelody(melody, noteDurations, 4);
}
void playMelody( int melody[], byte noteDurations[], int anzNoten) {
for (int thisNote = 0; thisNote < anzNoten ; thisNote++) {
int noteDuration = 1000 / noteDurations[thisNote];
tone(pinSound, melody[thisNote], noteDuration);
int pauseBetweenNotes = noteDuration * 1.30;
delay(pauseBetweenNotes);
noTone(pinSound); // Tonausgabe stoppen
}
}
void loop() { // Hauptmenü
char *games[]= {"Snake","Test"};
int game=0;
int gamemax=1; // Anzahl Spiele
display.clearDisplay();
display.display();
int i=0;
while (1) {
display.clearDisplay();
display.setCursor(0,0);
display.setTextSize(2);
display.setTextColor(BLACK);
display.println ("Gamuino");
display.setTextSize(1);
display.println ("Choose Game:");
display.print (" < ");
display.print (games[game]);
display.println (" >");
display.println ("\nC: let's go");
display.display();
if (i==0) einschaltmelodie();
i++; if(i>1000) i =1;
delay(20);
if (digitalRead(btnC) == LOW) {
while (digitalRead(btnC) == LOW) delay (10); // auf loslassen warten
break;
}
if (digitalRead(btnD) == LOW) {
while (digitalRead(btnD) == LOW) delay (10); // auf loslassen warten
if (game>0) game--;
}
if (digitalRead(btnB) == LOW) {
while (digitalRead(btnB) == LOW) delay (10); // auf loslassen warten
if (game<gamemax) game++;
}
}
if (game == 0) snake();
if (game == 1) test();
}
void test() { // zeigt Joystick-Position und Buttons an
display.setTextSize(1);
display.setTextColor(BLACK);
while (1) {
display.clearDisplay();
display.setCursor(0,0);
display.println ("Joystick Test\n");
// Zustände einlesen und in Variablen speichern
ba=!digitalRead(btnA);
bb=!digitalRead(btnB);
bc=!digitalRead(btnC);
bd=!digitalRead(btnD);
be=!digitalRead(btnE);
bf=!digitalRead(btnF);
bk=!digitalRead(btnK);
jx=analogRead(joyX);
jy=analogRead(joyY);
dl = (jx < center - centerrange);
dr = (jx > center + centerrange);
du = (jy > center + centerrange);
dd = (jy < center - centerrange);
if (ButtonForDirection) {
if (!dl) dl=bd;
if (!dr) dr=bb;
if (!du) du=ba;
if (!dd) dd=bc;
}
display.print ("X:");
display.print (jx);
display.print (" Y:");
display.println (jy);
display.print ("Btns: ");
if (ba) display.print ("A");
if (bb) display.print ("B");
if (bc) display.print ("C");
if (bd) display.print ("D");
if (be) display.print ("E");
if (bf) display.print ("F");
if (bk) display.print ("K");
display.println();
display.print ("Dir.: ");
if (dl) display.print ("left ");
if (dr) display.print ("right ");
if (du) display.print ("up ");
if (dd) display.print ("down ");
display.println();
display.display();
delay(30);
}
}
void snake() { // Kennt man ja von Nokia Classic Phones
unsigned int score=0;
int level=1;
unsigned int highscore[10] = {0,0,0,0,0,0,0,0,0,0};
boolean newHighscore = false;
// Highscores laden
for (int i=0; i < 10; i++) {
highscore[i]=loadHighscore(0,i*2); // uint hat 2 Bytes
}
display.clearDisplay();
display.display();
display.setTextSize(2);
display.setTextColor(BLACK);
display.println ("Snake!");
display.setTextSize(1);
display.println ("(C) 2019 by");
display.println ("O. Kuhlemann");
display.println ("www.coolweb.de");
display.println ("Press Btn. C");
display.display();
// auf Tastendruck warten
while (digitalRead(btnC) == HIGH) delay (10);
start: //--------------------------------------------------
// evtl. C-loslassen abwarten
while (digitalRead(btnC) == LOW) delay (10);
display.clearDisplay();
display.display();
while (1) {
display.clearDisplay();
display.setCursor(0,0);
display.println ("Scores:");
display.print (" Last: ");
display.println (score);
display.print (" High: ");
display.println (highscore[level-1]);
display.fillRect (0,24, 84,10, WHITE);
display.print ("Level: < ");
display.print (level);
display.println (" >");
display.println ("\nC: start game");
display.display();
delay(20);
if (digitalRead(btnC) == LOW) {
while (digitalRead(btnC) == LOW) delay (10); // auf loslassen warten
break;
}
if (digitalRead(btnD) == LOW) {
while (digitalRead(btnD) == LOW) delay (10); // auf loslassen warten
if (level>1) level--;
}
if (digitalRead(btnB) == LOW) {
while (digitalRead(btnB) == LOW) delay (10); // auf loslassen warten
if (level<10) level++;
}
if (digitalRead(btnF) == LOW) {
while (digitalRead(btnB) == LOW) delay (10); // auf loslassen warten
display.clearDisplay();
display.setCursor(0,0);
display.print ("Really clear all Snake Highscores?\nA: Yes\nC: Cancel");
display.display();
// auf A oder C warten
while (digitalRead(btnA) == HIGH && digitalRead(btnC) == HIGH) delay (10);
if (digitalRead(btnA) == LOW) { // ja, tu es
for (int i=0; i < 40; i++) EEPROM.update (i,0);
display.clearDisplay();
display.setCursor(0,0);
display.print ("Done.\n\nPress Reset.");
display.display();
while (1) delay (1000);
}
}
}
display.clearDisplay();
display.display();
//Spielfeldgröße (in 4x4 px Quadranten)
byte xmax=20;
byte ymax=11;
for (int i=0; i <= 20; i++) {
for (int j=0; j <= 11; j++) {
sf[i][j]=' ';
}
}
// Umgrenzungen zeichnen, weil Snake daran zerschellt
// drawline (x,y, x2,y2, color)
display.drawLine(0,0, 83,0, BLACK);
display.drawLine(0,0, 0,47, BLACK);
display.drawLine(83,47, 83,0, BLACK);
display.drawLine(83,47, 0,47, BLACK);
// Anfangs-Snake zeichnen
// Snake ist 3 px dick, Schrittweite ist 4
// Startposition und Ausfüllung in Quadranten
int x=1;
int y=6;
char d='r';
char dbef='r';
int slen=3;
String weg= ""; // gegangenen weg speichern, um verfolgen zu können, wo Schwanzende zu löschen ist
// ersten drei Segmente der Anfangs-Snake
for (x=1; x <= slen; x++) {
sf [x][y]='#';
drawBlock(x,y, 'r');
}
x=slen;
weg="rr";
// ende speichern, um blöcke wieder zu löschen, wird nach jedem Löschen neu gesetzt nach gespeicherter richtung
int xend=1;
int yend=6;
boolean eaten=false;
char dend=' '; // direction schwanzende
score=0; // erreichte Punkte
int mult=level; // Multiplikator wegen Schwierigkeit
delay(1000);
punktSetzen();
while (1) { // Spiel-Schleife
dbef = d;
for (int w=0; w < 500/level; w++) { // Joystick-Abfrage Schleife, die auch Spielgeschwindigkeit bestimmt
// Zustände einlesen und in Variablen speichern
ba=!digitalRead(btnA);
bb=!digitalRead(btnB);
bc=!digitalRead(btnC);
bd=!digitalRead(btnD);
be=!digitalRead(btnE);
bf=!digitalRead(btnF);
bk=!digitalRead(btnK);
jx=analogRead(joyX);
jy=analogRead(joyY);
dl = (jx < center - centerrange);
dr = (jx > center + centerrange);
du = (jy > center + centerrange);
dd = (jy < center - centerrange);
if (ButtonForDirection) {
if (!dl) dl=bd;
if (!dr) dr=bb;
if (!du) du=ba;
if (!dd) dd=bc;
}
// weiterlaufen, wenn nichts gedrückt
// gegensätzlich zur aktuellen Laufrichtung ignorieren, das wär der sofortige
// Tod und der Joystick federt evtl. zurück, das wäre gemein
if (dl) { if (dbef!='r') d='l'; }
else if (dr) { if (dbef!='l') d='r'; }
else if (du) { if (dbef!='d') d='u'; }
else if (dd) { if (dbef!='u') d='d'; }
delay (1);
}
// eine Schrittweite in diese Richtung gehen
if (d == 'r') x++;
if (d == 'l') x--;
if (d == 'd') y++;
if (d == 'u') y--;
// Außerhalb des Rahmens?
if (x <1 || x >20 || y <1 || y>11) goto gameOver;
// in den eigenen Schwanz gebissen?
if (sf [x][y] == '#') goto gameOver;
// Punkt gefressen?
eaten = (sf [x][y] == 'o');
// wenn Punkt gefressen nur verlängern
// wenn keinen Punkt gefressen verlängern und letztes Glied löschen
// neuer Block
sf [x][y]='#';
weg += d;
dend=weg[0];
drawBlock(x,y,d);
if (eaten) {
tone(pinSound, 4000, 20);
score += mult;
// wir brauchen einen neuen Punkt zum fressen
punktSetzen();
} else {
// Schwanzende löschen
sf [xend][yend]=' ';
clearBlock(xend,yend);
// für nächsten Durchlauf vorbereiten
weg=weg.substring(1);
if (dend == 'r') xend++;
if (dend == 'l') xend--;
if (dend == 'd') yend++;
if (dend == 'u') yend--;
}
display.display();
}
gameOver: //--------------------------------------------------
for (int i=0; i<10; i++) {
tone(pinSound,200,5);
delay (5);
tone(pinSound,300,5);
delay (5);
}
newHighscore=false;
if (score > highscore[level-1]) {
newHighscore=true;
highscore[level-1] = score;
saveHighscore(0, (level-1)*2, score); // ein uint (bis 65535) speichern
}
display.setTextSize(2);
display.setTextColor(BLACK);
display.setCursor(5,5);
display.println(newHighscore ? "HIGH" : "GAME");
display.setCursor(5,20);
display.println(newHighscore ? "SCORE!" :"OVER!");
display.fillRect(4,37, 76,8, WHITE);
display.setTextSize(1);
display.setCursor(5,38);
display.print("Score: ");
display.print(score);
display.display();
// auf Tastendruck warten
while (digitalRead(btnC) == HIGH) delay (10);
goto start;
}