9
Arduino-Retro- Spielkonsole 126 | Make: 5/2015 MIKROCONTROLLER-DISPLAY | Ein günstiger Mini-Bildschirm, zwei Drucktaster und ein analoger Joystick verwandeln einen Arduino Uno im Handumdrehen in eine Spielkonsole. Mit der macht nicht nur das Programmieren Spaß. von Maik Schmidt © Copyright by Maker Media GmbH Persönliches PDF für Günther Ehrenberger aus 4511 Allhaming

Arduino-Retro- Spielkonsole · Make: 5/2015 | 127 D ie massenhafte Produktion von MP3-Spielern, Smartphones und Videospiel-Konsolen ist in mancher Hinsicht ein wahrer Segen für Hobby-Elektroniker

  • Upload
    phamque

  • View
    217

  • Download
    0

Embed Size (px)

Citation preview

Arduino-Retro-Spielkonsole

126 | Make: 5/2015

MIKROCONTROLLER-DISPLAY |

Ein günstiger Mini-Bildschirm, zwei Drucktaster und einanaloger Joystick verwandeln einen Arduino Uno imHandumdrehen in eine Spielkonsole. Mit der machtnicht nur das Programmieren Spaß.

von Maik Schmidt

ch.0515.126-134.qxp 08.10.15 17:06 Seite 126

© Copyright by Maker Media GmbHPersönliches PDF für Günther Ehrenberger aus 4511 Allhaming

Make: 5/2015 | 127

Die massenhafte Produktion von MP3-Spielern, Smartphones und Videospiel-

Konsolen ist in mancher Hinsicht ein wahrerSegen für Hobby-Elektroniker. Plötzlich gibtes viele Bauteile, die früher den Profis vorbe-halten waren, zu Schnäppchenpreisen an fastjeder Ecke. Das gilt insbesondere für Displaysaller Art und so einige davon eignen sich her-vorragend für den Einsatz am Arduino.

Zum Beispiel gibt es kostengünstige OLED-Displays, die nur wenig Strom verbrauchenund sich über gerade einmal zwei Pins kon-trollieren lassen. Was liegt da näher, als nocheinen Joystick und eine Handvoll Drucktasteraus der Werkzeugkiste zu kramen und denTraum von der eigenen Videospiel-Konsoleendlich mal Wirklichkeit werden zu lassen?

Dieser Artikel zeigt, wie sich ein solchesProjekt mit wenig Aufwand umsetzen lässt.Er zeigt auch, dass knapp Tausend Pixeldurchaus genug für unterhaltsame Action-Spiele sind und er erklärt Schritt für Schritt,wie die eingesetzte Hardware funktioniertund wie sie durch ein wenig Software opti-mal genutzt wird.

Ein Exklusiv-Titel inklusive

Wie es sich gehört, liegt der Mini-Spielkon -sole ein erstes Spiel namens Shootduino bei.Der Protagonist in Shootduino ist ein Raum-schiff, das der Spieler über den gesamtenBildschirm bewegen kann. Ein Sternenfeldim Hintergrund erweckt den Eindruck, dasRaumschiff bewege sich permanent vonlinks nach rechts. Am rechten Bildschirmrandtauchen immer wieder Asteroiden auf, diesich in unterschiedlichen Geschwindigkeitenauf den Spieler zu bewegen.

Ziel des Spiels ist es, so viele Asteroidenwie möglich mit einer Laserkanone zu elimi-nieren. Dazu hat der Spieler drei Versuche.Sind diese aufgebraucht, ist das Spiel zuEnde. Das Spiel endet allerdings auch, wennder Spieler mindestens fünf Asteroidennicht zerstört.

Ein Kilopixel

Für das Projekt kommt ein OLED-Displaymit einer Bildschirmdiagonale von 0,96 Zollund einer Auflösung von 128  x 64  Pixelnzum Einsatz. Das entspricht in etwa dem,was in den Nullerjahren des 21.  Jahrhun-derts in Handys und MP3-Spielern verbautwurde. Diese Displays sind preiswert, ver-brauchen nur wenig Energie und sind ein-fach zu programmieren.

Für die einfache Ansteuerung sorgt einSSD1306-Controller (Links zu Datenblättern,Bibliotheken und Bezugsquellen siehe Endedieses Artikels). Eine seiner nützlichsten Eigenschaften ist der interne Grafikspeichervon 128 x 64 Bits (1 KByte), der exakt den In-halt einer Bildschirmseite aufnehmen kann.Solange der Controller keine neuen Kom-mandos empfängt und der Display-Speichernicht verändert wird, zeigt er den aktuellenInhalt des internen Speichers an. Arduino-Anwendungen müssen sonst in der Regelden Inhalt des internen Display-Speichersauch im Speicher repräsentieren. Bei die-sem Display reicht es, eine lokale Kopie desBildschirms (1 KByte) zu erzeugen und nurbei Bedarf zu übertragen, worum sich dieAdafruit-Bibliothek kümmert. Es ist möglich,eigene Bibliotheken zu entwickeln, die ganzohne SRAM auskommen.

Der SSD1306 unterstützt unter anderemdie seriellen Protokolle I2C und SPI und be-legt daher nur zwei beziehungsweise vierPins auf dem Arduino. Allerdings reichennicht alle Displays die Pins für alle Protokol-le nach außen. Das vorliegende Projektsetzt auf I2C, und das hier verwendete Dis-play hat auch tatsächlich nur vier Pins:Stromversorgung (VCC), Masse (GND), SCL(Clock Line) und SDA (Data Line).

