"Ring-Pong"

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.

Video: Live-Demonstration des Spiels (noch mit zwei Steckbrettern)

Funktionsweise des Spiels

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!

Verwendete Bauteile

Aufbau

Aufbau der Schaltung
Abb.: Aufbau der Schaltung

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.

Aufbau am Breadboard
Abb.: Aufbau am Breadboard

Sketch

#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);
}

Variante "String-Pong"

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)

Anschluss des WS2812B-LED-Streifens am Breadboard
Abb.: Anschluss des WS2812B-LED-Streifens am Breadboard

DIY-Controller

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:

Gehäuse für Gamecontroller

Gehäuse für Gamecontroller gcontroller.scad
Anschluss am Gamecontroller
Abb.: Anschluss am Gamecontroller für das Breadboard
Druckschalter des Gamecontroller
Abb.: Druckschalter des Gamecontroller

Sketch

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();
}

Demonstration

Video: Demonstration von String-Pong

Weitere Ideen

zurück