Code Atari Breakout and Build your own Arduino Controller

Updated: May 7

Code the classic Breakout game in Processing3, and play with an interactive controller built from an Arduino complete with Joystick, LED display, and Play/Pause Button.



This is a video of the game that we are going to make!!

As an Amazon Associate I earn from qualifying purchases from paid links in this article

You can think of this project in 3 pieces:

1 - The Processing code

2 - The Arduino Code

3- The physical Arduino project


Parts List (contains paid links)


Step 1 - The Arduino physical interface

Building the Controller

Below is the diagram of how to wire this up on a breadboard, followed by some notes about each of the connections:












Arduino:

5V -> Positive Rail

GND -> GND Rail

A0 -> Joystick HOR

D2 -> Pushbutton GND

D3 thru D10 -> 7 Segment LED (Details further below)

D11 > 330 Ohm Resistor -> Red LED


Joystick:

  • VCC -> Positive Rail

  • HOR -> Arduino A0

  • GND -> Ground Rail

PushButton:

  • VCC -> Positive Rail

  • GND --> 330 Ohm Resistor --> Ground Rail

7-Segment Common Cathode Numerical Display :


Refer to the article 7-Segment LED Tutorial for a more detailed discussion on how this component works.


In our project the display has one 330 ohm resistor connected from it's common cathode (Pin 4) to GND. All other connections are shown in the below chart.

This is a chart that maps the segments to pins, then finally to the Arduino digital pins as shown in the schematic below. The order is important for reasons explained below. The code must know the mapping in order to light up the correct segments to draw numbers.





Step 2 - Writing the Code for the Controller

Interacting with the Hardware in the Arduino IDE

Here is the Arduino Code for the controller:


Joystick.ino


 
#include "SevSeg.h"
SevSeg sevseg; 

// Arduino pin numbers
const int X_pin = 0; // analog pin connected to X output
const int LED = 11;
const int BUTTON = 2;

int buttonVal = 0;
int oldButtonVal = 0;
int buttonState = 0;
int displayNum = 3;
char state;

void setup() {
  
  // setup serial communication
  Serial.begin(9600);   
  // Set digital pins
  pinMode(LED, OUTPUT);
  pinMode(BUTTON, INPUT);

  // Setup 7-Segment Display
  byte numDigits = 1;
  byte digitPins[] = {};  
  byte  segmentPins[] = {3, 4, 5, 6, 7, 8, 9, 10};
  bool resistorsOnSegments = true;
  byte hardwareConfig = COMMON_CATHODE; 
  sevseg.begin(hardwareConfig, numDigits, digitPins, segmentPins, 
               resistorsOnSegments);
  sevseg.setBrightness(90);   
}
 
void loop() {    
  Serial.print(analogRead(X_pin));
  Serial.print("/");
  Serial.println(buttonState);
  
  // See if button was pressed
  buttonVal = digitalRead(BUTTON);  
  if ((buttonVal == HIGH) && (oldButtonVal == LOW)) {
    buttonState = 1 - buttonState;
    delay(10);
  }
  oldButtonVal = buttonVal;
  
  // Decided if LED should be on
  if (buttonState == 1) {
    digitalWrite(LED, HIGH);   
  } else {
    digitalWrite(LED, LOW);
  }

  // Read current score from Processing sketch
  if (Serial.available() > 0) {
    state = Serial.parseInt();      
    displayNum = state;    
  }    
  
  // Write score to LED number display
  sevseg.setNumber(displayNum);                                 
  sevseg.refreshDisplay();     
  delay(50);
}

Now if you connect your Arduino to your computer with a USB cable (in this case micro USB for Arduino Nano), and then click play to run the sketch......nothing happens! This is because we haven't written any code yet for the actual Breakout game (which will be written in the Processing 3 language). But for now we can test our hardware by going to Tools --> Serial monitor within the processing environment. And then as we move around the joystick and press the button we will see the values printed to the Serial monitor.




The screenshot above shows the serial monitor displaying the analog value from the joystick ranging from 771 - 776, and the value for the button-press at 1.


Step 2 - Send data between the Computer and Arduino