Getreu dem Protokoll

Das Protokoll zwischen Arduino und demDisplay ist nicht allzu kompliziert. Dennoch

|

KurzinfoZeitaufwand:eine Stunde

Kosten:rund 40 Euro

Programmieren:gute Grundkenntnissemit Arduino

Löten:höchstens ein paar Lötpunkte

Material»Arduino Uno»0,96-Zoll-OLED-Display»Analog-Joystick»Breakout-Board für

Analog-Joystick»zwei Drucktaster»Breadboard (Steckplatine)»Kabel

Schwierigkeitsgrad

leicht schwer

Die Idee, den Arduino mit hochwerti-gen Displays zu verbinden, ist nahelie-gend und es überrascht nicht, dass esim Internet einige ähnliche Projektegibt.

Arduboy wurde erst kürzlich erfolg-reich über eine Kickstarter-Kampagnefinanziert und die Hardware ähnelt derim Artikel vorgestellten sehr. Allerdingssteckt der Arduboy in einem schickenGehäuse und hat einen Akku, der sichper USB aufladen lässt. Zur Steuerungdienen statt eines Joysticks aber vierDrucktaster; das Spielgefühl unterschei-det sich von unserer Konsole.

Der TinyDuino verfügt gleich über zweiAnalog-Sticks und ein Farb-Display. Er hatebenfalls einen Akku und ist extrem klein.

Schließlich gibt es noch den ArduinoColor, der so etwas wie ein Hybrid ausArduboy und TinyDuino ist. Er hat einenAnalog-Joystick und ein Farb-Display.Portabel ist er auch, aber statt durcheinen Akku wird er durch vier Batterienangetrieben.

Ein paar Spiele gibt es bereits für alldiese Plattformen. Die Techniken, die indiesem Artikel vermittelt werden, lassensich auch auf Arduboy & Co. übertragen.

ÄHNLICHE PROJEKTE

ch.0515.126-134.qxp 08.10.15 17:07 Seite 127

© Copyright by Maker Media GmbHPersönliches PDF für Günther Ehrenberger aus 4511 Allhaming

128 | Make: 5/2015

wäre es lästig, zeitaufwendig und fehler-trächtig, das ganze Byte-Gefriemel selbst zuimplementieren.

Dankenswerterweise haben anderediese Arbeit bereits erledigt und auf Githubdie Bibliothek Adafruit-SSD1306 veröffent-licht. Diese Bibliothek kapselt alle Internader SPI- und I2C-Kommunikation und stelltdie nativen Funktionen des Displays übereine schlanke C++-Klasse zur Verfügung.

Neben der Darstellung des Display-Spei-cherinhalts erledigt der SSD1306 auf Hard-ware-Ebene nur wenige Aufgaben. Dazu ge-hören die Invertierung der Aus gabe, dasDimmen des Displays oder das Scrollen vonInhalten. Alles andere, wie das Zeichnen vonPunkten, Linien, Kreisen, Texten oder Bitmap-Grafiken, muss der Arduino übernehmen.

Auch hier gibt es Erleichterung in Formder Bibliothek Adafruit-GFX. Diese unter-stützt neben dem SSD1306 noch weitereController und bietet eine Vielzahl anFunktionen zum Zeichnen von Grafiken.

Die Installation von Bibliotheken erfolgtin der aktuellen Arduino-IDE am bestenüber den Library-Manager. Der verbirgt sichhinter dem Menüpunkt „Sketch/Include Library“ und bietet eine komfortable Such-funktion. Neue Bibliotheken kann man hierper Mausklick installieren, sofern sie auf derListe der offiziellen Arduino-Bibliothekenstehen. Sowohl für Adafruit-SSD1306 alsauch für Adafruit-GFX ist das der Fall.

Hallo Display!

Um die Funktionstüchtigkeit des Displayszu testen, bietet es sich an, das Beispielssd1306_128x64_i2c in der Arduino-IDEüber das Menü „File/Examples/AdafruitSSD1306“ zu laden. Dieser Sketch enthälteine umfangreiche Demo, die alle Möglich-keiten der Adafruit-GFX-Bibliothek zeigt.

Bleibt das Display allerdings schwarz,nachdem der Sketch übersetzt und auf denArduino übertragen wurde, liegt der Grundin folgender Anweisung in der setup()-Funk-tion des Demo-Sketches:

display.begin(SSD1306_SWITCHCAPVCC, 0x3D);

Hier wird das Display initialisiert und diehexadezimale Zahl 0x3D als ID des anzu-sprechenden I2C-Geräts verwendet. Dieüberwiegende Mehrheit der getestetenDisplays hatte aber die ID 0x3C. Oft ist dieID auch auf der Platine des Displays ver-merkt. Aber Achtung: Dort stehen in derRegel 8-Bit-Adressen, während der Ardu -

MIKROCONTROLLER-DISPLAY |

FirstSteps

#include <SPI.h>#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h>

const uint8_t I2C_ADDRESS_DISPLAY = 0x3C;const uint8_t OLED_RESET = 4;

Adafruit_SSD1306 display(OLED_RESET);

void setup() {display.begin(SSD1306_SWITCHCAPVCC, I2C_ADDRESS_DISPLAY); display.clearDisplay();display.setTextColor(WHITE);display.setTextSize(4);display.setCursor(16, 16);display.print("Make");display.drawRect(0, 0, 127, 63, WHITE);display.display();

}

void loop() { }

Der Aufbau des Projekts ist einfach, denn sogut wie alle Teile lassen sich über gesteckteKabel verbinden. ANDERES

