Pong Game with Arduino UNO and OLED Display – Project Explanation

Arduino UNO Pong Game with OLED Display | SSD1306 I2C Project | MakeMindz

Arduino Game Project

Pong Game on Arduino UNO with OLED Display

A classic arcade Pong game running on Arduino UNO with a 0.96″ SSD1306 OLED — featuring real-time ball physics, a CPU opponent, push-button controls and sound effects from a piezo buzzer.

🟢 Beginner–Intermediate 🖥️ SSD1306 OLED / I2C 🎮 Real-Time Game Logic

01 — Bill of Materials

Components Required

Everything you need to build this project. The buzzer is optional but adds great sound feedback.

🟦
Arduino UNO
ATmega328P board
🖥️
0.96″ OLED
SSD1306, I2C, 128×64
🔘
Push Buttons ×2
UP and DOWN control
🔔
Piezo Buzzer
Connected to D11
🧱
Breadboard
Half or full size
🔌
Jumper Wires
Male-to-male
🔋
USB Cable
Type-B to USB-A
🎛️
Potentiometer
Optional (smooth control)

02 — Concept Overview

How It Works

The Arduino drives a 128×64 OLED screen over I2C. Every 16 ms the ball moves one pixel; every 64 ms the paddles update. The CPU opponent tracks the ball automatically, while the player uses UP/DOWN buttons. A buzzer fires different tones for paddle hits, wall bounces and scoring events.

⚡ Timing (millis)

  • BALL_RATE = 16 ms
  • PADDLE_RATE = 64 ms
  • No blocking delays in game loop

🎯 Collision Detection

  • Ball vs top/bottom walls
  • Ball vs left/right walls (score)
  • Ball vs player paddle
  • Ball vs CPU paddle

🖼️ Graphics Functions

  • drawPixel() — ball
  • drawFastVLine() — paddles
  • drawRect() — court
  • setCursor/print() — score

🔊 Sound Events

  • Player paddle hit (250 Hz)
  • CPU paddle hit (225 Hz)
  • Wall bounce (200 Hz)
  • Score jingle (two-tone)


 

03 — Visual Schematic

Circuit Diagram

Arduino UNO + SSD1306 OLED + Push Buttons + Buzzer

ARDUINO UNO A4 SDA A5 SCL 5V GND D2 (UP) D3 (DOWN) D11 (Buzzer) GND USB-B SSD1306 OLED 0.96″ 128×64 I2C 0 0 VCC GND SDA SCL UP BTN DOWN BTN PIEZO BUZZER Shared GND Wire Colour Key SDA (I2C Data) SCL (I2C Clock) 5V Power GND UP Button DOWN Button Buzzer Signal

04 — Pin Connections

Wiring Connections

SSD1306 OLED → Arduino UNO

OLED PinArduino PinWireNote
VCC5VRedPower supply
GNDGNDBlackGround
SDAA4YellowI2C Data
SCLA5BlueI2C Clock

Push Buttons → Arduino UNO

ButtonArduino PinWireNote
UP Button (one leg)D2PurpleINPUT_PULLUP — reads LOW when pressed
UP Button (other leg)GNDBlackNo external resistor needed
DOWN Button (one leg)D3TealINPUT_PULLUP — reads LOW when pressed
DOWN Button (other leg)GNDBlackShared GND bus

Piezo Buzzer → Arduino UNO

Buzzer PinArduino PinWireNote
Positive (+)D11Orangetone() signal output
Negative (−)GNDBlackGround

05 — Sketch

Full Arduino Code

Install Adafruit_SSD1306 and Adafruit_GFX via the Arduino IDE Library Manager before uploading.

Arduino C++ — Pong Game (Full Sketch)
/*
  Pong Game — Arduino UNO + SSD1306 OLED
  MakeMindz.com | makemindz.com/2026/02/pong-game-with-arduino-uno-and-oled.html

  Based on Arduino Pong by eholk (github.com/eholk/Arduino-Pong)
  Original Wokwi version: notabug.org/Maverick/WokwiPong
*/

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define UP_BUTTON   2
#define DOWN_BUTTON 3

const unsigned long PADDLE_RATE  = 64;
const unsigned long BALL_RATE    = 16;
const uint8_t       PADDLE_HEIGHT = 12;
const uint8_t       SCORE_LIMIT   = 9;

Adafruit_SSD1306 display = Adafruit_SSD1306(128, 64, &Wire);

bool    game_over, win;
uint8_t player_score, mcu_score;
uint8_t ball_x = 53, ball_y = 26;
uint8_t ball_dir_x = 1, ball_dir_y = 1;

unsigned long ball_update, paddle_update;

