zurück

Spiel "Breakout"

13.07.2018

Diesen Spiele-Klassiker wollte ich schon länger auf dem Arduino nachbauen.

Video: Live-Demonstration von "Breakout"

Verwendete Bauteile

Spielverlauf

Ziel des Spiels ist es, alle "Mauersteine" abzuräumen.
Der Spieler steuert einen Schläger (Paddle), mit welchem er auf der Unterseite des Spielfeldes hin- und herfahren kann. Ein Ball springt zwischen dem Schläger und einer Wand aus "Mauersteinen", die beim Kontakt mit dem Ball zerspringen. Hat der Spieler alle "Mauersteine" abgeräumt, beginnt ein neues Level. Verliert der Spieler den Ball, so ist das Spiel vorüber.

Aufbau

Aufbau der Schaltung
Abb.: Aufbau der Schaltung

Sketch

/**
 * Breakout game for Arduino and a 8x8 WS2812 LED matrix
 * Hint: XY(0,0) = top left
 */

#define PIN_LED_MATRIX        9
#define PIN_PIEZO             5
#define PIN_BUTTON_LEFT       13
#define PIN_BUTTON_RIGHT      12

#define DEBOUNCE_TIME 100      // in ms

#define X_MAX         8
#define Y_MAX         8
#define BRICK_AMOUNT  X_MAX * 3

#define BALL_DELAY_MAX   350  // in ms
#define BALL_DELAY_MIN   100  // in ms
#define BALL_DELAY_STEP    5  // in ms

#define PADDLE_WIDTH 3

#define DIRECTION_NONE  0
#define DIRECTION_LEFT  1
#define DIRECTION_RIGHT 2

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

#define TONE_PADDLE    1
#define TONE_BRICK     2
#define TONE_END_GAME  3
#define TONE_NEW_LEVEL 4

#define GAME_STATE_RUNNING 1
#define GAME_STATE_END     2
#define GAME_STATE_LEVEL   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);
byte gameState;
byte level;
byte destroyedBricks;
Coords paddle[PADDLE_WIDTH];
Coords bricks[BRICK_AMOUNT];
Coords ball;
int ballMovement[2];
unsigned int ballDelay;
unsigned int score;
unsigned long lastBallUpdate = 0;
unsigned long lastButtonClick = 0;

void setup()
{
  Serial.begin(9600);
  pinMode(PIN_BUTTON_LEFT, INPUT_PULLUP);
  pinMode(PIN_BUTTON_RIGHT, INPUT_PULLUP);
  pinMode(PIN_PIEZO, OUTPUT);
  pixels.begin();
  resetLEDs();
  gameState = GAME_STATE_END;
}

void loop()
{
  switch(gameState) {
    case GAME_STATE_LEVEL:
      if (buttonPressed() != DIRECTION_NONE) {
        newLevel();
      }
      break;
    case GAME_STATE_RUNNING:
      updateBall();
      updatePaddle();
      break;
    case GAME_STATE_END:
      if (buttonPressed() != DIRECTION_NONE) {
        initGame();
      }
      break;
  }
}

void initGame()
{
    resetLEDs();
    lastButtonClick = millis();
    ballDelay = BALL_DELAY_MAX;
    score = 0;
    level = 0;
    newLevel();
}

void initBricks()
{
  destroyedBricks = 0;
  for(byte i=0; i<BRICK_AMOUNT; i++) {
    bricks[i].x = i % X_MAX;
    bricks[i].y = i / X_MAX;
    toggleLed(bricks[i].x, bricks[i].y, LED_TYPE_BRICK);
    delay(25);
    pixels.show();
  }
}

void newLevel()
{
  playTone(TONE_NEW_LEVEL);
  initBricks();
  for(byte i=0; i<PADDLE_WIDTH; i++) {
    paddle[i].x = (X_MAX/2) - (PADDLE_WIDTH/2) + i;
    paddle[i].y = Y_MAX - 1;
    toggleLed(paddle[i].x, paddle[i].y, LED_TYPE_PADDLE);
  }
  ball.x = paddle[1].x;
  ball.y = paddle[1].y - 1;
  toggleLed(ball.x, ball.y, LED_TYPE_BALL);
  ballMovement[0] =  1;
  ballMovement[1] = -1;
  lastBallUpdate = 0;
  pixels.show();
  level++;
  gameState = GAME_STATE_RUNNING;
}

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

  if (ballMovement[1] == 1) {
    // collision with bottom
    if (ball.y == (Y_MAX - 1)) {
      endGame();
      return;
    }
    checkPaddleCollision();
  }

  // collision detection with bricks
  for(byte i=0; i<BRICK_AMOUNT; i++) {
    if (bricks[i].x == ball.x && bricks[i].y == ball.y) {
      hitBrick(i);
      break;
    }
  }
  if (destroyedBricks >= BRICK_AMOUNT) {
    gameState = GAME_STATE_LEVEL;
    return;
  }

  // collision detection with wall
  if (ball.x <= 0 || ball.x >= (X_MAX - 1)) {
    ballMovement[0] *= -1;
  }
  if (ball.y <= 0) {
    ballMovement[1] *= -1;
  }

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

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