DISPLAY?Falls Sie ein anderes Display

verwenden, etwa das Monochrome1,3" 128ˇxˇ64 von Adafruit, finden

Sie über den Link am Ende desArtikels Informationen, wie Sie die

Schaltung der Spielkonsoleverändern müssen.

ch.0515.126-134.qxp 08.10.15 17:07 Seite 128

© Copyright by Maker Media GmbHPersönliches PDF für Günther Ehrenberger aus 4511 Allhaming

Make: 5/2015 | 129

|

ino die 7-Bit-Adresse verwendet. Auf demeingesetzten Display liest man beispiels-weise die Adresse 0x78. Schiebt man dieum ein Bit nach rechts, ergibt das 0x3C.

Sobald die richtige ID eingetragen undder Sketch auf den Arduino übertragenwurde, sollte es auf dem Display jedeMenge zu sehen geben. Das Studium desDemo-Sketches ist in jedem Fall sinnvoll,aber aufwendig. Einfacher ist es, mit einemkleinen Beispiel anzufangen und diesessukzessive zu erweitern.

Erste Schritte

Unser FirstSteps-Sketch (siehe Listing aufSeite 128) demonstriert die Initialisierungdes Displays und gibt sowohl einen Text(„Make“) als auch ein Rechteck aus. Die ge-samte Action findet in der setup()-Funktionstatt, in der zuerst das globale Objekt displaymit der begin()-Funktion initialisiert wird. Deranschließende Aufruf von clearDisplay() istnotwendig, weil der Display-Speicher sonstdas Logo der Firma Adafruit enthält.

Die folgenden beiden Anweisungensetzen die Textfarbe auf weiß und die Text-größe auf 4. Der Basis-Zeichensatz hat eineBreite von 6  Pixeln und die Textgröße  1.Die weiteren Textgrößen sind jeweils Viel-fache von  6, also haben Zeichen mit derTextgröße 4 eine Breite von 24 Pixeln.

Der Aufruf von setCursor() positioniertden Cursor pixelgenau auf eine x-y-Koor-dinate und print() gibt schließlich den Textaus. Die Funktion drawRect() zeichnet einRechteck und erwartet die Koordinatender linken oberen Ecke und die Breite undHöhe des zu zeichnenden Rechtecks. Mitder display()-Funktion wird der Inhalt desinternen Grafik-Puffers an das Displayübertragen und ausgegeben. In der loop()-Funktion ist anschließend nichts zu tun,weil das Display den einmal übertragenenInhalt so lange anzeigt, bis er verändertwird.

Texte ausgeben

Die Adafruit-Bibliotheken lassen in Bezugauf die Ausgabe von Texten kaum Wün-sche offen. Texte können pixelgenau posi-tioniert werden und es stehen verschiede-ne Schriftgrößen zur Verfügung. Zu langeTexte werden automatisch abgeschnittenoder auf Wunsch umgebrochen.

Ein kleines Problem gibt es aber doch,denn die Ausgabe von Texten, die sich imFlash-RAM befinden, ist nicht ohne Weite-res möglich. Das ist für Anwendungen wie

Alternativ zum Joystick und den Drucktastern kannauch ein fertiges Joystick-Shield eingesetzt werden.

Den Mini-Joystickmuss man nochauf das Breakout-Board löten.

FirstStepsFlash

#include <SPI.h>#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h>#include "textutils.h"

const uint8_t I2C_ADDRESS_DISPLAY = 0x3C;const uint8_t OLED_RESET = 4;

Adafruit_SSD1306 display(OLED_RESET);

void setup() {display.begin(SSD1306_SWITCHCAPVCC, I2C_ADDRESS_DISPLAY);display.clearDisplay();pmem_print_center(16, 4, PSTR("Make"));display.drawRect(0, 0, 127, 63, WHITE);display.display();

}

void loop() { }

ch.0515.126-134.qxp 08.10.15 17:07 Seite 129

© Copyright by Maker Media GmbHPersönliches PDF für Günther Ehrenberger aus 4511 Allhaming

MIKROCONTROLLER-DISPLAY |

Spiele aber zwingend notwendig, weilauch viele kurze Texte recht schnell vielPlatz im kostbaren SRAM belegen. Dahersollten Texte tunlichst im Flash-RAM abge-legt werden.

Das PSTR-Makro verlagert einen Text insFlash-RAM. Leider funktioniert die folgendeAnweisung nicht wie gewünscht:

display.print(PSTR("Make"));

Der Mangel ist aber schnell behoben und diezum Download bereitgestellten Dateien textutils.h und textutils.cpp enthalten Funk-tionen, mit denen sich Texte aus dem Flash-RAM ausgeben lassen. pmem_print_center() gibtTexte sogar gleich horizontal zentriert aus.

Die eigentliche Arbeit erfolgt inpmem_print(), denn hier wird der Text mit derFunktion pgm_read_byte() Byte für Byte ausdem Flash-RAM gelesen und ausgegeben.

Das Beispiel FirstStepFlash zeigt, wie dieFunktionen eingesetzt werden.

Effekthascherei

Die bisherigen Beispiele waren allesamt sta-tisch, das heißt, auf dem Bildschirm bewegtesich nichts. Das lässt sich schnell ändern unddas animierte Sternenfeld im Hintergrundvon Shootduino ist ein dankbares Demons-trationsobjekt. Es verleiht dem Spiel ein ge-wisses Weltraumflair und lässt sich mit nurwenigen Zeilen Code implementieren. Als insich abgeschlossenes Demoprogramm gibtes das Sternenfeld im Ordner namens Star-field in unseren Downloads, im fertigenShootduino-Spiel ist der Code in starfield.hund starfield.cpp zu finden.

