Spiel "Pong"

Durch den Erwerb einer 8x8 WS2812 RGB-LED-Matrix kam mir die Idee, einen Computer-Spielklassiker nach zu programmieren.

Video: Live-Demonstration von Pong

Verwendete Bauteile

Aufbau

Die genaue Art des Aufbaus kann man aus dem Aufbau von WS2812-LEDs und Joysticks erfahren.

Aufbau der Schaltung
Abb.: Aufbau der Schaltung

Spielverlauf

Ziel des Spiels ist es, einen Ball zu Gegner zu schießen, so dass dieser ihn nicht zurückschlagen kann.
Das Spielprinzip von Pong ist simpel und ähnelt dem des Tennis: Ein Punkt ("Ball") bewegt sich auf der LED-Matrix hin und her. Jeder der beiden Spieler steuert einen senkrechten Strich ("Paddle"), den er mit seinem Joystick nach oben und unten verschieben kann. Lässt man den Ball am Paddle vorbei, gewinnt der andere Spieler bzw. er erhält einen Punkt (letzteres ist nicht implementiert).

Sketch

/**
 * Player 1: x = 0
 * Player 2: x = X_MAX-1
 */

#define PIN_LED_MATRIX        2
#define PIN_JOYSTICK_1_BUTTON 6
#define PIN_JOYSTICK_2_BUTTON 7
#define PIN_PIEZO             11
#define PIN_JOYSTICK_1_Y      A0
#define PIN_JOYSTICK_2_Y      A1

#define JOYSTICK_OFFSET_MIN 200
#define JOYSTICK_OFFSET_MAX 700

#define DEBOUNCE_TIME 300  // in ms

#define X_MAX 8
#define Y_MAX 8

#define GAME_DELAY 80         // in ms
#define BALL_DELAY_MAX   350  // in ms
#define BALL_DELAY_MIN    50  // in ms
#define BALL_DELAY_STEP    5  // in ms

#define PLAYER_AMOUNT 2
#define PLAYER_1 0
#define PLAYER_2 1

#define PADDLE_WIDTH 3

#define PADDLE_MOVE_NONE  0
#define PADDLE_MOVE_UP    1
#define PADDLE_MOVE_DOWN  2

#define LED_TYPE_OFF      1
#define LED_TYPE_PADDLE   2
#define LED_TYPE_BALL     3
#define LED_TYPE_BALL_RED 4

#define TONE_PLAYER 1
#define TONE_WALL   2
#define TONE_BUZZ   3

#define GAME_STATE_RUNNING 1
#define GAME_STATE_END     2
#define GAME_STATE_INIT    3

#include <Adafruit_NeoPixel.h>

struct Coords {
  byte x;
  byte y;
};

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(X_MAX * Y_MAX, PIN_LED_MATRIX, NEO_GRB + NEO_KHZ800);
bool buttonPressed = false;
byte gameState;
byte joystickPins[PLAYER_AMOUNT] = {PIN_JOYSTICK_1_Y, PIN_JOYSTICK_2_Y};
Coords paddles[PLAYER_AMOUNT][PADDLE_WIDTH];
Coords ball;
int ballMovement[2];
unsigned int ballDelay;
unsigned long lastDrawUpdate = 0;
unsigned long lastBallUpdate = 0;
unsigned long lastButtonClick = 0;

void setup()
{
  pinMode(PIN_JOYSTICK_1_Y, INPUT);
  pinMode(PIN_JOYSTICK_1_BUTTON, INPUT_PULLUP);
  pinMode(PIN_JOYSTICK_2_Y, INPUT);
  pinMode(PIN_JOYSTICK_2_BUTTON, INPUT_PULLUP);
  pixels.begin();
  resetLEDs();
  gameState = GAME_STATE_END;
}

void loop()
{
  switch(gameState) {
    case GAME_STATE_INIT:
      initGame();
      break;
    case GAME_STATE_RUNNING:
      updateBall();
      updateGame();
      break;
    case GAME_STATE_END:
      if (isButtonPressed()) {
        gameState = GAME_STATE_INIT;
      }
      break;
  }
}

void initGame()
{
  resetLEDs();
  lastButtonClick = millis();

  ball.x = 1;
  ball.y = (Y_MAX/2) - (PADDLE_WIDTH/2) + 1;
  ballMovement[0] =  1;
  ballMovement[1] = -1;
  ballDelay = BALL_DELAY_MAX;

  for(byte i=0; i<PADDLE_WIDTH; i++) {
    paddles[PLAYER_1][i].x = 0;
    paddles[PLAYER_1][i].y = (Y_MAX/2) - (PADDLE_WIDTH/2) + i;
    paddles[PLAYER_2][i].x = X_MAX - 1;
    paddles[PLAYER_2][i].y = paddles[PLAYER_1][i].y;
  }

  gameState = GAME_STATE_RUNNING;
}