const uint8_t MCU_X    = 12;   uint8_t mcu_y    = 16;
const uint8_t PLAYER_X = 115;  uint8_t player_y = 16;

void setup() {
    display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
    display.display();                    // Adafruit splash screen
    unsigned long start = millis();

    pinMode(UP_BUTTON,   INPUT_PULLUP);
    pinMode(DOWN_BUTTON, INPUT_PULLUP);
    pinMode(7, OUTPUT);
    digitalWrite(7, LOW);

    display.clearDisplay();
    drawCourt();
    while(millis() - start < 2000);     // 2-second intro
    display.display();

    ball_update = paddle_update = millis();
}

void loop() {
    bool          update_needed = false;
    unsigned long time = millis();
    static bool   up_state = false, down_state = false;

    up_state   |= (digitalRead(UP_BUTTON)   == LOW);
    down_state |= (digitalRead(DOWN_BUTTON) == LOW);

    /* ── BALL UPDATE ── */
    if(time > ball_update) {
        uint8_t new_x = ball_x + ball_dir_x;
        uint8_t new_y = ball_y + ball_dir_y;

        // Vertical walls → scoring
        if(new_x == 0 || new_x == 127) {
            ball_dir_x = -ball_dir_x;
            new_x += ball_dir_x + ball_dir_x;
            if(new_x < 64) { player_scoreTone(); player_score++; }
            else           { mcu_scoreTone();    mcu_score++;    }
            if(player_score == SCORE_LIMIT || mcu_score == SCORE_LIMIT) {
                win = player_score > mcu_score;
                game_over = true;
            }
        }
        // Horizontal walls → bounce
        if(new_y == 0 || new_y == 53) {
            wallTone();
            ball_dir_y = -ball_dir_y;
            new_y += ball_dir_y + ball_dir_y;
        }
        // CPU paddle collision
        if(new_x == MCU_X && new_y >= mcu_y && new_y <= mcu_y + PADDLE_HEIGHT) {
            mcuPaddleTone();
            ball_dir_x = -ball_dir_x;
            new_x += ball_dir_x + ball_dir_x;
        }
        // Player paddle collision
        if(new_x == PLAYER_X && new_y >= player_y && new_y <= player_y + PADDLE_HEIGHT) {
            playerPaddleTone();
            ball_dir_x = -ball_dir_x;
            new_x += ball_dir_x + ball_dir_x;
        }

        display.drawPixel(ball_x, ball_y, BLACK);
        display.drawPixel(new_x, new_y, WHITE);
        ball_x = new_x; ball_y = new_y;
        ball_update += BALL_RATE;
        update_needed = true;
    }

    /* ── PADDLE UPDATE ── */
    if(time > paddle_update) {
        paddle_update += PADDLE_RATE;
        const uint8_t hp = PADDLE_HEIGHT >> 1;

        // CPU AI tracks ball
        display.drawFastVLine(MCU_X, mcu_y, PADDLE_HEIGHT, BLACK);
        if(mcu_y + hp > ball_y) mcu_y += (ball_x > MCU_X ? -1 :  1);
        if(mcu_y + hp < ball_y) mcu_y += (ball_x > MCU_X ?  1 : -1);
        mcu_y = constrain(mcu_y, 1, 53 - PADDLE_HEIGHT);
        display.drawFastVLine(MCU_X, mcu_y, PADDLE_HEIGHT, WHITE);

        // Player movement
        display.drawFastVLine(PLAYER_X, player_y, PADDLE_HEIGHT, BLACK);
        if(up_state)   player_y -= 1;
        if(down_state) player_y += 1;
        up_state = down_state = false;
        player_y = constrain(player_y, 1, 53 - PADDLE_HEIGHT);
        display.drawFastVLine(PLAYER_X, player_y, PADDLE_HEIGHT, WHITE);
        update_needed = true;
    }

    /* ── DISPLAY ── */
    if(update_needed) {
        if(game_over) {
            const char* text = win ? "YOU WIN!!" : "YOU LOSE!";
            display.clearDisplay();
            display.setCursor(40, 28);
            display.print(text);
            display.display();
            delay(5000);
            // Reset everything
            ball_x = 53; ball_y = 26; ball_dir_x = 1; ball_dir_y = 1;
            mcu_y = player_y = 16; mcu_score = player_score = 0;
            game_over = false;
            display.clearDisplay(); drawCourt();
        }
        display.setTextColor(WHITE, BLACK);
        display.setCursor(0,   56); display.print(mcu_score);
        display.setCursor(122, 56); display.print(player_score);
        display.display();
    }
}