Jeder Stern wird durch die Struktur Starrepräsentiert. Die Struktur enthält die aktu-ellen Bildschirmkoordinaten des Sterns undseine aktuelle Geschwindigkeit vx in x-Rich-tung. Das Sternenfeld wird durch ein Arraysolcher Strukturen dargestellt und durchdrei Funktionen manipuliert.

Die Funktion init_starfield() belegt die Attri-bute aller Sterne mit zufälligen Werten.move_stars subtrahiert die aktuelle Geschwin-digkeit von der x-Koordinate eines jedenSterns. Dadurch hat es den Anschein, derStern bewege sich von rechts nach links. Ver-lässt der Stern den Bildschirm auf der linkenSeite, wird er durch einen neuen Stern er-setzt. Schließlich gibt draw_stars() alle Sterneals einzelne Pixel auf dem Bildschirm aus.

Auf mehrfarbigen Displays simuliert eineinfacher Trick übrigens deutlich mehrräumliche Tiefe. Dazu müssen die Sternelediglich in verschiedenen zufälligen Grau-tönen dargestellt werden. Dunklere Sterneerscheinen dann automatisch weiter weg.

Grafiken erzeugen …

1024 monochrome Pixel können schwer-lich als Grundlage für ein opulentes Grafik-feuerwerk dienen. Trotzdem sind sie aus-reichend für unterhaltsame Spiele, wobeiknackige Bitmaps ordentlich zum Spiel-spaß beitragen.

Prinzipiell eignet sich jedes Grafikpro-gramm der Welt zur Erstellung der Mini-Bitmaps, die das Display darstellen kann.Allerdings haben moderne Anwendungenwie GIMP oder Photoshop einen völlig an-deren Fokus und ihre Werkzeuge sindnicht in erster Linie für die Bearbeitungkleinster Grafiken konzipiert. Das gilt im

Starfield

#include <SPI.h>#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h>

const uint8_t I2C_ADDRESS_DISPLAY = 0x3C;const uint8_t OLED_RESET = 4;const uint8_t MAX_STARS = 10;

struct Star { int8_t x, y, vx; };

Star starfield[MAX_STARS];Adafruit_SSD1306 display(OLED_RESET);

void init_starfield() {for (uint8_t i = 0; i < MAX_STARS; i++) {starfield[i].x = random(display.width());starfield[i].y = random(display.height()); starfield[i].vx = random(3) + 1;

}}

void move_stars() {for (uint8_t i = 0; i < MAX_STARS; i++) {starfield[i].x -= starfield[i].vx;if (starfield[i].x < 0) {starfield[i].x = display.width();starfield[i].y = random(display.height()); starfield[i].vx = random(3) + 1;

}}

}

void draw_stars() {for (uint8_t i = 0; i < MAX_STARS; i++) {

display.drawPixel(starfield[i].x, starfield[i].y, WHITE);}

}

void setup() {randomSeed(analogRead(A0));init_starfield();display.begin(SSD1306_SWITCHCAPVCC, I2C_ADDRESS_DISPLAY); display.display();delay(1000);

}

void loop() {display.clearDisplay();move_stars();draw_stars();display.display();

}

130 | Make: 5/2015

ch.0515.126-134.qxp 08.10.15 17:07 Seite 130

© Copyright by Maker Media GmbHPersönliches PDF für Günther Ehrenberger aus 4511 Allhaming

Make: 5/2015 | 131

|

Besonderen für die Erzeugung von Anima-tionen.

Hier sind spezielle Grafikprogrammedeutlich im Vorteil und für Shootduino fieldie Wahl auf Piskel (siehe Link am Endedes Artikels). Piskel läuft in jedem moder-nen Web-Browser und ist frei verfügbar. Esbietet die Standardwerkzeuge zur Erstel-lung und Bearbeitung von Pixel-Bitmapsund -Animationen. Darüber hinaus erlaubtPiskel den Export von Grafiken in die For-mate GIF und PNG.

Mit den begrenzten Ressourcen, die aufdem Arduino zur Verfügung stehen, ist esaber nicht sinnvoll beziehungsweise un-möglich, PNG- oder GIF-Dateien direkt zuverarbeiten. Die exportierten Dateien müs-sen daher noch in C/C++-Code umgewan-delt werden, der mit dem Rest des Spielsübersetzt und automatisch im Flash-RAMgespeichert wird.

… und ausgeben

Für diesen Umwandlungsprozess gibt eseine Reihe von Optionen. Ganz Hartgesot-tene können zum Beispiel die Pixel perHand in Binärzahlen umwandeln unddabei ein wenig in Erinnerungen an dieachtziger Jahre schwelgen. Fortschritt -lichere Geister setzen eher auf eine auto-matische Lösung und schreiben ein kleinesKonvertierungsprogramm oder sie setzenauf bereits bestehende Lösungen. Davonstehen sowohl online als auch offline eine

ganze Menge zur Wahl. Unter anderemgibt es Image2Code, das Bestandteil derAdafruit GFX-Bibliothek ist. Allerdingskommt es nicht mit allen Grafikdateien zu-recht.

Für Shootduino kam daher das eigens inder Programmiersprache Ruby entwickeltegif2cpp zum Einsatz, dass ebenfalls in unse-rem Download-Paket enthalten ist. Es wan-delt GIF-Dateien automatisch in C++-Codefür den Arduino um. GIF ist in diesem Falldie sinnvollste Wahl, weil Piskel es unter-stützt und weil GIF im Gegensatz zu PNGauch Animationen repräsentieren kann.