void updateBall()
{
  bool hitBall = false;
  if ((millis() - lastBallUpdate) < ballDelay) {
    return;
  }
  lastBallUpdate = millis();
  toggleLed(ball.x, ball.y, LED_TYPE_OFF);

  // collision detection for player 1
  if (ballMovement[0] == -1 && ball.x == 1) {
    for(byte i=0; i<PADDLE_WIDTH; i++) {
      if (paddles[PLAYER_1][i].y == ball.y) {
        hitBall = true;
        break;
      }
    }
  }

  // collision detection for player 2
  if (ballMovement[0] == 1 && ball.x == X_MAX-2) {
    for(byte i=0; i<PADDLE_WIDTH; i++) {
      if (paddles[PLAYER_2][i].y == ball.y) {
        hitBall = true;
        break;
      }
    }
  }

  if (hitBall == true) {
    ballMovement[0] *= -1;
    playTone(TONE_PLAYER);
    if (ballDelay > BALL_DELAY_MIN) {
      ballDelay -= BALL_DELAY_STEP;
    }
  }

  ball.x += ballMovement[0];
  ball.y += ballMovement[1];

  if (ball.x <=0 || ball.x >= X_MAX-1) {
    endGame();
    return;
  }

  if (ball.y <= 0 || ball.y >= Y_MAX-1) {
    ballMovement[1] *= -1;
    playTone(TONE_WALL);
  }

  toggleLed(ball.x, ball.y, LED_TYPE_BALL);
  pixels.show();
}

void endGame()
{
  gameState = GAME_STATE_END;
  toggleLed(ball.x, ball.y, LED_TYPE_BALL_RED);
  pixels.show();
  playTone(TONE_BUZZ);
}

void updateGame()
{
  if ((millis() - lastDrawUpdate) < GAME_DELAY) {
    return;
  }
  lastDrawUpdate = millis();

  // turn off paddle LEDs
  for(byte p=0; p<PLAYER_AMOUNT; p++) {
    for(byte i=0; i<PADDLE_WIDTH; i++) {
      toggleLed(paddles[p][i].x, paddles[p][i].y, LED_TYPE_OFF);
    }
  }

  // move paddles
  for(byte p=0; p<PLAYER_AMOUNT; p++) {
    byte movement = getPlayerMovement(p);
    if (movement == PADDLE_MOVE_UP && paddles[p][PADDLE_WIDTH-1].y < (Y_MAX-1)) {
      for(byte i=0; i<PADDLE_WIDTH; i++) {
        paddles[p][i].y++;
      }
    }
    if (movement == PADDLE_MOVE_DOWN && paddles[p][0].y > 0) {
      for(byte i=0; i<PADDLE_WIDTH; i++) {
        paddles[p][i].y--;
      }
    }
  }

  // show paddle LEDs
  for(byte p=0; p<PLAYER_AMOUNT; p++) {
    for(byte i=0; i<PADDLE_WIDTH; i++) {
      toggleLed(paddles[p][i].x, paddles[p][i].y, LED_TYPE_PADDLE);
    }
  }
  pixels.show();
}

byte getPlayerMovement(byte playerId)
{
  int value = analogRead(joystickPins[playerId]);
  if (value < JOYSTICK_OFFSET_MIN) {
    if (playerId == PLAYER_2) {
      return PADDLE_MOVE_DOWN;
    } else {
      return PADDLE_MOVE_UP;
    }
  } else if (value > JOYSTICK_OFFSET_MAX) {
    if (playerId == PLAYER_2) {
      return PADDLE_MOVE_UP;
    } else {
      return PADDLE_MOVE_DOWN;
    }
  }
  return PADDLE_MOVE_NONE;
}

bool isButtonPressed()
{
  if ((millis() - lastButtonClick) < DEBOUNCE_TIME) {
    return false;
  }
  if (digitalRead(PIN_JOYSTICK_1_BUTTON) == LOW) {
    lastButtonClick = millis();
    return true;
  }
  if (digitalRead(PIN_JOYSTICK_2_BUTTON) == LOW) {
    lastButtonClick = millis();
    return true;
  }
  return false;
}

void resetLEDs()
{
  for(byte i=0; i<X_MAX*Y_MAX; i++) {
    pixels.setPixelColor(i, pixels.Color(0, 0, 0));
  }
  pixels.show();
}

void toggleLed(byte x, byte y, byte type)
{
  byte ledIndex = y * X_MAX + x;
  uint32_t color;

  switch(type) {
    case LED_TYPE_PADDLE:
      color = pixels.Color(0, 8, 8);
      break;
    case LED_TYPE_BALL_RED:
      color = pixels.Color(12, 0, 0);
      break;
    case LED_TYPE_BALL:
      color = pixels.Color(0, 10, 0);
      break;
    case LED_TYPE_OFF:
      color = pixels.Color(0, 0, 0);
      break;
  }

  pixels.setPixelColor(ledIndex, color);
}

void playTone(byte type)
{
  switch(type) {
    case TONE_PLAYER:
      tone(PIN_PIEZO, 440, 50);
      break;
    case TONE_WALL:
      tone(PIN_PIEZO, 550, 50);
      break;
    case TONE_BUZZ:
      for(byte i=0; i<20; i++) {
        tone(PIN_PIEZO, 220-i*10, 50);
        delay(50);
      }
      break;
  }
}

Weitere Ideen

zurück