Breakout game on an OLED, on an Arduino Mega
Breakout game on an OLED with a knob for the paddle: 32 bricks, three lives, a faster ball for every wall you clear, and sound on a piezo. The whole build, an Arduino Mega and 4 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced Runs in your browser. Free, and no account needed.
The parts retail for about $54. On Mokxi they're free.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Breakout on a 128x64 OLED, with a knob for the paddle, on an Arduino Mega.
//
// OLED SDA -> 20 (SDA), SCL -> 21 (SCL), VCC -> 5 V, GND -> GND (0x3C),
// mounted upside down, pins at the bottom: setRotation(2) turns
// the picture round to match
// knob the ends to 5 V and GND, the wiper to A0: moves the paddle
// button pin 2 and GND (INPUT_PULLUP): starts a game
// piezo pin 8 and GND
//
// Knock out all 32 bricks with the ball. Each brick is 10 points. Miss the
// ball and you lose one of three lives. Clear the wall and a new one comes,
// with a faster ball.
//
// Before a game, the computer plays: the paddle follows the ball by itself.
// Press the button to take over with the knob.
//
// # How the ball moves
//
// The ball's position and speed are kept in sixteenths of a pixel, in whole
// numbers. That lets it move at speeds like 1.5 pixels a frame without using
// slow floating point. Each frame the sketch works out where the ball would
// go next. If that spot is past a wall, inside a brick or on the paddle, the
// ball bounces: its speed in that direction changes sign. Where the ball
// hits the paddle sets its angle, so you can aim: the ends send it off
// sideways, the middle straight up.
//
// # Drawing only what changes
//
// The OLED library sends only the parts of the screen that changed since the
// last display(). So instead of clearing the screen and drawing everything
// each frame, the sketch rubs out the ball and paddle where they were and
// draws them where they are. That is a few bytes on the I2C bus, not 1024.
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
const uint8_t KNOB = A0;
const uint8_t BUTTON = 2;
const uint8_t PIEZO = 8;
const int W = 128;
const int H = 64;
const int TOP = 10; // the top wall; the score is above it
const int ROWS = 4;
const int COLS = 8;
const int BRICK_W = 15;
const int BRICK_H = 4;
const int BRICK_Y = 14; // the top row of bricks
const int PADDLE_W = 20;
const int PADDLE_Y = 60;
const int BALL = 2; // the ball is 2x2 pixels
const unsigned long FRAME_MS = 20;
Adafruit_SSD1306 display(W, H, &Wire, -1);
bool brick[ROWS][COLS];
int bricksLeft = 0;
// Ball position and speed, in sixteenths of a pixel.
long bx, by;
int vx, vy;
int speed = 24; // 1.5 pixels a frame to start
int paddle = 54; // its left edge
int lives = 3;
unsigned int score = 0;
bool demo = true;
bool lastButton = HIGH;
// What is drawn now, so it can be rubbed out.
int shownBallX = -10, shownBallY = -10, shownPaddle = -100;
long shownScore = -1;
void drawBrick(int r, int c, bool on) {
display.fillRect(c * (BRICK_W + 1) + 1, BRICK_Y + r * (BRICK_H + 2), BRICK_W, BRICK_H,
on ? SSD1306_WHITE : SSD1306_BLACK);
}
void newWall() {
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
brick[r][c] = true;
drawBrick(r, c, true);
}
}
bricksLeft = ROWS * COLS;
}
void serve() {
bx = (long)(paddle + PADDLE_W / 2) * 16;
by = (long)(PADDLE_Y - 8) * 16;
vx = random(2) ? speed / 2 : -speed / 2;
vy = -speed;
}
void drawStatus() {
long now = (long)score * 10 + lives;
if (now == shownScore) return;
shownScore = now;
display.fillRect(0, 0, W, 8, SSD1306_BLACK);
display.setCursor(0, 0);
if (demo) {
display.print("DEMO press start");
} else {
display.print("SCORE ");
display.print(score);
display.setCursor(84, 0);
display.print("LIVES ");
display.print(lives);
}
}
void newGame(bool asDemo) {
demo = asDemo;
score = 0;
lives = 3;
speed = 24;
display.clearDisplay();
display.drawFastHLine(0, TOP - 1, W, SSD1306_WHITE);
shownBallX = -10;
shownPaddle = -100;
shownScore = -1;
newWall();
serve();
drawStatus();
display.display();
Serial.println(demo ? "demo: press the button to play" : "game on: turn the knob to move");
}
// Does a ball with its corner at (x, y) touch this brick?
bool hitsBrick(int x, int y, int r, int c) {
int left = c * (BRICK_W + 1) + 1;
int top = BRICK_Y + r * (BRICK_H + 2);
return x + BALL > left && x < left + BRICK_W && y + BALL > top && y < top + BRICK_H;
}
void step() {
long nx = bx + vx;
long ny = by + vy;
int px = nx / 16;
int py = ny / 16;
// the side walls and the top
if (px < 0 || px > W - BALL) {
vx = -vx;
nx = bx + vx;
tone(PIEZO, 600, 10);
}
if (py < TOP) {
vy = -vy;
ny = by + vy;
tone(PIEZO, 600, 10);
}
px = nx / 16;
py = ny / 16;
// the bricks: the first one the ball would land in breaks
for (int r = 0; r < ROWS; r++) {
for (int c = 0; c < COLS; c++) {
if (!brick[r][c] || !hitsBrick(px, py, r, c)) continue;
brick[r][c] = false;
bricksLeft--;
drawBrick(r, c, false);
score += 10;
// Came in from the side if it was not already level with the brick.
if (hitsBrick(bx / 16, py, r, c)) {
vy = -vy;
} else {
vx = -vx;
}
tone(PIEZO, 1200 - r * 150, 20);
if (!demo) {
Serial.print("brick! score ");
Serial.println(score);
}
r = ROWS; // one brick a frame
break;
}
}
// the paddle
if (vy > 0 && py + BALL >= PADDLE_Y && by / 16 + BALL <= PADDLE_Y &&
px + BALL > paddle && px < paddle + PADDLE_W) {
// -8 at the left end to +8 at the right: the angle it goes off at
int where = (px + BALL / 2 - paddle) * 16 / PADDLE_W - 8;
vx = where * speed / 10;
vy = -speed;
ny = (long)(PADDLE_Y - BALL) * 16;
tone(PIEZO, 400, 15);
}
bx = bx + vx;
by = ny;
if (bx < 0) bx = 0;
if (bx > (long)(W - BALL) * 16) bx = (long)(W - BALL) * 16;
}
void loseBall() {
lives--;
tone(PIEZO, 120, 300);
Serial.print("ball lost, ");
Serial.print(lives);
Serial.println(lives == 1 ? " life left" : " lives left");
if (lives == 0) {
Serial.print("game over: score ");
Serial.println(score);
display.fillRect(28, 30, 72, 12, SSD1306_BLACK);
display.setCursor(37, 32);
display.print("GAME OVER");
display.display();
delay(2500);
newGame(true);
return;
}
delay(600);
serve();
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON, INPUT_PULLUP);
pinMode(PIEZO, OUTPUT);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("No OLED at 0x3C: check SDA and SCL");
while (true) {
}
}
// The OLED is mounted upside down, with its pins at the bottom next to the
// Mega's header, so turn the picture round to read the right way up.
display.setRotation(2);
display.setTextColor(SSD1306_WHITE);
display.setTextSize(1);
// Nothing is wired to A1, so it reads noise: a different serve each time.
randomSeed(analogRead(A1));
newGame(true);
}
void loop() {
unsigned long started = millis();
bool button = digitalRead(BUTTON);
if (button == LOW && lastButton == HIGH && demo) newGame(false);
lastButton = button;
if (demo) {
// The computer keeps the paddle under the ball.
paddle = constrain((int)(bx / 16) - PADDLE_W / 2 + 3, 0, W - PADDLE_W);
} else {
paddle = map(analogRead(KNOB), 0, 1023, 0, W - PADDLE_W);
}
step();
if (by / 16 > H) {
display.fillRect(shownBallX, shownBallY, BALL, BALL, SSD1306_BLACK);
shownBallX = -10;
display.display();
loseBall();
return;
}
if (bricksLeft == 0) {
speed += 6;
Serial.println("wall cleared: faster ball");
newWall();
serve();
}
// rub out and redraw only what moved
int x = bx / 16;
int y = by / 16;
if (x != shownBallX || y != shownBallY) {
if (shownBallX >= 0) display.fillRect(shownBallX, shownBallY, BALL, BALL, SSD1306_BLACK);
display.fillRect(x, y, BALL, BALL, SSD1306_WHITE);
shownBallX = x;
shownBallY = y;
}
if (paddle != shownPaddle) {
display.fillRect(0, PADDLE_Y, W, 3, SSD1306_BLACK);
display.fillRect(paddle, PADDLE_Y, PADDLE_W, 3, SSD1306_WHITE);
shownPaddle = paddle;
}
drawStatus();
display.display();
while (millis() - started < FRAME_MS) {
}
}
Parts list
6 parts, plus the jumper wires. They retail for about $54; on Mokxi they're free.
- 1 × Arduino Mega 2560 R3
- 1 × OLED display, 128x64
- 1 × Power, 5 V
- 1 × Arcade button
- 1 × Piezo speaker
- 1 × Potentiometer
How it is wired
11 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- Arduino Mega 2560 R3 pin 8; Piezo speaker pin 1
- Arduino Mega 2560 R3 pin 2; Arcade button pin 2
- Arduino Mega 2560 R3 pin 20; OLED display, 128x64 pin SDA
- Arduino Mega 2560 R3 pin 21; OLED display, 128x64 pin SCL
- Arduino Mega 2560 R3 pin 5V; Potentiometer pin 3
- Arduino Mega 2560 R3 pin A0; Potentiometer pin 2
- Ground: OLED display, 128x64 pin GND
- 5 V: OLED display, 128x64 pin VCC
- Ground: Arcade button pin 1
- Ground: Piezo speaker pin 2
- Ground: Potentiometer pin 1
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.