
Get full C++ & Python code + lifetime access
The ESP32 Breakout Game is an embedded graphics and real-time gaming project that converts an ESP32 microcontroller into a standalone mini arcade console using a 128Ć160 ST7735 SPI TFT display and two tactile pushbuttons.
The system recreates the classic Breakout arcade game, where the player controls a paddle at the bottom of the display using left and right buttons. A moving ball bounces between the paddle, walls, and colored bricks. The objective is to destroy all the bricks without allowing the ball to fall below the paddle.
The entire game runs directly on the ESP32 without requiring a computer, operating system, internet connection, or external game engine.
The firmware uses a full-screen 16-bit RGB framebuffer stored inside ESP32 RAM. Instead of drawing each object directly onto the TFT one by one, the complete game frame is first rendered in memory and then transferred to the display. This significantly reduces flickering and produces smoother animations.
The game graphics are rendered at approximately 60 FPS, while the internal ball physics and collision calculations run at approximately 120 Hz for more accurate movement and collision detection.
This project introduces:
INPUT_PULLUP)ESP32 Development Board Ć 1
1.8-inch ST7735 SPI TFT Display (128Ć160) Ć 1
Tactile Pushbuttons Ć 2
Half-Size or Full-Size Breadboard Ć 1
Male-to-Male Jumper Wires Ć 1 Set
Micro-USB or Type-C Cable Ć 1
USB Power Source Ć 1
Design Note: The ST7735 display communicates with the ESP32 using SPI. The display only requires data transmission from the ESP32, so the SPI MISO line is not required. GPIO 19 can therefore be used as the left control button instead of SPI MISO.
Connect the ST7735 display to the ESP32:
VCC ā ESP32 3.3VGND ā ESP32 GNDSCK / CLK ā ESP32 GPIO 18SDA / MOSI ā ESP32 GPIO 23CS ā ESP32 GPIO 5RST / RESET ā ESP32 GPIO 4DC / A0 ā ESP32 GPIO 2The display is used in vertical portrait orientation with a resolution of:
128 Ć 160 pixels
Both buttons are connected between their assigned ESP32 GPIO pins and ground.
The ESP32's internal pull-up resistors are enabled in software, so no external resistors are required.
Left Button:
GPIO 19GNDRight Button:
GPIO 21GNDThe buttons are configured using:
pinMode(BTN_LEFT, INPUT_PULLUP);
pinMode(BTN_RIGHT, INPUT_PULLUP);
This means the buttons operate using Active-LOW logic:
Button Released = HIGH
Button Pressed = LOW
Inside setup(), the ESP32 first initializes the SPI communication bus:
SPI.begin(
TFT_SCLK,
-1,
TFT_MOSI,
TFT_CS
);
The SPI MISO pin is set to -1 because the TFT display does not need to send data back to the ESP32.
This also allows GPIO 19 to be used independently as the left control button.
The ST7735 display is initialized using:
tft.initR(INITR_BLACKTAB);
The display is then configured for vertical portrait orientation:
tft.setRotation(0);
The resulting game area is:
Width = 128 pixels
Height = 160 pixels
To reduce flickering, the game does not directly redraw each individual object onto the TFT.
Instead, a complete frame is generated inside ESP32 RAM using:
GFXcanvas16 *frame;
The framebuffer is created using:
frame = new GFXcanvas16(
SCREEN_W,
SCREEN_H
);
The framebuffer contains:
128 Ć 160 = 20,480 pixels
Each pixel uses 16-bit RGB565 color data, requiring approximately:
20,480 Ć 2 bytes
ā 40 KB RAM
The paddle, ball, bricks, background, score, and game interface are first drawn into this memory buffer.
The finished frame is then transferred to the TFT using:
tft.writePixels(
frame->getBuffer(),
SCREEN_W * SCREEN_H,
true
);
This technique produces significantly smoother graphics compared with drawing each game object directly onto the display.
The game targets approximately 60 frames per second.
The frame interval is calculated using:
const uint32_t FRAME_TIME_US =
1000000UL / 60;
This produces a target frame duration of approximately:
16.67 milliseconds
The firmware uses:
micros();
to determine when the next frame should be rendered.
This prevents the game speed from depending directly on how quickly the ESP32 executes the main program loop.
The graphics and physics systems operate independently.
The internal physics update interval is configured as:
const float PHYSICS_STEP =
1.0f / 120.0f;
This means ball movement and collision calculations are performed approximately:
120 times per second
while the TFT graphics are rendered approximately:
60 times per second
Running collision calculations faster than the display refresh helps prevent the ball from passing through thin bricks or missing paddle collisions when moving quickly.
The paddle position is stored as a floating-point value:
float paddleX;
When the left button is pressed:
paddleX -= paddleSpeed * dt;
When the right button is pressed:
paddleX += paddleSpeed * dt;
The variable dt represents the elapsed time between updates.
Using time-based movement instead of moving the paddle by a fixed number of pixels every loop produces more consistent and smooth controls.
The firmware also limits the paddle position so it cannot move outside the left or right boundaries of the display.
The ball uses independent horizontal and vertical velocity values:
float ballVX;
float ballVY;
During every physics update, its position is changed using:
ballX += ballVX * dt;
ballY += ballVY * dt;
The velocity signs determine the direction:
ballVX > 0 ā Moving Right
ballVX < 0 ā Moving Left
ballVY > 0 ā Moving Down
ballVY < 0 ā Moving Up
This allows the ball to move naturally in different directions across the screen.
When the player presses either button on the start screen, the game begins.
The ball waits for only a very short period before launching:
ballLaunchTimer =
millis() + 180;
This creates an approximately:
180 ms
launch delay.
The short delay allows the first game frame to appear while keeping the controls responsive and avoiding a noticeable pause before gameplay begins.
The firmware continuously checks the ball position against the screen boundaries.
When the ball reaches the left wall:
ballVX = fabs(ballVX);
When the ball reaches the right wall:
ballVX = -fabs(ballVX);
When the ball reaches the upper wall:
ballVY = fabs(ballVY);
Reversing the appropriate velocity direction produces the bouncing effect.
The paddle collision system calculates the exact location where the ball touches the paddle.
The paddle center is calculated using:
float paddleCenter =
paddleX + paddleW / 2.0f;
The impact position is converted into a normalized value representing where the ball contacted the paddle.
A ball hitting the left side of the paddle is redirected toward the left.
A ball hitting the right side is redirected toward the right.
A ball hitting near the center travels in a more vertical direction.
The horizontal velocity is calculated using:
ballVX =
hit *
ballSpeed *
0.70f;
This allows the player to influence the direction of the ball instead of every paddle collision producing exactly the same bounce.
The ball begins with a controlled starting speed:
START_BALL_SPEED = 120
After successful paddle collisions, the speed gradually increases:
ballSpeed += 2.0f;
The maximum speed is limited to:
MAX_BALL_SPEED = 180
This creates a gradual increase in difficulty without allowing the ball to become uncontrollably fast.
The brick field is stored using a two-dimensional Boolean array:
bool bricks[BRICK_ROWS][BRICK_COLS];
An active brick has the state:
true
A destroyed brick has the state:
false
During every physics update, the ESP32 checks the ball against all remaining bricks.
When the ball collides with a brick:
bricks[row][col] = false;
The brick is removed from the next rendered frame.
The player's score is also increased:
score += 10;
The firmware calculates how deeply the ball overlaps each side of a brick.
It compares:
If the collision occurred primarily on the side of a brick:
ballVX = -ballVX;
If the collision occurred primarily on the top or bottom:
ballVY = -ballVY;
This creates more natural brick collision behavior than simply reversing the vertical direction for every collision.
The game intentionally uses a single-life system.
There are no additional rounds or spare lives.
If the player misses the ball and it falls below the paddle:
gameOver = true;
The game immediately enters the Game Over state.
The gameplay sequence is:
Start Game
ā
Control Paddle
ā
Destroy Bricks
ā
Miss Ball
ā
Game Over
This keeps the gameplay simple, fast, and suitable for a small standalone embedded arcade project.
When the player misses the ball, physics processing stops and a dedicated Game Over interface appears.
The screen displays:
GAME
OVER
The player can start a fresh game by pressing both control buttons together.
The firmware detects:
if (left && right) {
newGame();
}
The score, paddle, ball, and brick field are then reset.
After every brick collision, the firmware checks whether any active bricks remain.
If every brick has been destroyed:
gameWon = true;
The normal game physics stop and the display shows:
YOU WIN!
The game can then be restarted by pressing both buttons.
The ST7735 display uses 16-bit RGB565 colors.
Custom colors are generated using:
tft.color565(
red,
green,
blue
);
Different brick rows use bright arcade-style colors such as:
The game uses a dark background with bright game elements to create a clean, high-contrast arcade interface on the small TFT display.
The complete game operates using the following sequence:
Power ESP32
ā
Initialize GPIO Buttons
ā
Initialize SPI Bus
ā
Initialize ST7735 TFT
ā
Allocate 16-Bit Framebuffer
ā
Display BREAKOUT Start Screen
ā
Player Presses Button
ā
Start New Game
ā
Launch Ball
ā
Read Left / Right Buttons
ā
Update Paddle Position
ā
Calculate Ball Movement
ā
Check Wall Collision
ā
Check Paddle Collision
ā
Check Brick Collision
ā
Update Score
ā
Check Win / Game Over State
ā
Render Complete Frame in RAM
ā
Transfer Framebuffer to TFT
ā
Repeat
The graphics system operates at approximately:
60 FPS
while game physics are calculated at approximately:
120 Hz
The completed ESP32 Breakout Game operates as a fully standalone miniature arcade system.
Once powered, the ESP32 independently handles:
No computer, operating system, internet connection, external graphics processor, or game engine is required while the game is running.
The project demonstrates how an ESP32 can simultaneously perform real-time graphics rendering, game physics calculations, collision detection, digital input processing, framebuffer management, and high-speed SPI communication on a compact embedded platform.