Serial Communication

Before we go into the complete code for how to actually implement this game, let's focus on the way we get Processing and Arduino to actually interact with each other. For a complete tutorial on this, please see the post Arduino Communication with Processing.


Here is the code that makes the magic happen (you can skip to Section 3 if you just want to see the final processing code)


Arduino To Processing


This the Arduino code that sends the data to Processing over the Serial Port:

  Serial.print(analogRead(X_pin));
  Serial.print("/");
  Serial.println(buttonState);

This is the receiving-end code in Processing:

void serialEvent (Serial myPort) {
  serialMonitorString = myPort.readStringUntil('\n');
  if (serialMonitorString != null) {
    serialMonitorString = trim(serialMonitorString);
    // split the string at "/"
    String items[] = split(serialMonitorString, '/');
    if (items.length > 1) {
      //Paddle Movement and Button State
      paddleMovement = float(items[0]);
      buttonPressed = int(items[1]);
    }
  }
}


Processing To Arduino


This is the Processing code that sends the score as value of 'lives' to Arduino:

  myPort.write((char)(lives+'0'));

This Arduino code receives the score from Processing and stores it in the variable 'state':

 if (Serial.available() > 0) {
    state = Serial.parseInt();      
    displayNum = state;    
  }    



Step 3 - Coding the Game

The Sketch for Breakout is written in Processing

Now that we have our controller built, verified that it works, got a sneak peak into understanding how it will communicate with our game written in processing -- let's code the actual game!


You need the Processing IDE, and we are going to create 2 files. 1 is the main code, and the other is a class for the bricks. There is nothing too fancy here, and we will not be using any physics libraries (because we are a glutton for punishment). As a result we need to write out own collision detection.

This is the full code for the breakout game implemented in Processing:


Breakout.pde


import processing.sound.*;
import processing.serial.*;

Serial myPort;  
String serialMonitorString = "";   
ArrayList<Brick> bricks;
int buttonPressed = 0;
int lives = 3;
float paddleMovement;
float ballX;
float ballY;
float paddleX;
float paddleY;
float xSpeed = 6;
float ySpeed = -6;
float ballRadius = 15;
float paddleLength = 180;
float paddleHeight = 15;
float rightBounds;
float leftBounds;
float upperBounds;
float lowerBounds;
int numBrickRows = 6;
int numBrickCols = 10;
int offset = 5;
float brickWidth;
float brickHeight = 30;
float attackTime = 0.001;
float sustainTime = 0.004;
float sustainLevel = 1.0;
float releaseTime = 0.05;

TriOsc triOsc;
Env env;
Float[][] notes;

// Manage state of the game
enum GameState {
  START,
  PLAYING,
  LOSE,
  GAMEOVER,
  PAUSED
}

GameState state;

void setup() {
  
  size(900,600);
  
  String portName = Serial.list()[1]; 
  myPort = new Serial(this, portName, 9600);

  // Initialize dimensions of paddle, ball, bricks
  ballX = (width / 8);
  ballY = height - (height / 8);
  paddleX = (width / 2) - (paddleLength / 2);
  paddleY = height - paddleHeight - 30;
  rightBounds = width - ballRadius / 2;
  leftBounds = 0 + ballRadius / 2;
  lowerBounds = height - ballRadius / 2;
  upperBounds = 0 + ballRadius / 2;
  brickWidth = (width - (numBrickCols * offset)) / numBrickCols;
  bricks = new ArrayList<Brick>();
 
  // Initiliaze Brick Location Array List
  int startY = int(brickHeight) * 2;
    for (int i = 0; i < numBrickRows; i++) {
      int startX = 0;
      for (int j = 0; j < numBrickCols; j++) {
        bricks.add(new Brick(startX, startY + offset, brickWidth, brickHeight, random(230), random(230), random(230)));
        startX += brickWidth + offset;
      }
     startY += brickHeight + offset;
   }

  // Initialize startup screen
  background(0);  
  drawBricks();
  drawPaddle();
  drawBall(); 
  state = GameState.START;
  textSize(32);
  textAlign(CENTER);
  text("PRESS START TO BEGIN", width / 2, height - (height / 3)); 
   
  // Create triangle wave and envelope
  triOsc = new TriOsc(this);  
  env  = new Env(this); 
   
}
 
