Das Spiel Pong gehört zu den bekanntesten und ältesten Computer- und Videospielen. Seine Karriere begann 1972 als eine
Entwicklung der Firma Atari. Das Spielprinzip ähnelt dem des Tischtennis (Ping Pong). Normalerweise ist Pong ein Spiel für zwei
Personen. Daran wollen wir uns jedoch nicht stören und basteln uns ein Spiel für nur eine Person. Die Regeln sind einfach.
Auf einem Spielfeld bewegt sich hin und her ein Ball. Auf der rechten Seite des Spielfeldes befindet sich ein Ballschläger, der
von dem Spieler rauf und runter bewegt werden kann. Die Aufgabe des Spielers ist es, den Ballschläger so zu steuern, dass der
Ball, sobald er den rechten Rand des Spielfeldes erreicht, von dem Ballschläger getroffen wird. Gelingt es nicht, hat der Spieler
die Runde verloren.
Als Spielfeld kommt eine RGB-Matrix mit 64 RGB-Leuchtdioden zum Einsatz. Eine blau leuchtende LED symbolisiert den Ball.
Zwei grüne Leuchtdioden auf dem rechten Rand der Matrix übernehmen die Aufgabe des Ballschlägers. Die Steuerung des Ballschlägers
erfolgt mithilfe eines Joysticks. Ein Mikrocontroller, hier Arduino Uno, koordiniert das Geschehen auf dem Spielfeld.
In dieser Grundausführung verzichten wir auf weitere Erweiterungen, die bei solchen Spielen üblich sind. Man kann die
folgende Lösung noch kräftig ausbauen und optimieren. Eine Anreicherung des Spiels mit z.B. akustischen Geräuschen bzw. Signalen
und einem Spielstandzähler wäre durchaus empfehlenswert.
RGB-Matrix
Eine 8x8 RGB-Matrix fungiert in der Schaltung als das Spielfeld.
Bei dem Joystick-Modul handelt es sich um ein kleines Modul, das für verschiedene Entwicklungs- und
Testschaltungen eingesetzt werden kann. Das Modul arbeitet mit einer Spannung von 5V und eignet sich sehr gut für Experimente
mit Arduino. Das Modul hat fünf Anschlüsse. Zwei Anschlüsse werden für die Versorgungsspannung benötigt. Zwei weitere
Anschlüsse werden für die Achse X und Y verwendet und liefen jeweils ein analoges Signal im Bereich von 0V bis 5V. Der letzte
Pin ist der Ausgang eines Tasters, der in das Modul integriert ist. Der Taster wird durch einen senkrechten Druck auf den
Joystick aktiviert und liefert ein digitales Signal. In diesem Moment wird der Pin mit Masse verbunden. Der Mikrocontroller
Eingang wird deswegen als Pullup-Eingang definiert. Alle Signale können problemlos direkt von Arduino erfasst und
ausgewertet werden. Einbindung einer Bibliothek ist nicht erforderlich.
Pinbelegung:
GND – Masse
+5V –Spannungsversorgung +5V
VRx – Anschluss für die X-Achse
VRy – Anschluss für die Y-Achse
SW – Switch – Anschluss. Pin für den intergierten Taster.
Die Abmessungen des Moduls betragen (BxLxH) 26 x 34 x 36 mm. Es kann bei vielen Anbietern erworben werden.
Der Preis liegt im Bereich 2-3 Euro.
Arduino Uno
Der Mikrocontroller Arduino Uno bringt die Farbe und Bewegung auf das Spielfeld.
Spannungsregler. Die Matrix besteht aus 8x8x3 = 192 Leuchtdioden. Deswegen empfiehlt es sich, für sie eine separate
Spannungsversorgung vorzusehen. Je nachdem, wie viele Leuchtdioden gleichzeitig angesteuert werden, kann die Matrix für den +5V
Ausgang des Arduino zu große Belastung darstellen.
Um eventuellen Störungen vorzubeugen, kann der Anschluss der Matrix um einen Kondensator und Widerstand erweitert werden.
Ein Beispiel für den Anschluss mit diesen Komponenten findet man hier:
// *****************************************************************************************
// Pong-Spiel mit analogem Joystick
// Arduino UNO, RGB-Matrix 8x8
// IDE 1.8.16
// *****************************************************************************************
#include<Adafruit_NeoPixel.h>#define LED_PIN 13
#define LED_COUNT 64
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
#define Joystick_Start 2 // Joystick digital
bool Verloren;
int PosX, PosY; // aktuelle Ball-Position
int Pixel_Anzeige [] = { -1, -1, -1, -1 }; // Ball mit Kometenschweifint Bewegungsrichtung; // Aktuelle Bewegungsrichtung des Balles
int PosXY [6][2] = { // Additionswerte für X und Y bei bestimmter Richtung
{ 1, 1 },
{ 1, 0 },
{ 1, -1 },
{-1, 1 },
{-1, 0 },
{-1, -1 } };
// Mögliche Bewegungsrichtungen:
// 0, LRS = Bewegung von Links nach Rechts, Steigend)
// 1, LRW = Bewegung von Links nach Rechts, Waagerecht)
// 2, LRF = Bewegung von Links nach Rechts, Fallend)
// 3, RLS = Bewegung von Rechts nach Links, Steigend)
// 4, RLW = Bewegung von Rechts nach Links, Waagerecht)
// 5, RLF = Bewegung von Rechts nach Links, Fallend)
// Statustabelle für Bestimmung der Folgerichtung
// beim Erreichen des Randes
int Status [18][5] = { // { Status, Max für Random, Richtung 1, Richtung 2, Richtung 3)
// 9 - ohne Bedeutung
{ 114, 2, 0, 1, 9 }, // PosX=1, PosY=1, RLW
{ 115, 2, 0, 1, 9 }, // PosX=1, PosY=1, RLF
{ 183, 2, 1, 2, 9 }, // PosX=1, PosY=8, RLS
{ 184, 2, 1, 2, 9 }, // PosX=1, PosY=8, RLW
{ 193, 2, 0, 1, 9 }, // PosX=1, PosY=2..7, RLS
{ 194, 3, 0, 1, 2 }, // PosX=1, PosY=2..7, RLW
{ 195, 2, 1, 2, 9 }, // PosX=1, PosY=2..7, RLF
{ 711, 2, 3, 4, 9 }, // PosX=7, PosY=1, LRW
{ 712, 2, 3, 4, 9 }, // PosX=7, PosY=1, LRF
{ 780, 2, 4, 5, 9 }, // PosX=7, PosY=8, LRS
{ 781, 2, 4, 5, 9 }, // PosX=7, PosY=8, LRW
{ 790, 2, 3, 4, 9 }, // PosX=7, PosY=2..7, LRS
{ 791, 3, 3, 4, 5 }, // PosX=7, PosY=2..7, LRW
{ 792, 2, 4, 5, 9 }, // PosX=7, PosY=2..7, LRF
{ 912, 2, 0, 1, 9 }, // PosX=2..6, PosY=1, LRF
{ 915, 2, 3, 4, 9 }, // PosX=2..6, PosY=1, RLF
{ 980, 2, 1, 2, 9 }, // PosX=2..6, PosY=8, LRS
{ 983, 2, 4, 5, 9 }, // PosX=2..6, PosY=8, RLS
};
int Schlaeger [8][2] = { // Mögliche Schläger-Positionen + Feldbewertung
{ 63, 0 },
{ 55, 0 },
{ 47, 0 },
{ 39, 0 },
{ 31, 0 },
{ 23, 0 },
{ 15, 0 },
{ 7, 0 } };
int Verloren_Kreuz_Pixel [] = { 18, 21, 27, 28, 35, 36, 42, 45 }; // Rotes Kreuz für Verloren
void setup() { // SetUp
strip.begin();
strip.show();
strip.setBrightness(30);
pinMode(Joystick_Start, INPUT_PULLUP);
randomSeed(analogRead(A2)); // Startzahl für Random-Funktion
}
void loop() { // Hauptprogramm
if (!digitalRead(Joystick_Start)) {
Verloren_Kreuz (0); // Rotes Kreuz löschen
Verloren = false;
PosX = 2; // Start-Position via Zufall
PosY = random (2,8);
Bewegungsrichtung = random (0,3); // Start Richtung via Zufall
while (!Verloren) {
// Tennisschläger
for (int Schlaeger_Zeit = 0; Schlaeger_Zeit < 50; Schlaeger_Zeit++) {
int Pos_Schlaeger = 1023-(analogRead(0)); // Joystick abfragen
int PosY_Schlaeger = Pos_Schlaeger / 170;
for (int i=0; i<8; i++) { // Schläger-Felder löschen
strip.setPixelColor(Schlaeger [i][0], 0, 0, 0);
Schlaeger [i][1] = 0; // Feldbewertung löschen
}
Schlaeger [PosY_Schlaeger][1] = 1; // Neue Feldbewertung
Schlaeger [PosY_Schlaeger+1][1] = 1;
strip.setPixelColor(Schlaeger [PosY_Schlaeger][0], 0, 255, 0);
strip.setPixelColor(Schlaeger [PosY_Schlaeger+1][0], 0, 255, 0);
strip.show();
delay (3);
}
// Start der Prüfung der Randposition
int X = PosX; // Statusberechnung für aktuelle Position
if (PosX > 1 and PosX < 7) { X = 9; }
int Y = PosY;
if (PosY > 1 and PosY < 8) { Y = 9; }
int StatusXY = (X * 100) + (Y * 10) + Bewegungsrichtung;
int Richtungswechsel = -1; // -1 für keinen Richtungswechsel
for (int i=0; i<18; i++) { // Durchsuchung der Statustabelle
if (Status [i][0] == StatusXY) {
Richtungswechsel = i; } // Richtungswechsel erforderlich
}
if (Richtungswechsel > -1) { // Neue Richtung via Zufall bestimmen
int Richtung_Zufall = random (0,Status [Richtungswechsel][1]);
Bewegungsrichtung = Status [Richtungswechsel][Richtung_Zufall + 2];
}
PosX = PosX + PosXY [Bewegungsrichtung][0]; // Berechnung der neuen Koordinaten
PosY = PosY + PosXY [Bewegungsrichtung][1];
int Pixel_Nr = (9-PosY)*8 - (8-PosX)-1; // Pixel Nummer auf der Matrix
Ball_Anzeige (Pixel_Nr); // Ball bewegen
if ((PosX == 7) and (Schlaeger [PosY - 1][1] != 1)) { // Spiel verloren
Verloren = true;
Verloren_Kreuz (255); // Rotes Kreuz anzeigen
}
}
}
}
void Ball_Anzeige (int Pixel_Nummer) {
for (int i=3; i>0; i--) {
Pixel_Anzeige [i] = Pixel_Anzeige [i-1];
}
Pixel_Anzeige [0] = Pixel_Nummer;
strip.setPixelColor(Pixel_Anzeige [3], 0, 0, 0);
strip.setPixelColor(Pixel_Anzeige [2], strip.gamma32(strip.ColorHSV(43000, 255, 100)));
strip.setPixelColor(Pixel_Anzeige [1], strip.gamma32(strip.ColorHSV(43000, 255, 200)));
strip.setPixelColor(Pixel_Anzeige [0], 0, 0, 255);
strip.show();
}
void Verloren_Kreuz (int Wert) {
for (int i=0; i<8; i++) {
strip.setPixelColor(Verloren_Kreuz_Pixel [i], Wert, 0, 0);
}
strip.show();
}
// *****************************************************************************************
Das Spielfeld betrachten wir als ein Koordinatensystem mit X und Y Achse. Die Position des Balles in der unteren
linken Ecke hat die Koordinaten PosX=1 und PosY=1. Folglich hat die obere rechte Ecke die Koordinaten PosX=8 und PosY=8.
Das Spiel beginnt, sobald der Schalter am Joystick betätigt wird. Hierzu wird der digitale Eingang 2 des Arduino abgefragt.
Nach dem Start geht das Programm in eine while-Schleife, die erst dann verlassen wird, wenn der Spieler den Ball nicht trifft.
Über die Fortsetzung des Spiels entscheidet die Variable „Verloren“, die während des Spiels auf false steht. Die Untersuchung
findet immer dann statt, wenn der Ball in der siebten Spalte steht (PosX=7). Hier wird geprüft, ob das Feld in der achten Spalte
mit der gleichen Y-Position bereits belegt ist. Wenn der Spieler es nicht geschafft hat, den Schläger rechtzeitig auf die
richtige Position zu bringen, geht „Verloren“ auf true und das Spiel wird beendet.
Die Bewegung des Schlägers ist in einer For-Schleife gefangen. Erst nach Verlassen dieser Schleife erfolgt die Änderung der
Position des Balles. Nach jedem Durchlauf der Schleife wird das Programm mit delay(3) kurz angehalten. Auf diese Weise kann man
die Geschwindigkeit des Balles steuern. An dieser Stelle könnte man einen weiteren Schalter in die Schaltung integrieren, um mehrere
Levels des Spieles zu kreieren. Zwischen den Wartezeiten wird die Position des Joysticks untersucht und der Schläger entsprechend
positioniert. Das geschieht, indem der analoge Ausgang des Joysticks VRx ausgelesen wird (Arduino analoger Eingang A0). In dem
Beispiel wird nur ein Kanal des Joysticks verwendet. Positioniert man den Joystick anders, kann es notwendig sein, den anderen
Kanal (VRy) abzufragen.
Die Bewegung des Balles koordiniert die Tabelle (zweidimensionales Array) „Status“. An der ersten Stelle jeder Zahlenfolge
steht hier eine Zahl, die aktuelle PosX, PosY und Bewegungsrichtung zusammenfasst. Diese Zahlen beziehen sich nur auf mögliche
Randpositionen des Balles. Bevor der Ball bewegt wird, wird zuerst sein Status (Variable StatusXY) berechnet und mit der Tabelle
verglichen. Die Grundformel hier lautet:
StatusXY = PosX * 100 + PosY * 10 + Bewegungsrichtung.
Die möglichen Bewegungsrichtungen sind festgelegt und nummeriert. Eine waagerechte Bewegung von rechts nach links hat z.B.
die Nummer 4. Die vollständige Nummerierung steht in Kommentaren des Programms.
In der linken oberen Ecke des Feldes hat demnach der Ball, der bis dato waagerecht von rechts nach links bewegt wurde,
einen StatusXY = 1*100 + 8*10 + 4 = 184.
Für die Positionen 1 < PosY < 8 am linken Rand steht pauschal die Zahl 9. Befindet sich der Ball nach
einer waagerechten Bewegung von rechts nach links an der Position PosX=1, PosY=3, so hat sein Status den Wert 194. Ähnlich wird
auch bei anderen Grenzlagen verfahren.
Diese Werte signalisieren dem Programm, dass ein Richtungswechsel notwendig ist. Die möglichen neuen Richtungen stehen
ebenfalls in der Variable „Status“ und zwar auf den Plätzen 3, 4 und 5. Stimmt also der aktuelle Status des Balles mit einem der
Werte in der Tabelle überein, wird via Zufall eine neue Richtung bestimmt. An der zweiten Stelle in der Tabelle steht der
maximale Wert für die Funktion random(). Je nach Position und bisheriger Bewegungsrichtung sind 2 oder 3 Folgerichtungen
möglich.