gif2cpp setzt eine Installation von Rubyund von ImageMagick voraus. Ferner wirddie Ruby-Bibliothek rmagick benötigt, diewie folgt installiert wird:

$ gem install rmagick

Gegebenenfalls ist dem Kommando nochein sudo voranzustellen.

Weil gif2cpp eine Kommandozeilen-An-wendung ist, eignet es sich auch gut zurAutomatisierung.

Die Funktionsweise von gif2cpp ist ein-fach. Die Funktion Image::read() liest alle Bil-der, die in einer Datei gespeichert sind,und liefert sie als Liste zurück. Bei einerGIF-Datei ohne Animationen enthält dieListe nur ein Element.

Die anschließende Schleife iteriert überalle Zeilen und Spalten eines jeden Bildesund gibt die Pixel als Binärzahlen aus. Eine

1 steht für ein weißes Pixel und eine 0 fürein schwarzes. Bei den zu erwartendenBildgrößen hat die binäre Ausgabe denVorteil, dass der Bildinhalt sogar im Quell-text zu erkennen ist. Beispielsweise wirdaus der Bitmap des Raumschiffes der fol-gende Code:

const unsigned char spaceship_bmp[] PROGMEM ={

B00110000, B00000000,B00111000, B11100000,B01111100, B10000000,B11111111, B10000000,B01111111, B11000000,B00111111, B11100000,B00111111, B11111000,B00111111, B11100000,B01111111, B11000000,B11111111, B10000000,B01111100, B10000000,B00111000, B11100000,B00110000, B00000000,};

In einer weiteren Ausbaustufe könntegif2cpp mehrere Dateien auf einmal verar-beiten und auch noch die Header-Datei ge-nerieren.

Kontrolle erlangen

Zentrales Element fast aller Videospiele istdie Steuerung – gerade bei Action-Spielenist sie enorm wichtig. Für die Realisierungder Steuerung gibt es einige Alternativen.Beispielsweise könnte sie komplett ausDrucktastern bestehen, denn vier Drucktas-ter reichen für Bewegungen in alle Him-melsrichtungen aus; zwei weitere sindgenug für Aktionen.

Shootduino setzt für Aktionen auf zweiDrucktaster, aber die Steuerung der Bewe-gung erfolgt über einen Analog-Joystick.Die sind mittlerweile günstig zu haben undeinfach abzufragen. Typischerweise belegtein Analog-Joystick zwei analoge Pins, näm-lich einen für die horizontale und den ande-ren für die vertikale Richtung. Die meistenModelle haben sogar noch einen eingebau-ten Taster, das heißt, sie lassen sich nachunten drücken.

Wer es ganz komfortabel mag, kann al-ternativ ein Joystick-Shield kaufen. Das Mo-dell von Sparkfun hat beispielsweise einenklickbaren Analog-Joystick und vier Druck-taster. Mit dem Display lässt es sich aller-dings nicht direkt verbinden. Dazu bedarfes wieder eines Breadboards oder einer di-rekten Kabelverbindung.

Mit dem kostenlosen Webdienst Piskel zeichnet man Mini-Grafiken Pixelfür Pixel – auch animierte GIFs.

ch.0515.126-134.qxp 08.10.15 17:07 Seite 131

© Copyright by Maker Media GmbHPersönliches PDF für Günther Ehrenberger aus 4511 Allhaming

132 | Make: 5/2015

Mehr Flexibilität und Pioniergeist ver-spricht in jedem Fall der blanke Analog-Joy-stick. Um ihn mit einem Breadboard zu ver-binden, fehlt aber noch ein Breakout-Board.Solche Boards haben die Anbieter von Ana-log-Joysticks häufig auch im Programm undsie kosten nicht viel. Allerdings kommenBoard und Joystick in der Regel separat anund müssen noch per Lötkolben verbun-den werden.