void draw() {
  // Check state of game 
  if (state == GameState.START) {
    if (buttonPressed == 1) {
      state = GameState.PLAYING;
     }        
    } else if (state == GameState.PAUSED) {  
        textSize(32);
        textAlign(CENTER);
        text("PAUSED", width / 2, height - (height / 3));
        if (buttonPressed == 1) {
          state = GameState.PLAYING;
        }  
      } else if (state == GameState.PLAYING) { // Main game state
          if (buttonPressed == 0) {
            state = GameState.PAUSED;
          }  
         background(0);  
         drawBricks();
         drawPaddle();
         drawBall(); 
     
      /* paddleMovement       
        Center value is 592
        Max value is 1020
        Min value is 116
        There is (1020-592) = 428 of movement to right side
        There is (592 - 116) = 476 of movement to the left side
        Divide by a scaling factor of 20, and then change sign if 
        the result is less than (592 / 20) = 29.6. But to account 
        for natural signal variations/noise, we want += 3.
       */        
       if  ( abs((paddleMovement  / 20) - 29) > 3) {
       // Stop paddle from moving off screen to the right
       if ((paddleX + (paddleMovement  / 20) - 29) > (width - 
            paddleLength) ) { 
         if (((paddleMovement  / 20) - 29) < 0) {
                 paddleX += (paddleMovement  / 20) - 29;  
             }
          // Stop paddle from moving off screen to the left
          } else if ( (paddleX + (paddleMovement  / 20) - 29) <= (0) ) 
       {
              if ( ((paddleMovement  / 20) - 29) > 0) {
                 paddleX += (paddleMovement  / 20) - 29; 
               }                     
             }    
      // Paddle is not at either end and should move freely from input
              else {
                paddleX += (paddleMovement  / 20) - 29;
          }          
       }
       
      checkBoundsBall();
      checkPaddleCollision();
      checkBrickCollision();
      checkLose();
      incrementBallPos();
  
  } else if (state == GameState.LOSE) {
      lives--; 
      myPort.write((char)(lives+'0'));
      checkGameOver();
    } else if (state == GameState.GAMEOVER) {
        textSize(32);
        textAlign(CENTER);
        text("GAME OVER", width / 2, height - (height / 3));
    }
}

void checkGameOver() {
  if (lives == 0) {
    state = GameState.GAMEOVER;  
  } else {
    reset();
  }
}

void reset() {
  delay(500);      
  ballX = 0 + (width / 8);
  ballY = height - (height / 8);
  state = GameState.PLAYING;
  xSpeed = 6;
  ySpeed = -6;  
}

void checkLose() {
  if ((ballY + ballRadius) > paddleY + paddleHeight + ySpeed) {
    state = GameState.LOSE;
   }  
}  

void drawPaddle() { 
  fill(102, 101, 232);
  stroke(102, 101, 232);
  rect(paddleX, paddleY, paddleLength, paddleHeight, 5);  
}

void drawBricks() {
  for (int i = 0; i < bricks.size(); i++) {    
      bricks.get(i).display();       
  }  
}

void drawBall() {
  fill(220, 220, 227);
  stroke(220, 220, 227);
  ellipse(ballX, ballY, ballRadius, ballRadius);
}

void incrementBallPos() {
  ballX = ballX + xSpeed;
  ballY = ballY + ySpeed; 
}

void checkPaddleCollision() { 
  if ((ballX + ballRadius) >= paddleX && 
  (ballX - ballRadius) <= (paddleX + paddleLength) && 
  (ballY + ballRadius ) >= paddleY &&
  (ballY + ballRadius ) < paddleY + 6) {   
    
    // Check if to the right of center
    if (ballX > (paddleX + (paddleLength / 2))) {
      xSpeed = abs(xSpeed);
    } else if  (ballX < (paddleX + (paddleLength / 2))) {
     xSpeed = abs(xSpeed) * -1; 
    } else {
     xSpeed = xSpeed * -1.0;
    }
     ySpeed = ySpeed * -1.0;    
     triOsc.play(220,0.5);
     env.play(triOsc, attackTime, sustainTime, sustainLevel, releaseTime);   
   }  
  
}

