Vor Kurzem hatte ich mir einen Ring aus 16 RGB-LEDs des Typs WS2812
bestellt (auch Neopixel genannt) und wollte damit ein wenig herumexperimentieren. Mir kam eine Idee einer
quasi 1-dimensionalen Adaption des Spiele-Klassikers "Pong".
So erfand ich kurzerhand ein paar neue Regeln, baute ein Prototyp auf und programmierte das Spiel in
kurzer Zeit. Und da das Spiel auf einem Ring angezeigt wird, nannte ich es einfach "Ring-Pong". Später
stellte ich fest, dass jemand auf YouTube schon ein ähnliches Projekt mit demselben Namen umgesetzt hatte,
aber meine Regeln weichen etwas von diesem Konzept ab.
Das Spielfeld ist ringförmig aufgebaut, d.h. das "Aus" (rot) des Spielfeldes befindet sich am unteren Ende
des Ringes (auf 6-Uhr-Stellung). Das Netz (gelb) befindet sich am oberen Ende des Ringes (12-Uhr-Stellung).
Die Spieler (blau) stehen zu Beginn jeweils links und rechts auf dem Ring.
Bei Spielbeginn hat einer der beiden Spieler Aufschlag (wird zufällig ermittelt) und der Ball (weiß) wird
langsam auf die andere Seite über das Netz zum Gegner geschlagen. Diese muss versuchen den Ball rechtzeitig
zu treffen, um ihn seinerseits zurückzuschlagen. Trifft der jeweilige Spieler den Ball genau 1 LED vor sich,
so wird der Ball einfach zurückgeschlagen. Trifft er ihn, wenn er auf demselben Feld wie der Spieler selbst
befindet, so muss der Spieler einen Schritt zurückgehen. Trifft er ihr zu früh - d.h. zwischen Gegner und
sich selbst, so rückt der Spieler näher dem Netz zu. Verpasst der Spieler den Ball ganz, so geht dieser
ins "Aus" und der Gegner gewinnt das Spiel.
Nach jedem erfolgreichen Schlag wird die Spielgeschwindigkeit ein wenig höher. Läuft ein Spieler ins Netz
oder ins "Aus", so gewinnt der Gegner.
Viel Spaß beim Nachbauen und Spielen!
Der Aufbau selbst ist relativ einfach: Es wird der LED-Ring richtig mit 5V und GND verkabelt und die DIN (data in)-Leitung bekommt noch einen Vorwiderstand. Der Kondensator ist als Sicherheit vor den LED-Ring geschaltet, um Stromspitzen abzufangen. Die beiden Mikrotaster werden mit je einem digitalen Eingang des Arduino verdrahtet und bekommt noch jeweils einen pulldown-Widerstand zur Entprellung.
#include <Adafruit_NeoPixel.h>
#define PIN_LED_RING 8
#define PIN_BUTTON_1 10
#define PIN_BUTTON_2 11
#define NUMPIXELS 16
#define GAME_INIT 0
#define GAME_RUNNING 1
#define GAME_ENDED 2
#define PLAYER_1 0
#define PLAYER_2 1
#define PLAYER_NONE 255
// LED positions of game objects
#define POS_NET 8
#define POS_OUT 0
#define POS_PLAYER1_START 4
#define POS_PLAYER2_START 12
// game speedup after player change in milliseconds
#define GAME_DELAY_SPEEDUP 100
// game delay in milliseconds
#define GAME_DELAY_INIT 1000
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN_LED_RING, NEO_GRB + NEO_KHZ800);
byte ledMapping[NUMPIXELS] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
byte playerPos[2], ballPos, gameState = GAME_ENDED, currentPlayer, wonPlayer, gameDelaySpeedup;
bool button1Pressed = false, button2Pressed = false;
byte button1State, button2State;
unsigned long previousMillis, currentMillis;
unsigned int gameDelay = GAME_DELAY_INIT;
void setup()
{
pixels.begin();
randomSeed(analogRead(0));
for(byte i=0; i<NUMPIXELS; i+=2) {
pixels.setPixelColor(i, pixels.Color(10, 10, 0));
pixels.show();
}
delay(400);
for(byte i=1; i<NUMPIXELS; i+=2) {
pixels.setPixelColor(i, pixels.Color(10, 10, 10));
pixels.show();
}
delay(400);
for(byte i=0; i<NUMPIXELS; i+=2) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
pixels.show();
}
delay(400);
for(byte i=1; i<NUMPIXELS; i+=2) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
pixels.show();
}
}
void loop()
{
button1State = digitalRead(PIN_BUTTON_1);
if (button1State == LOW) {
button1Pressed = false;
}
if (button1State == HIGH && !button1Pressed) {
button1Pressed = true;
buttonPressedPlayer1();
}
button2State = digitalRead(PIN_BUTTON_2);
if (button2State == LOW) {
button2Pressed = false;
}
if (button2State == HIGH && !button2Pressed) {
buttonPressedPlayer2();
}
if(gameState == GAME_RUNNING) {
drawObjects();
updateBall();
}
}
void initGame()
{
gameState = GAME_INIT;
gameDelay = GAME_DELAY_INIT;
if (random(0, 100) > 50) {
currentPlayer = PLAYER_2;
ballPos = POS_PLAYER1_START+1;
} else {
currentPlayer = PLAYER_1;
ballPos = POS_PLAYER2_START-1;
}
playerPos[PLAYER_1] = POS_PLAYER1_START;
playerPos[PLAYER_2] = POS_PLAYER2_START;
wonPlayer = PLAYER_NONE;
gameState = GAME_RUNNING;
gameDelaySpeedup = GAME_DELAY_SPEEDUP;
currentMillis = millis();
previousMillis = currentMillis;
for(byte i=0; i<NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(10, 10, 10));
delay(50);
pixels.show();
}
for(byte i=0; i<NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
delay(50);
pixels.show();
}
}
void updateBall()
{
currentMillis = millis();
if (currentMillis - previousMillis >= gameDelay) {
previousMillis = currentMillis;
if (currentPlayer == PLAYER_1) {
ballPos--;
} else {
ballPos++;
}
if (ballPos > NUMPIXELS-1) {
ballPos = 0;
}
checkGameState();
}
}
void checkGameState()
{
if (currentPlayer == PLAYER_1 && ballPos == POS_OUT
|| playerPos[PLAYER_1] == POS_NET
|| playerPos[PLAYER_1] == POS_OUT) {
wonPlayer = PLAYER_2;
} else if (currentPlayer == PLAYER_2 && ballPos == POS_OUT
|| playerPos[PLAYER_2] == POS_NET
|| playerPos[PLAYER_2] == POS_OUT) {
wonPlayer = PLAYER_1;
}
if (wonPlayer != PLAYER_NONE) {
gameState = GAME_ENDED;
drawEndScreen(wonPlayer);
}
}
void buttonPressedPlayer1()
{
if (gameState == GAME_RUNNING) {
if (currentPlayer == PLAYER_1) {
if (ballPos == playerPos[PLAYER_1]) {
playerPos[PLAYER_1]--;
} else if (ballPos > playerPos[PLAYER_1]+1) {
playerPos[PLAYER_1] = ballPos-1;
}
playerPos[PLAYER_1] = max(0, min(POS_NET, playerPos[PLAYER_1]));
if (ballPos >= playerPos[PLAYER_1]) {
currentPlayer = PLAYER_2;
speedUpGame();
}
}
} else if (gameState == GAME_ENDED) {
initGame();
}
}
void buttonPressedPlayer2()
{
if (gameState == GAME_RUNNING) {
if (currentPlayer == PLAYER_2) {
if (ballPos == playerPos[PLAYER_2]) {
playerPos[PLAYER_2]++;
} else if (ballPos < playerPos[PLAYER_2]-1) {
playerPos[PLAYER_2] = ballPos+1;
}
playerPos[PLAYER_2] = max(POS_NET, playerPos[PLAYER_2]);
if (playerPos[PLAYER_2] > NUMPIXELS-1) {
playerPos[PLAYER_2] = 0;
}
if (playerPos[PLAYER_2] == 0 || ballPos <= playerPos[PLAYER_2]) {
currentPlayer = PLAYER_1;
speedUpGame();
}
}
} else if (gameState == GAME_ENDED) {
initGame();
}
}
void speedUpGame()
{
byte diff = (byte)gameDelaySpeedup * 0.1;
if (gameDelaySpeedup-diff > 0) {
gameDelaySpeedup -= diff;
}
gameDelay -= gameDelaySpeedup;
gameDelay = max(10, gameDelay);
}
void drawObjects()
{
for(byte i=0; i<NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
}
// Net (top center)
pixels.setPixelColor(POS_NET, pixels.Color(10, 10, 0));
// Out (bottom center)
pixels.setPixelColor(POS_OUT, pixels.Color(10, 0, 0));
// Ball
pixels.setPixelColor(ballPos, pixels.Color(20, 20, 10));
// playerPositions
pixels.setPixelColor(playerPos[PLAYER_1], pixels.Color(0, 0, 20));
pixels.setPixelColor(playerPos[PLAYER_2], pixels.Color(0, 0, 20));
pixels.show();
}
void drawEndScreen(byte wonPlayer)
{
for(byte i=0; i<NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(2, 0, 0));
if (wonPlayer == PLAYER_1 && i<=POS_NET
|| wonPlayer == PLAYER_2 && i>POS_NET) {
pixels.setPixelColor(i, pixels.Color(0, 50, 0));
}
}
pixels.setPixelColor(POS_NET, pixels.Color(0, 0, 0));
pixels.setPixelColor(POS_OUT, pixels.Color(0, 0, 0));
pixels.show();
delay(3000);
}
Das Spiel kann durch einen WS2812B-LED-Streifen auch zu String-Pong, ein Pong-Spiel, welches auf eine langen Bahn gespielt wird. Im Folgenden habe ich einen Streifen mit 60 Einzel-LEDs verwendet, aber es kann eigentliche jede beliebige Anzahl genommen werden. Damit der LED-Streifen an ein Breadboard angeschlossen werden kann, zeigt das folgende Bild einen improvisierten Adapter mit einem Stück Stiftleiste 2,54mm (männlich, gewinkelt)
Statt den im obigen Aufbau verwendeten Mikrotastern habe ich zwei simple Gamecontroller gebaut, bestehend aus jeweils einem Druckschalter und einem selbst gedruckten Gehäuse. Der Stecker ist ebenfall ein Stückstiftleiste mit etwas Heißkleber ummantelt:
Einige kleine Modifikationen im oben gezeigten Sketch
Folgende Werte müssen entsprechend des verwendeten LED-Streifens angepasst werden:
NUMPIXELS
: Anzahl der Einzel-LEDs
POS_NET
: Mitte des LED-Streifens
POS_OUT2
: Aus-Punkt des zweiten Spielers (sollte der höchste Index der LEDs sein)
POS_PLAYER1_START
: Startposition des ersten Spielers
POS_PLAYER2_START
: Startposition des zweiten Spielers
#include <Adafruit_NeoPixel.h>
#define PIN_LED_RING 8
#define PIN_BUTTON_1 10
#define PIN_BUTTON_2 11
#define NUMPIXELS 60
#define GAME_INIT 0
#define GAME_RUNNING 1
#define GAME_ENDED 2
#define PLAYER_1 0
#define PLAYER_2 1
#define PLAYER_NONE 255
// LED positions of game objects
#define POS_NET 30
#define POS_OUT1 0
#define POS_OUT2 59
#define POS_PLAYER1_START 4
#define POS_PLAYER2_START 52
// game speedup after player change in milliseconds
#define GAME_DELAY_SPEEDUP 2
// game delay in milliseconds
#define GAME_DELAY_INIT 150
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, PIN_LED_RING, NEO_GRB + NEO_KHZ800);
byte playerPos[2], ballPos, gameState = GAME_ENDED, currentPlayer, wonPlayer, gameDelaySpeedup;
bool button1Pressed = false, button2Pressed = false;
byte button1State, button2State;
unsigned long previousMillis, currentMillis;
unsigned int gameDelay = GAME_DELAY_INIT;
void setup()
{
pixels.begin();
randomSeed(analogRead(0));
for (byte j = 0; j < 16; j++) {
for (byte i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
}
for (byte i = j % 2; i < NUMPIXELS; i += 2) {
pixels.setPixelColor(i, pixels.Color(10 * ((j + 1) % 2), 10 * (j % 2), 0));
}
pixels.show();
delay(200);
}
for (byte i = 1; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
}
pixels.show();
}
void loop()
{
button1State = digitalRead(PIN_BUTTON_1);
if (button1State == LOW) {
button1Pressed = false;
}
if (button1State == HIGH && !button1Pressed) {
button1Pressed = true;
buttonPressedPlayer1();
}
button2State = digitalRead(PIN_BUTTON_2);
if (button2State == LOW) {
button2Pressed = false;
}
if (button2State == HIGH && !button2Pressed) {
buttonPressedPlayer2();
}
if (gameState == GAME_RUNNING) {
drawObjects();
updateBall();
}
}
void initGame()
{
gameState = GAME_INIT;
gameDelay = GAME_DELAY_INIT;
if (random(0, 100) > 50) {
currentPlayer = PLAYER_2;
ballPos = POS_PLAYER1_START + 1;
} else {
currentPlayer = PLAYER_1;
ballPos = POS_PLAYER2_START - 1;
}
playerPos[PLAYER_1] = POS_PLAYER1_START;
playerPos[PLAYER_2] = POS_PLAYER2_START;
wonPlayer = PLAYER_NONE;
gameState = GAME_RUNNING;
gameDelaySpeedup = GAME_DELAY_SPEEDUP;
currentMillis = millis();
previousMillis = currentMillis;
for (byte i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(10, 10, 10));
delay(10);
pixels.show();
}
for (byte i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
delay(10);
pixels.show();
}
}
void updateBall()
{
currentMillis = millis();
if (currentMillis - previousMillis >= gameDelay) {
previousMillis = currentMillis;
if (currentPlayer == PLAYER_1) {
ballPos--;
} else {
ballPos++;
}
if (ballPos > NUMPIXELS - 1) {
ballPos = 0;
}
checkGameState();
}
}
void checkGameState()
{
if (currentPlayer == PLAYER_1 && ballPos == POS_OUT1
|| playerPos[PLAYER_1] == POS_NET
|| playerPos[PLAYER_1] == POS_OUT1) {
wonPlayer = PLAYER_2;
} else if (currentPlayer == PLAYER_2 && ballPos == POS_OUT2
|| playerPos[PLAYER_2] == POS_NET
|| playerPos[PLAYER_2] == POS_OUT2) {
wonPlayer = PLAYER_1;
}
if (wonPlayer != PLAYER_NONE) {
gameState = GAME_ENDED;
drawEndScreen(wonPlayer);
}
}
void buttonPressedPlayer1()
{
if (gameState == GAME_RUNNING) {
if (currentPlayer == PLAYER_1) {
if (ballPos == playerPos[PLAYER_1]) {
playerPos[PLAYER_1]--;
} else if (ballPos > playerPos[PLAYER_1] + 1) {
playerPos[PLAYER_1] = ballPos - 1;
}
playerPos[PLAYER_1] = max(0, min(POS_NET, playerPos[PLAYER_1]));
if (ballPos >= playerPos[PLAYER_1]) {
currentPlayer = PLAYER_2;
speedUpGame();
}
}
} else if (gameState == GAME_ENDED) {
initGame();
}
}
void buttonPressedPlayer2()
{
if (gameState == GAME_RUNNING) {
if (currentPlayer == PLAYER_2) {
if (ballPos == playerPos[PLAYER_2]) {
playerPos[PLAYER_2]++;
} else if (ballPos < playerPos[PLAYER_2] - 1) {
playerPos[PLAYER_2] = ballPos + 1;
}
playerPos[PLAYER_2] = max(POS_NET, playerPos[PLAYER_2]);
if (playerPos[PLAYER_2] > NUMPIXELS - 1) {
playerPos[PLAYER_2] = 0;
}
if (playerPos[PLAYER_2] == 0 || ballPos <= playerPos[PLAYER_2]) {
currentPlayer = PLAYER_1;
speedUpGame();
}
}
} else if (gameState == GAME_ENDED) {
initGame();
}
}
void speedUpGame()
{
byte diff = (byte)gameDelaySpeedup * 0.1;
if (gameDelaySpeedup - diff > 0) {
gameDelaySpeedup -= diff;
}
gameDelay -= gameDelaySpeedup;
gameDelay = max(20, gameDelay);
}
void drawObjects()
{
for (byte i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
}
// Net (top center)
pixels.setPixelColor(POS_NET, pixels.Color(10, 10, 0));
// Out (bottom center)
pixels.setPixelColor(POS_OUT1, pixels.Color(10, 0, 0));
pixels.setPixelColor(POS_OUT2, pixels.Color(10, 0, 0));
// Ball
pixels.setPixelColor(ballPos, pixels.Color(20, 20, 10));
// playerPositions
pixels.setPixelColor(playerPos[PLAYER_1], pixels.Color(0, 0, 20));
pixels.setPixelColor(playerPos[PLAYER_2], pixels.Color(0, 0, 20));
pixels.show();
}
void drawEndScreen(byte wonPlayer)
{
for (byte j = 0; j < 16; j++) {
for (byte i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
}
if (wonPlayer == PLAYER_1) {
for (byte i = j % 2; i <= POS_NET; i += 2) {
pixels.setPixelColor(i, pixels.Color(0, 20, 0));
}
} else {
for (byte i = (POS_NET + j % 2); i < NUMPIXELS; i += 2) {
pixels.setPixelColor(i, pixels.Color(0, 20, 0));
}
}
pixels.show();
delay(100);
}
delay(3000);
for (byte i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
}
pixels.show();
}