/* ── SOUND FUNCTIONS ── */
void playerPaddleTone() { tone(11,250,25); delay(25); noTone(11); }
void mcuPaddleTone()    { tone(11,225,25); delay(25); noTone(11); }
void wallTone()         { tone(11,200,25); delay(25); noTone(11); }

void player_scoreTone() {
    tone(11,200,25); delay(50); noTone(11);
    delay(25); tone(11,250,25); delay(25); noTone(11);
}
void mcu_scoreTone() {
    tone(11,250,25); delay(25); noTone(11);
    delay(25); tone(11,200,25); delay(25); noTone(11);
}

void drawCourt() { display.drawRect(0,0,128,54,WHITE); }

06 — Build Guide

Step-by-Step Instructions

1

Install Required Libraries

Open Arduino IDE → Sketch → Include Library → Manage Libraries. Search for and install Adafruit SSD1306 and Adafruit GFX Library. Both are required. Wire.h and SPI.h come built-in.

2

Wire the SSD1306 OLED Display

Connect OLED VCC → 5V, GND → GND, SDA → A4, SCL → A5. Most 0.96″ breakout modules have these four pins in order.

💡 I2C address: The default is 0x3C. If your display doesn't initialise, try 0x3D and update the display.begin() call accordingly.
3

Connect the UP and DOWN Buttons

Wire one leg of the UP button to D2 and the other to GND. Repeat for the DOWN button on D3. The code uses INPUT_PULLUP, so no external resistors are needed.

4

Wire the Piezo Buzzer

Connect the buzzer's positive pin to D11 and negative to GND. The tone() function will generate frequencies automatically for each game event.

⚠️ Optional: Skip the buzzer entirely if you don't need sound. The game will still work — just remove or comment out the tone functions.
5

Upload the Sketch

Copy the full code above into Arduino IDE. Select Board: Arduino UNO and the correct COM port. Click Upload. The OLED will show the Adafruit splash for 2 seconds, then draw the Pong court.

6

Play!

Press the UP and DOWN buttons to move your right-side paddle. The CPU controls the left paddle automatically. First to 9 points wins. After 5 seconds the game resets automatically.

🎮 Pro tip: For smoother control, replace the buttons with a potentiometer on A0 and use map(analogRead(A0), 0, 1023, 1, 41) to set player_y.

08 — Try It Online

Simulation Links

Wokwi Online Simulator

Wokwi is the recommended simulator for this project — it supports SSD1306 OLED, push buttons and the tone() function natively. No hardware needed.

🔌

Cirkit Designer Visual Simulation

Load the diagram.json above to get a fully-wired schematic view, then run the simulation inside Cirkit Designer.

▶ Open Cirkit Designer

09 — What You Gain

Learning Outcomes & Applications

🎓 Skills Learned

  • I2C communication protocol
  • SSD1306 OLED interfacing
  • Embedded graphics (Adafruit GFX)
  • Non-blocking timing with millis()
  • Collision detection logic
  • Digital INPUT_PULLUP buttons
  • tone() / noTone() sound output

🚀 Applications

  • Embedded game development
  • Graphics programming basics
  • OLED display learning projects
  • STEM / robotics education
  • Game physics simulation

10 — Keep Building

More MakeMindz Projects

Comments

Product Cards
Buddy Bot eBook
⭐ New 2026 Release
Build Your
Own Robot!
3D design, wiring &
Arduino coding.
Young inventors love it!
🖨️
3D Print
All parts
Wire it
Circuit guide
💻
Code it
Arduino IDE
🤖
Watch it
Walk & react
📋 Your Details
Enter your name
Valid 10-digit no.
Enter a valid email
Special Website Offer
₹499 300
🌍 International: $5 USD
One-time · Instant digital delivery
🔒 Secured by Razorpay · Your data is safe
📄 Download Free Sample Copy
🔒 Secured by Razorpay · Your data is safe
🍓
Raspberry Pi Pico Mastery
21 Projects
⚡ Launch Price — 80% OFF
Learn Pico
Build 21 Projects!
MicroPython · Wokwi
IoT · Certificate
Perfect for beginners!
🖥️
Wokwi
No hardware
🐍
MicroPy
From zero
🔨
21 Projects
IoT + sensors
📄
Certificate
Verified cert
📋 Your Details
Enter your name
Valid 10-digit no.
Enter a valid email
Special Launch Offer
₹999 200 80% OFF
🌍 International: $5 USD
One-time · Lifetime access · No subscription
🔒 Secured by Razorpay · UPI · Cards · NetBanking
🎉

You're in!

Payment successful! Your Buddy Bot eBook is ready. Time to build!

📖 Access Your eBook Now
🎉

Enrolled!

Payment successful! Lifetime access to all 21 Pico Projects is yours!

🍓 Go to My Course