void checkBrickCollision() {
  for (int i = 0; i <= bricks.size() - 1; i++) {
    if (((ballX + ballRadius) >= bricks.get(i).xLoc) &&                 // Left corner of brick 
       ((ballX - ballRadius) <= (bricks.get(i).xLoc + brickWidth)) &&    // Right corner of brick
       ((ballY + ballRadius ) >= bricks.get(i).yLoc) &&                   // Top of brick
       ((ballY - ballRadius ) <= (bricks.get(i).yLoc + brickHeight))) {    // Bottom of brick 
         ySpeed = ySpeed * -1.0; 
         bricks.remove(i);
         triOsc.play(329.628,0.5);
         env.play(triOsc, attackTime, sustainTime, sustainLevel, releaseTime);
       }   
    } 
}

void checkBoundsBall() {
  if (ballX > rightBounds  || ballX < leftBounds) {   
    xSpeed = xSpeed * -1.0; 
    triOsc.play(277.183,0.5);
    env.play(triOsc, attackTime, sustainTime, sustainLevel, releaseTime);
  }
  
  if (ballY >= lowerBounds || ballY <= upperBounds) {
    ySpeed = ySpeed * -1.0; 
    triOsc.play(277.183,0.5);
    env.play(triOsc, attackTime, sustainTime, sustainLevel, releaseTime);
  }
}

void serialEvent (Serial myPort) {
  serialMonitorString = myPort.readStringUntil('\n');
  if (serialMonitorString != null) {
    serialMonitorString = trim(serialMonitorString);
    // split the string at "/"
    String items[] = split(serialMonitorString, '/');
    if (items.length > 1) {
      //Paddle Movement and Button State
      paddleMovement = float(items[0]);
      buttonPressed = int(items[1]);
    }
  }
}

Brick.pde

class Brick {
  float xLoc, yLoc; 
  float brickWidth, brickHeight;
  float red, green, blue;

  
  // Contructor
 Brick(float x, float y, float w, float h, float r, float g, float b) {
    xLoc = x;
    yLoc = y; 
    brickWidth = w;
    brickHeight = h;
    red = r;
    green = g;
    blue = b;
  } 
  
  // Custom method for drawing the object
  void display() {
    fill(red, green, blue);
    rect(xLoc, yLoc, brickWidth, brickHeight, 3);
  }
}

There's a lot to unpack here, but the basics are this:


1. Object oriented programming


We are using a Brick class to create each brick in a specified pattern.


2. Top-Down Design ("Stepwise Refinement")

In Processing, there always needs to be 2 functions - setup() and draw(). The function setup() only runs once in the beginning. The draw() function runs every single frame. It would be cumbersome for the program to do everything it needs within the function, so each task is separated out into separate functions and called from draw(). For example, drawBricks(), drawPaddle(), drawBall(); checkBoundsBall(), checkPaddleCollision(), etc...


3. GameState Enum


The state of the game is tracked with an enum dataType, which then gives us the ability to make the draw() function a massive loop that just evaluates the value of GameState, and then based off that calls the appropriate helper functions. Here is a commented code example of how enum's work:


// Initialize the GameState Enum
enum GameState {
  START,
  PLAYING,
  LOSE,
  GAMEOVER,
  PAUSED
}
// Create a variable so we can assign it a state
GameState state;

// Assign an initial value to 'state' variable
state = GameState.START;

// We can check the value of 'state' like this:
if (state == GameState.START) {

  // We can also change the value of 'state' as needed
  state = GameState.PLAYING   
}

4. Collision Detection

We are not using any physics libraries, so we need to implement our own collision detection. This can get a bit messy with a lot of if-then statements.


5. Understand where on the paddle the ball is hitting

Depending on which half of the paddle the ball bounces off, we need to either reverse or continue its direction.