void hitBrick(byte i)
{
  bricks[i].x = -1;
  bricks[i].y = -1;
  //ballMovement[1] *= -1;
  score++;
  destroyedBricks++;
  if (ballDelay > BALL_DELAY_MIN) {
    ballDelay -= BALL_DELAY_STEP;
  }
  toggleLed(bricks[i].x, bricks[i].y, LED_TYPE_OFF);
  playTone(TONE_BRICK);
}

void checkPaddleCollision()
{
  if ((paddle[0].y-1) != ball.y) {
    return;
  }

  // reverse movement direction on the edges
  if (ballMovement[0] == 1 && (paddle[0].x-1) == ball.x ||
      ballMovement[0] == -1 && (paddle[PADDLE_WIDTH-1].x+1) == ball.x) {
    ballMovement[0] *= -1;
    ballMovement[1] *= -1;
    playTone(TONE_PADDLE);
    return;
  }
  if (paddle[PADDLE_WIDTH/2].x == ball.x) {
    ballMovement[0] = 0;
    ballMovement[1] *= -1;
    playTone(TONE_PADDLE);
    return;
  }

  for(byte i=0; i<PADDLE_WIDTH; i++) {
    if (paddle[i].x == ball.x) {
      ballMovement[1] *= -1;
      if (random(2) == 0) {
        ballMovement[0] = 1;
      } else {
        ballMovement[0] =-1;
      }
      playTone(TONE_PADDLE);
      break;
    }
  }
}

void updatePaddle()
{
    byte buttonState = buttonPressed();
    if(buttonState == DIRECTION_NONE) {
      return;
    }

    unsigned int moveDirection = 0;
    if (buttonState == DIRECTION_LEFT && paddle[0].x > 0) {
        moveDirection = -1;
    }
    if (buttonState == DIRECTION_RIGHT && paddle[PADDLE_WIDTH-1].x < (X_MAX-1)) {
        moveDirection = 1;
    }

    if (moveDirection != 0) {
        // turn off paddle LEDs
        for(byte i=0; i<PADDLE_WIDTH; i++) {
          toggleLed(paddle[i].x, paddle[i].y, LED_TYPE_OFF);
        }
        for(byte i=0; i<PADDLE_WIDTH; i++) {
            paddle[i].x += moveDirection;
        }
        for(byte i=0; i<PADDLE_WIDTH; i++) {
          toggleLed(paddle[i].x, paddle[i].y, LED_TYPE_PADDLE);
        }
    }
    pixels.show();
}

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

byte buttonPressed()
{
  if ((millis() - lastButtonClick) < DEBOUNCE_TIME) {
    return DIRECTION_NONE;
  }
  lastButtonClick = millis();
  if (digitalRead(PIN_BUTTON_LEFT) == LOW) {
    return DIRECTION_LEFT;
  }
  if (digitalRead(PIN_BUTTON_RIGHT) == LOW) {
    return DIRECTION_RIGHT;
  }
  return DIRECTION_NONE;
}

void endGame()
{
  Serial.println("GAME OVER!");
  gameState = GAME_STATE_END;
  toggleLed(ball.x, ball.y, LED_TYPE_BALL_RED);
  pixels.show();
  Serial.println("Final score: " + String(score) + " in level " + String(level));
  playTone(TONE_END_GAME);
}

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_BRICK:
      color = pixels.Color(8, 8, 0);
      break;
    case LED_TYPE_BALL:
      color = pixels.Color(0, 10, 0);
      break;
    case LED_TYPE_BALL_RED:
      color = pixels.Color(12, 0, 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_PADDLE:
      tone(PIN_PIEZO, 440, 50);
      break;
    case TONE_BRICK:
      tone(PIN_PIEZO, 550, 50);
      break;
    case TONE_NEW_LEVEL:
      tone(PIN_PIEZO, 350, 80);
      delay(200);
      tone(PIN_PIEZO, 350, 80);
      delay(200);
      tone(PIN_PIEZO, 350, 80);
      delay(200);
      tone(PIN_PIEZO, 280, 300);
      break;
    case TONE_END_GAME:
      for(byte i=0; i<20; i++) {
        tone(PIN_PIEZO, 220-i*10, 50);
        delay(50);
      }
      break;
  }
}