Die Software zur Abfrage des Joysticksund der Drucktaster ist nicht weiter kompli-ziert. Ein paar Präprozessor-Direktiven(#ifdef) machen sogar ein Modul möglich,das sowohl mit dem Joystick-Shield alsauch mit dem blanken Analog-Joystick zu-rechtkommt. Die Schnittstelle des Modulsdeklariert die Datei joystick.h. Sie definierterst einmal Konstanten für die Pins, mitdenen die Drucktaster verbunden sind. Istdas Präprozessor-Makro JOYSTICK_SHIELD ge-setzt, werden zwei Konstanten mehr defi-niert, weil das Joystick-Shield über zweiDrucktaster mehr verfügt.

Anschließend definiert die Datei dieStruktur JoystickState, die den Zustand desJoysticks und aller Taster jeweils mit einemBit repräsentiert. Um zum Beispiel abzufra-gen, ob der Spieler den Joystick geradenach links bewegt, muss geprüft werden,ob das Attribut left in der Struktur denWert 1 hat.

Außerdem gibt es noch zwei öffentlicheFunktionen: init_joystick() macht die Pins, diemit den Drucktastern verbunden sind, zuEingabe-Pins und aktiviert die internen Pull -up-Widerstände. Auf diese Weise „flackern“die Taster nicht, sondern liefern immer einklares Signal. Ferner bestimmt die Funktiondie Werte für die Ruheposition des Joysticksin beide Richtungen. init_joystick() wird ein-

malig in der setup-Funktion() aufgerufen.Die Funktion update_joystick() liest den ak-

tuellen Stand des Analog-Sticks und derDrucktaster. Sie muss regelmäßig von derloop()-Funktion aufgerufen werden. In derFunktion gibt es eine Besonderheit bei derErmittlung der up- und down-Werte, denndie Wertebereiche des Joystick-Shields unddes blanken Analog-Sticks verlaufen entge-gengesetzt.

Die Game-Schleife

Sobald die Hardware gebändigt ist, ist dieeigentliche Spielprogrammierung halb sowild. Im Wesentlichen geht es um die Ver-waltung diverser Zustände. Der Code ist zuumfangreich, um ihn hier im Heft abzudru-cken, deshalb werden im Folgenden nur einpaar interessante Details erläutert.

Eine zentrale Rolle spielt die StrukturGame, die in der Datei game.h definiertwird. Sie enthält unter anderem die aktu-elle Anzahl an Leben (lives) und die Anzahlder Asteroiden, die bisher verfehlt wurden(asteroids_missed).

Darüber hinaus verwaltet sie wichtigeInformationen fürs Timing. Das Attributticks enthält die Anzahl der Millisekunden,die seit dem Start des Arduino verstrichensind. bullet_fired enthält den Zeitpunkt (inMillisekunden), an dem der letzte Laser ab-gefeuert wurde. Mit diesen Informationenist es möglich, die Schussfrequenz der Laserkanone einzustellen. Analoges gilt fürasteroid_started. Hier wird vermerkt, wannzuletzt ein Asteroid auf die Reise geschicktwurde.

Für die Kontrolle der Spiellogik ist stateverantwortlich, denn hier wird der aktuelleZustand des Spiels gespeichert:

enum GameState : uint8_t {INTRO, RUNNING, PAUSED, LOST_LIVE,DONE, ENTER_HS, SHOW_HS };

Hat der Zustand den Wert INTRO, wird derTitelbildschirm des Spiels angezeigt. Die-ser Zustand kann sich auf verschiedeneArten ändern. Beispielsweise wird er inden Zustand RUNNING überführt, wenn derSpieler den rechten Drucktaster betätigt.Der Zustand ändert sich aber auch nachsieben Sekunden Inaktivität und wirddann zu SHOW_HS. In diesem Zustand zeigtShootduino die aktuelle High-Score-Listean. Die Details zu den Zustandsübergän-gen finden sich in den Funktionen, die fürdie jeweiligen Zustände verantwortlichsind und die wiederum in der loop()-Funk-tion aufgerufen werden:

void loop() {shootduino.ticks = millis();update_joystick();display.clearDisplay();switch (shootduino.state) {

case INTRO: intro(); break;case PAUSED: pause_game(); break;case RUNNING: update_game(); break;case LOST_LIVE: lost_live(); break;case SHOW_HS: show_highscores(); break;case ENTER_HS: enter_highscore(); break;case DONE: game_over(); break;

}display.display();

}

Das ist eine klassische Game-Loop, wie sieübersichtlicher nicht sein könnte. Jedemmöglichen Zustand ist exakt eine Funk -tion zugeordnet und ansonsten werdennur das Timing, die Steuerung und dasDisplay aktualisiert.

MIKROCONTROLLER-DISPLAY |

Im nächstenSekundenbruchteil wirdder von rechtsheranrasende Asteroiddas Raumschiffzerschmettern – hätteder Spieler doch seineHände nicht vomJoystick (links) undvom Feuerknopf (ganzrechts) genommen …

ch.0515.126-134.qxp 08.10.15 17:07 Seite 132

© Copyright by Maker Media GmbHPersönliches PDF für Günther Ehrenberger aus 4511 Allhaming

Make: 5/2015 | 133

|

Auch die Implementierung der einzelnenZustandsfunktionen ist übersichtlich. Bei-spielsweise zeigt pause_game() neben derNachricht „Pause“ die aktuelle Punktzahl unddie noch verbleibenden Leben an. Wenn derSpieler den linken Drucktaster drückt, wech-selt das Spiel in den Zustand RUNNING und dieWeltraumschlacht geht weiter.

Aufwendiger ist nur noch die Verwal-tung der beweglichen Objekte auf demBildschirm, also des Raumschiffs, der Aste-roiden und der Laserstrahlen. Raketenwis-senschaft ist das aber auch nicht und wirdin den Dateien game_objects.h undgame_objects.cpp realisiert.

Jedes bewegliche Objekt (mit Ausnahmeder Sterne) wird mit der Struktur GameObjectrepräsentiert. Neben den Koordinaten undder aktuellen Geschwindigkeit in x- und y-Richtung enthält die Struktur auch noch In-formationen über den Objekttypen und dieaktuelle Animationsphase. Damit ist es ein-fach möglich, den Typen und die Darstel-lung eines Objekts zu ändern. Zum Beispielkann aus einem Asteroiden eine Explosionwerden, wenn er von einem Laserstrahl ge-troffen wurde.

Die gesamte Verwaltung aller Spiel -objekte wird von den drei Funktioneninit_objects(), draw_objects() und move_objects()übernommen. Sie arbeiten alle nach dem-selben Prinzip: Sie iterieren über eine Listevon Objekten und behandeln dann alle Ob-jekte, die aktiv sind, also deren is_active-Flagden Wert true hat. Diese Vorgehensweisevermeidet die dynamische Verwaltung vonSpeicher, denn wenn ein neues Objekt be-nötigt wird, ersetzt es einfach ein inaktives.

Nichts ist für die Ewigkeit?

Die bisher beschriebenen Funktionen sindabsolut notwendig, aber kein Arcade-Shoo-ter wäre komplett ohne eine Liste der High-Scores. Stilecht bietet auch Shootduino denbesten Weltraumpiloten die Möglichkeit,sich mit dreistelligen Initialen und ihrerPunktzahl zu verewigen. Dabei ist „verewi-gen“ durchaus ernst gemeint, denn die High-Scores werden im EEPROM gespeichert undüberdauern ein Abschalten des Arduino.

Der Arduino Uno bietet 2 KByte EEPROMund die Arduino-Umgebung enthält eineStandardbibliothek, um Daten im EEPROMzu manipulieren. Der EEPROM hat keinerleivorgegebene Struktur und ist für ein Ardu ino-Programm nichts weiter als eineListe von 2048  Bytes. Diese Bytes könnenüber einen Index von 0 bis 2047 adressiertwerden.

Zur Speicherung von High-Scores sinddas ausgezeichnete Voraussetzungen, dennpro Eintrag sind drei Buchstaben (dreiBytes) und eine Punktzahl in Form eines In-teger-Werts ohne Vorzeichen (zwei Bytes)zu speichern. Shootduinos High-Score-Listeenthält drei Einträge und belegt somit ma-gere 3ˇxˇ(3ˇ+ˇ2)ˇ=ˇ15 Bytes im EEPROM.

Eine Sache ist allerdings noch zu beach-ten: Wenn Shootduino zum ersten Mal aufeinem Arduino installiert wird, muss dieListe der High-Scores initialisiert werden.Bei dieser Initialisierung werden alle Initia-len auf AAA und alle Punktzahlen auf 0 ge-setzt. Diese Initialisierung darf aber nichtbei jedem Neustart des Arduino erfolgen,denn dann würde die Liste der High-Scores

jedes Mal überschrieben. Shootduino spei-chert daher im ersten Byte des EEPROM,also an der Adresse 0, den Wert 42, nach-dem die Liste initialisiert wurde. Beim Startprüft das Programm den Wert an Adresse 0und unterlässt die Initialisierung, wenn esdort eine 42 findet.

Die Dateien highscores.h und high -scores.cpp enthalten alle Strukturen, Varia-blen und Funktionen, die Shootduino zurVerwaltung der High-Scores benötigt. DieStruktur HighScoreEntry repräsentiert eineneinzelnen Eintrag in der High-Score-Tabelle.

EEPROM im Zugriff

Die EEPROM-Bibliothek des Arduino bietetzwei unterschiedliche Zugriffsmöglich -keiten. Die Funktionen EEPROM.read() undEEPROM.write() operieren auf Byte-Ebene.Mit ihnen können Bytes an beliebigenAdressen gelesen und geschrieben wer-den. Komfortabler sind die Funktionen EE-PROM.get() und EEPROM.put(), denn mit ihnenlassen sich beliebige Datentypen- undStrukturen lesen und schreiben. Beispiels-weise kommen sie in den Funktionenget_entry() und set_entry() zum Einsatz, umHighScoreEntry-Objekte zu lesen und zuschreiben.

Das reduziert den Aufwand zur Verwal-tung des EEPROM erheblich und so dienennur die ersten knapp 100  Zeilen in high -scores.cpp der Manipulation des EEPROM.Den Löwenanteil machen dabei die Initiali-sierung init_highscores() und die Funktion insert_entry() zum Einfügen eines neuenHigh-Scores aus.

Alle weiteren Funktionen kümmern sichum die Ein- und Ausgabe der High-Scoreswährend des Spiels. Bei der Eingabe der Ini-tialen kann der Spieler den Joystick nachoben beziehungsweise unten bewegen, umzum nächsten beziehungsweise zum vor-hergehenden Zeichen zu wechseln. DerWechsel zur nächsten Stelle in den Initialenerfolgt über den linken Drucktaster. Abge-schlossen wird die Eingabe mit dem rech-ten Drucktaster.

Die Funktionen zur Eingabe der Initialenenthalten keine großen Besonderheiten. Lediglich die Konstante INITIALS_LETTERS isterwähnenswert. Sie liegt im Flash-RAM undenthält alle Buchstaben, die in den Initialenerlaubt sind. Statt die aktuellen Buchstabenzu speichern, speichern die Eingabe-Funk-tionen jeweils den Index, den der Buchsta-be in INITIALS_LETTERS hat.

Der Titel-Screen des Beispielspielsnamens Shootduino

Drückt man den linken Taster, schaltetman das Spiel in den Pausenmodus.

ch.0515.126-134.qxp 08.10.15 17:07 Seite 133

© Copyright by Maker Media GmbHPersönliches PDF für Günther Ehrenberger aus 4511 Allhaming

134 | Make: 5/2015

MIKROCONTROLLER-DISPLAY | |

Auslastung

Der Quelltext von Shootduino besteht aussechzehn Dateien mit insgesamt wenigerals 1000 Zeilen Code (darin sind die gene-rierten Zeilen für die Bitmap-Grafiken ent-halten). Das ist für eine Arduino-Anwen-dung nicht gerade wenig, aber immer nochsehr überschaubar. Der tatsächliche Spei-cherbedarf sieht wie folgt aus:–ˇ17ˇ888 Bytes Flash-RAM (55 %) –ˇ1541 Bytes SRAM (75 %)–ˇ16 Bytes EEPROM (2 %)Die Bilanz kann sich durchaus sehen lassenund der verbleibende Speicher lässt nochjede Menge Spielraum für Erweiterungen.Allerdings gibt es auch keinen Anlass fürÜbermut, denn die verbleibenden 507Bytes im SRAM können auch schnell aufge-zehrt werden. Das muss nicht einmal dasProdukt als solches beeinflussen, sondernkann schon während der Entwicklung einProblem werden.

Beispielsweise sind Ausgaben auf der se-riellen Schnittstelle beim Aufspüren vonProgrammfehlern sehr hilfreich. Die Serial-Bibliothek benötigt aber 173  Bytes SRAMund kann ab einem bestimmten Punktnicht mehr verwendet werden.

Aus diesem Grund ist es wichtig, stetsein Auge auf den Speicherbedarf neuer Ob-jekte und Bibliotheken zu haben. Insbeson-dere sollten Datentypen nie unnötig großgewählt werden. Auch kann übermäßigeGeneralisierung den Speicher schnellschrumpfen lassen. Aus diesem Grund gibt

es zum Beispiel die globale Variableplayer_hit und kein Attribut was_hit in derStruktur GameObject.

Mit einfachen Tricks lässt sich ein wenigPlatz sparen, ohne dass der Quelltext da-durch unübersichtlicher würde. Häufiglohnt sich der Einsatz von Techniken, diezwar nicht unbedingt jedem geläufig sind,aber zum offiziellen Sprachumfang vonC/C++ gehören.

Zum Beispiel macht die Struktur Joystick-State von Bit-Feldern Gebrauch:

struct JoystickState {bool left : 1;bool right : 1;bool up : 1;bool down : 1;bool joy_button : 1;bool left_button : 1;bool right_button : 1;

};

Dadurch belegt jedes Attribut nur ein Bitim Speicher, sodass die ganze Struktur miteinem Byte auskommt. Leider klappt die-ser Trick nicht immer. In der Struktur Star,die zur Implementierung des Sternen-Felds dient, käme das Attribut vx mit nurzwei Bits aus. Weil der Compiler aus Grün-den der Effizienz aber automatisch Füll-Bytes (Padding) in die Struktur integriert,bringt es nichts, die Struktur wie folgt zudefinieren:

struct Star { int8_t x, y, vx : 2; };

Im Gegenteil: Der Bedarf an SRAM bleibt indiesem Fall gleich und das Programm ver-braucht sogar etwas mehr an Flash-RAM, weilder Compiler zum Zugriff auf das Attribut vxweniger effizienten Code generieren muss.

Ein weiterer Trick betrifft Enumerationen.Moderne C++-Compiler erlauben nämlichenum-Deklarationen mit Typ-Deklarationen:

enum GameObjectType : uint8_t

Diese Deklaration legt fest, dass eine Aus-prägung der Enumeration GameObjectTypevom Typ uint8_t sein soll, also nur ein Bytebenötigt. Per Voreinstellung wären essonst zwei.

Wie geht es weiter?

Shootduino ist nicht nur spielbar, sondernmacht durchaus Spaß. Trotzdem kann esnoch ein paar Erweiterungen und Verbes-serungen vertragen. Auf der Software-Seiteließe sich beispielsweise ein Power-Up-Sys-tem einfügen und der Schwierigkeitsgradvariieren. Auch gäbe ein Demo-Modusdem Spiel einen professionelleren Touch.

Viele Teile des Shootduino-Codes eig-nen sich auch für andere Spiele. Der einzigeAspekt, der bisher nicht berührt wird, istScrolling, aber das wird vom Display sogarper Hardware unterstützt und sollte daherkeine Probleme bereiten.

Auf der Hardware-Seite sind der Fantasiekaum Grenzen gesetzt. Ganz sicher würdeeine Audio-Ausgabe die kleine Konsoleenorm aufwerten. In Form eines eigenenShields ließe sich das Ganze sogar in eineportable Konsole mit SD-Kartenslot, Akkuund Gehäuse verwandeln. Selbstverständ-lich könnte das Display auch durch einFarb-Display ersetzt werden.

Der Arduino ist kein Kraftpaket, aber fürein paar flotte Spielchen hat er genug MIPSunter der Haube. Besonders attraktiv wird erals Spieleplattform durch die günstigen undguten Displays, die mittlerweile überall zuhaben sind und sich dank guter Bibliothekenkinderleicht einbinden lassen.

Die Arduino-Umgebung selbst ist viel-leicht nicht die komfortabelste der Welt,aber sie hat sich über die Jahre gemausertund wurde insbesondere in den letztenMonaten ständig verbessert. Gerade dieUnterstützung moderner C/C++-Featuresvereinfacht die Programmierung enorm.

Bei aller Euphorie übers Zocken sollteaber nicht vergessen werden, dass sich dieDisplays auch für seriöse Zwecke eignenund auch beim Debugging eine gute Al-ternative zum seriellen Monitor sind. Ideenfür weitere Einsatzzwecke finden Sie on-line – einfach den Link unten in denBrowser tippen. —pek

EIGENE SPIELEWenn Sie selbst eine Spielidee

umgesetzt oder einen Klassiker auf

unsere Arduino-Konsole portiert haben,

freuen wir uns über eine Mail mit dem

Code an [email protected]!

Links und Forenmake-magazin.de/x4ef

Die benötigten Adafruit-Bibliotheken werden offiziell von der Arduino-Bibliothek unterstützt.

ch.0515.126-134.qxp 08.10.15 17:07 Seite 134

© Copyright by Maker Media GmbHPersönliches PDF für Günther Ehrenberger aus 4511 Allhaming