Raspberry Pi Pico Retro Gaming Console with OLED Display – 4 Classic Arcade Games

Raspberry Pi Pico Retro Gaming Console | Snake, Pong, Shooter, Breakout – Wokwi
🎮 Expert Level 🍓 Raspberry Pi Pico 🕹️ 4 Games 🟢 Wokwi

Retro Arcade Gaming Console
on Raspberry Pi Pico

Turn a Raspberry Pi Pico into a pocket arcade machine running Snake, Pong, Space Shooter, and Breakout — all on a 128×64 OLED display with real button controls, 8-bit sound effects, and MicroPython. Simulate everything in Wokwi first.

~30 min read 8 Components MicroPython 20 FPS game loop
🐍
Snake
D-Pad
🏓
Pong
UP / DOWN
🚀
Shooter
L/R + A
🧱
Breakout
LEFT / RIGHT
0Requirements

Hardware Components

🍓
Raspberry Pi Pico
RP2040, MicroPython
📺
SSD1306 OLED
0.96" 128×64, I2C 0x3C
🎮
6× Push Buttons
D-Pad + A + B
🔊
Active Buzzer
8-bit sound effects
〰️
6× 10kΩ Resistors
Button pull-ups (optional)
🔌
Breadboard + Wires
Prototyping setup
🐍
MicroPython Firmware
Flashed on Pico

All components are available in Wokwi — no physical hardware needed to get started. Internal pull-ups are used via MicroPython configuration.

1Wiring

Circuit Connections

📺 OLED SSD1306 (I2C)
SDAGP0
SCLGP1
VCC3.3V(OUT)
GNDGND.8
🎮 D-Pad Buttons (Active Low)
UPGP10 / GND.1
DOWNGP11 / GND.2
LEFTGP12 / GND.3
RIGHTGP13 / GND.4
🔴 Action Buttons
A (Action)GP14 / GND.5
B (Back/Menu)GP15 / GND.6
🔊 Buzzer
Positive (+)GP16
Negative (−)GND.7

All buttons use Pin.PULL_UP internally — no external resistors required.

🎮 Games

4 Classic Arcade Games

128×64 OLED
RETRO ARCADE ───────────── ▶Snake Pong Shooter Breakout
Game Menu
Game 01
🐍 Snake
Classic grid-based movement. Eat food to grow, avoid walls and your own tail.
D-Pad • +10pts/food
  • Grid logic
  • Self-collision detection
  • Random food generation
  • Direction control
Game 02
🏓 Pong
Arcade paddle ball. Keep the ball from passing your paddle to score points.
UP / DOWN • +1pt/rally
  • Ball physics
  • Paddle collision
  • Score increment
  • Velocity inversion
Game 03
🚀 Space Shooter
Destroy incoming enemies. 3 lives — survive as long as possible.
L/R + A (Shoot) • 3 lives
  • Projectile management
  • Enemy spawning
  • AABB hit detection
  • Life system
Game 04
🧱 Breakout
32 bricks, one ball. Clear all bricks without letting the ball fall.
LEFT / RIGHT • +5pts/brick
  • Brick grid management
  • Ball reflection physics
  • Win-state detection
  • Score scaling
⚙️ Design

Code Architecture

The project uses a clean state machine design with object-oriented game classes, running at 20 FPS (50ms sleep per frame).

Boot Splash
GameMenu
State: menu
Game Selected
A button
Gameplay
State: playing
B = Back
to menu
Classes
SSD1306_I2C
SnakeGame
PongGame
SpaceShooter
BreakoutGame
GameMenu
Key Functions
read_buttons()
button_pressed()
beep()
game.update()
game.draw()
oled.show()
Sound Patterns
"single"
"double"
"game_over"
2Wokwi Config

diagram.json

Paste this into Wokwi's diagram.json tab to auto-place and wire all components.

diagram.json
{
  "version": 1,
  "author": "Retro Gaming Console",
  "editor": "wokwi",
  "parts": [
    { "type": "wokwi-pi-pico", "id": "pico", "top": 0, "left": 0, "attrs": {} },
    {
      "type": "wokwi-ssd1306", "id": "oled1",
      "top": -137.3, "left": 213.1,
      "attrs": { "i2cAddress": "0x3C" }
    },
    {
      "type": "wokwi-pushbutton", "id": "btn_up",
      "top": -120, "left": -115.2,
      "attrs": { "color": "blue", "label": "UP" }
    },
    {
      "type": "wokwi-pushbutton", "id": "btn_down",
      "top": -60, "left": -115.2,
      "attrs": { "color": "blue", "label": "DOWN" }
    },
    {
      "type": "wokwi-pushbutton", "id": "btn_left",
      "top": -90, "left": -153.6,
      "attrs": { "color": "blue", "label": "LEFT" }
    },
    {
      "type": "wokwi-pushbutton", "id": "btn_right",
      "top": -90, "left": -76.8,
      "attrs": { "color": "blue", "label": "RIGHT" }
    },
    {
      "type": "wokwi-pushbutton", "id": "btn_a",
      "top": -90, "left": 57.6,
      "attrs": { "color": "red", "label": "A" }
    },
    {
      "type": "wokwi-pushbutton", "id": "btn_b",
      "top": -90, "left": 124.8,
      "attrs": { "color": "green", "label": "B" }
    },
    {
      "type": "wokwi-buzzer", "id": "buzzer1",
      "top": 120, "left": 100,
      "attrs": { "volume": "0.6" }
    }
  ],
  "connections": [
    [ "pico:GP0",       "oled1:SDA",      "green",   [ "v0" ] ],
    [ "pico:GP1",       "oled1:SCL",      "blue",    [ "v0" ] ],
    [ "pico:3V3(OUT)", "oled1:VCC",      "red",     [ "v0" ] ],
    [ "pico:GND.8",    "oled1:GND",      "black",   [ "v0" ] ],
    [ "pico:GP10",     "btn_up:1.l",    "blue",    [ "v0" ] ],
    [ "btn_up:2.l",   "pico:GND.1",    "black",   [ "v0" ] ],
    [ "pico:GP11",     "btn_down:1.l",  "blue",    [ "v0" ] ],
    [ "btn_down:2.l", "pico:GND.2",    "black",   [ "v0" ] ],
    [ "pico:GP12",     "btn_left:1.l",  "blue",    [ "v0" ] ],
    [ "btn_left:2.l", "pico:GND.3",    "black",   [ "v0" ] ],
    [ "pico:GP13",     "btn_right:1.l", "blue",    [ "v0" ] ],
    [ "btn_right:2.l","pico:GND.4",    "black",   [ "v0" ] ],
    [ "pico:GP14",     "btn_a:1.l",     "red",     [ "v0" ] ],
    [ "btn_a:2.l",    "pico:GND.5",    "black",   [ "v0" ] ],
    [ "pico:GP15",     "btn_b:1.l",     "green",   [ "v0" ] ],
    [ "btn_b:2.l",    "pico:GND.6",    "black",   [ "v0" ] ],
    [ "pico:GP16",     "buzzer1:1",     "magenta", [ "v0" ] ],
    [ "buzzer1:2",    "pico:GND.7",    "black",   [ "v0" ] ]
  ],
  "dependencies": {}
}
3Code

MicroPython Code (main.py)

Save this as main.py on your Pico, or paste into Wokwi's code editor.

main.py — MicroPython
from machine import Pin, I2C
import time
import framebuf
import random

# ── SSD1306 OLED Driver (128×64) ──────────────────────────────
class SSD1306_I2C:
    def __init__(self, width, height, i2c, addr=0x3C):
        self.i2c = i2c;  self.addr = addr
        self.width = width;  self.height = height
        self.pages = height // 8
        self.buffer = bytearray(self.pages * width)
        self.framebuf = framebuf.FrameBuffer(
            self.buffer, width, height, framebuf.MONO_VLSB)
        self.init_display()

    def init_display(self):
        for cmd in (
            0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40,
            0x8D, 0x14, 0x20, 0x00, 0xA1, 0xC8, 0xDA, 0x12,
            0x81, 0xCF, 0xD9, 0xF1, 0xDB, 0x40, 0xA4, 0xA6, 0xAF
        ):
            self.write_cmd(cmd)
        self.fill(0);  self.show()

    def write_cmd(self, cmd):
        self.i2c.writeto(self.addr, bytearray([0x00, cmd]))
    def write_data(self, buf):
        self.i2c.writeto(self.addr, b'\x40' + buf)
    def show(self):
        for page in range(self.pages):
            self.write_cmd(0xB0 + page)
            self.write_cmd(0x00);  self.write_cmd(0x10)
            self.write_data(self.buffer[page*self.width:(page+1)*self.width])
    def fill(self, c):       self.framebuf.fill(c)
    def pixel(self,x,y,c):   self.framebuf.pixel(x,y,c)
    def text(self,s,x,y,c=1):self.framebuf.text(s,x,y,c)
    def rect(self,x,y,w,h,c): self.framebuf.rect(x,y,w,h,c)
    def fill_rect(self,x,y,w,h,c): self.framebuf.fill_rect(x,y,w,h,c)
    def line(self,x1,y1,x2,y2,c): self.framebuf.line(x1,y1,x2,y2,c)

# ── Hardware Init ─────────────────────────────────────────────
i2c  = I2C(0, scl=Pin(1), sda=Pin(0), freq=400000)
oled = SSD1306_I2C(128, 64, i2c)

btn_up    = Pin(10, Pin.IN, Pin.PULL_UP)
btn_down  = Pin(11, Pin.IN, Pin.PULL_UP)
btn_left  = Pin(12, Pin.IN, Pin.PULL_UP)
btn_right = Pin(13, Pin.IN, Pin.PULL_UP)
btn_a     = Pin(14, Pin.IN, Pin.PULL_UP)
btn_b     = Pin(15, Pin.IN, Pin.PULL_UP)
buzzer    = Pin(16, Pin.OUT)

# ── Sound Effects ─────────────────────────────────────────────
def beep(duration=0.05, pattern="single"):
    if pattern == "single":
        buzzer.on();  time.sleep(duration);  buzzer.off()
    elif pattern == "double":
        for _ in range(2):
            buzzer.on();  time.sleep(0.05)
            buzzer.off(); time.sleep(0.05)
    elif pattern == "game_over":
        for f in [0.1, 0.1, 0.15]:
            buzzer.on();  time.sleep(f)
            buzzer.off(); time.sleep(0.05)

# ── Button Debouncing ─────────────────────────────────────────
last_press    = {"up":0, "down":0, "left":0, "right":0, "a":0, "b":0}
debounce_time = 150  # ms

def button_pressed(name):
    now = time.ticks_ms()
    if time.ticks_diff(now, last_press[name]) > debounce_time:
        last_press[name] = now;  return True
    return False

def read_buttons():
    return {
        "up":    btn_up.value()    == 0,
        "down":  btn_down.value()  == 0,
        "left":  btn_left.value()  == 0,
        "right": btn_right.value() == 0,
        "a":     btn_a.value()     == 0,
        "b":     btn_b.value()     == 0,
    }

# ── SNAKE ─────────────────────────────────────────────────────
class SnakeGame:
    def __init__(self): self.reset()
    def reset(self):
        self.snake = [[64,32],[60,32],[56,32]]
        self.direction = [4,0]
        self.food = [random.randint(0,30)*4, random.randint(0,14)*4]
        self.score = 0;  self.game_over = False

    def update(self, buttons):
        if self.game_over: return
        dx, dy = self.direction
        if buttons["up"]    and dy == 0: self.direction = [0,-4]
        elif buttons["down"]  and dy == 0: self.direction = [0,4]
        elif buttons["left"]  and dx == 0: self.direction = [-4,0]
        elif buttons["right"] and dx == 0: self.direction = [4,0]
        head = [self.snake[0][0]+self.direction[0], self.snake[0][1]+self.direction[1]]
        if head[0] < 0 or head[0] >= 128 or head[1] < 0 or head[1] >= 64:
            self.game_over = True;  beep(0.1, "game_over");  return
        if head in self.snake:
            self.game_over = True;  beep(0.1, "game_over");  return
        self.snake.insert(0, head)
        if head == self.food:
            self.score += 10;  beep(0.05, "double")
            self.food = [random.randint(0,30)*4, random.randint(0,14)*4]
        else: self.snake.pop()

    def draw(self):
        oled.fill(0)
        for s in self.snake: oled.fill_rect(s[0],s[1],4,4,1)
        oled.fill_rect(self.food[0],self.food[1],4,4,1)
        oled.text(f"Score:{self.score}",0,0)
        if self.game_over:
            oled.fill_rect(20,25,88,20,0);  oled.rect(20,25,88,20,1)
            oled.text("GAME OVER",30,30)
        oled.show()

# ── PONG ──────────────────────────────────────────────────────
class PongGame:
    def __init__(self): self.reset()
    def reset(self):
        self.paddle_y = 24;  self.ball_x = 64;  self.ball_y = 32
        self.ball_dx = 2;  self.ball_dy = 1
        self.score = 0;  self.game_over = False

    def update(self, buttons):
        if self.game_over: return
        if buttons["up"]   and self.paddle_y > 0:  self.paddle_y -= 3
        if buttons["down"] and self.paddle_y < 48: self.paddle_y += 3
        self.ball_x += self.ball_dx;  self.ball_y += self.ball_dy
        if self.ball_y <= 0 or self.ball_y >= 62:
            self.ball_dy *= -1;  beep(0.03)
        if self.ball_x <= 8 and self.paddle_y <= self.ball_y <= self.paddle_y+16:
            self.ball_dx *= -1;  self.score += 1;  beep(0.05)
        if self.ball_x < 0:
            self.game_over = True;  beep(0.1, "game_over")
        if self.ball_x >= 126: self.ball_dx *= -1

    def draw(self):
        oled.fill(0)
        oled.fill_rect(2,self.paddle_y,4,16,1)
        oled.fill_rect(self.ball_x,self.ball_y,2,2,1)
        oled.text(f"Score:{self.score}",40,0)
        if self.game_over:
            oled.fill_rect(20,25,88,20,0);  oled.rect(20,25,88,20,1)
            oled.text("GAME OVER",30,30)
        oled.show()

# ── SPACE SHOOTER ─────────────────────────────────────────────
class SpaceShooter:
    def __init__(self): self.reset()
    def reset(self):
        self.player_x = 60;  self.bullets = [];  self.score = 0
        self.enemies  = [[random.randint(0,120), random.randint(-30,-10)] for _ in range(3)]
        self.lives = 3;  self.game_over = False

    def update(self, buttons):
        if self.game_over: return
        if buttons["left"]  and self.player_x > 0:   self.player_x -= 3
        if buttons["right"] and self.player_x < 120: self.player_x += 3
        if buttons["a"] and button_pressed("a"):
            self.bullets.append([self.player_x+3, 50]);  beep(0.03)
        for b in self.bullets[:]:
            b[1] -= 4
            if b[1] < 0: self.bullets.remove(b)
        for e in self.enemies:
            e[1] += 1
            if e[1] > 64:
                e[0] = random.randint(0,120);  e[1] = random.randint(-30,-10)
                self.lives -= 1;  beep(0.1)
                if self.lives <= 0:
                    self.game_over = True;  beep(0.1, "game_over")
        for b in self.bullets[:]:
            for e in self.enemies[:]:
                if abs(b[0]-e[0]) < 6 and abs(b[1]-e[1]) < 6:
                    self.bullets.remove(b)
                    e[0] = random.randint(0,120);  e[1] = random.randint(-30,-10)
                    self.score += 10;  beep(0.05, "double");  break

    def draw(self):
        oled.fill(0)
        oled.fill_rect(self.player_x,54,8,6,1);  oled.pixel(self.player_x+4,52,1)
        for b in self.bullets: oled.fill_rect(b[0],b[1],2,4,1)
        for e in self.enemies:  oled.fill_rect(e[0],e[1],6,6,1)
        oled.text(f"S:{self.score}",0,0);  oled.text(f"L:{self.lives}",100,0)
        if self.game_over:
            oled.fill_rect(20,25,88,20,0);  oled.rect(20,25,88,20,1)
            oled.text("GAME OVER",30,30)
        oled.show()

# ── BREAKOUT ──────────────────────────────────────────────────
class BreakoutGame:
    def __init__(self): self.reset()
    def reset(self):
        self.paddle_x = 52;  self.ball_x = 64;  self.ball_y = 50
        self.ball_dx = 2;  self.ball_dy = -2
        self.bricks = [[x*16, y*6+10] for x in range(8) for y in range(4)]
        self.score = 0;  self.game_over = False;  self.won = False

    def update(self, buttons):
        if self.game_over or self.won: return
        if buttons["left"]  and self.paddle_x > 0:   self.paddle_x -= 4
        if buttons["right"] and self.paddle_x < 104: self.paddle_x += 4
        self.ball_x += self.ball_dx;  self.ball_y += self.ball_dy
        if self.ball_x <= 0 or self.ball_x >= 126:
            self.ball_dx *= -1;  beep(0.03)
        if self.ball_y <= 0:
            self.ball_dy *= -1;  beep(0.03)
        if (self.paddle_x <= self.ball_x <= self.paddle_x+24 and
                58 <= self.ball_y <= 60):
            self.ball_dy *= -1;  beep(0.05)
        if self.ball_y > 64:
            self.game_over = True;  beep(0.1, "game_over")
        for brick in self.bricks[:]:
            if (brick[0] <= self.ball_x <= brick[0]+14 and
                    brick[1] <= self.ball_y <= brick[1]+4):
                self.bricks.remove(brick);  self.ball_dy *= -1
                self.score += 5;  beep(0.04)
                if not self.bricks:
                    self.won = True;  beep(0.1, "double")
                break

    def draw(self):
        oled.fill(0)
        oled.fill_rect(self.paddle_x,58,24,4,1)
        oled.fill_rect(self.ball_x,self.ball_y,2,2,1)
        for b in self.bricks: oled.rect(b[0],b[1],14,4,1)
        oled.text(f"Score:{self.score}",0,0)
        if self.game_over:
            oled.fill_rect(20,25,88,20,0);  oled.rect(20,25,88,20,1)
            oled.text("GAME OVER",30,30)
        elif self.won:
            oled.fill_rect(20,25,88,20,0);  oled.rect(20,25,88,20,1)
            oled.text("YOU WIN!",35,30)
        oled.show()

# ── GAME MENU ─────────────────────────────────────────────────
class GameMenu:
    def __init__(self):
        self.games = ["Snake", "Pong", "Shooter", "Breakout"]
        self.selected = 0

    def update(self, buttons):
        if buttons["up"]   and button_pressed("up"):
            self.selected = (self.selected - 1) % len(self.games);  beep(0.03)
        if buttons["down"] and button_pressed("down"):
            self.selected = (self.selected + 1) % len(self.games);  beep(0.03)
        if buttons["a"] and button_pressed("a"):
            beep(0.05, "double");  return self.selected
        return None

    def draw(self):
        oled.fill(0);  oled.rect(0,0,128,64,1)
        oled.text("RETRO ARCADE",20,5);  oled.line(0,15,128,15,1)
        for i, game in enumerate(self.games):
            y = 22 + i * 10
            oled.text(f"{'>' if i==self.selected else ' '}{game}", 15, y)
        oled.show()

# ── MAIN LOOP ─────────────────────────────────────────────────
print("=" * 40);  print("🎮 RETRO GAMING CONSOLE");  print("=" * 40)
print("\nD-Pad: UP/DOWN/LEFT/RIGHT | A=Select | B=Back\n")

oled.fill(0)
oled.text("RETRO ARCADE",25,20);  oled.text("Loading...",30,35)
oled.show();  time.sleep(1)

menu = GameMenu();  current_game = None;  game_mode = "menu"

try:
    while True:
        buttons = read_buttons()
        if game_mode == "menu":
            selected = menu.update(buttons);  menu.draw()
            if selected is not None:
                current_game = [SnakeGame, PongGame, SpaceShooter, BreakoutGame][selected]()
                game_mode = "playing"
                print(f"🎮 Starting {menu.games[selected]}...")
        elif game_mode == "playing":
            current_game.update(buttons);  current_game.draw()
            if buttons["b"] and button_pressed("b"):
                game_mode = "menu";  beep(0.05)
                print("📋 Back to menu...")
        time.sleep(0.05)  # 20 FPS

except KeyboardInterrupt:
    print("\n🛑 Shutting down...")
    oled.fill(0);  oled.text("GAME OVER",35,28);  oled.show()
    buzzer.off()
📚 Skills

What You'll Learn

Game loop architecture
Frame rate control (20 FPS)
AABB collision detection
OOP in MicroPython
Pixel frame buffer rendering
State machine design
Input debouncing
GPIO sound generation
Memory-efficient coding
I2C display driving
Embedded UI/UX
Ball physics & reflection
4Run

Run the Simulation

Paste diagram.json

Copy into Wokwi's diagram tab to auto-wire all components.

Paste main.py

Copy the full code into Wokwi's code editor.

Click ▶ Play

Press the green start button — the OLED boot animation appears.

Navigate Menu

UP/DOWN to select a game, A to launch it.

Play Games

Use the 6 virtual buttons. B returns to the main menu.

More Arduino & Pico Projects

🟢 Beginner — Foundation & Basics
🔵 Intermediate — Displays & Sensors
🔴 Advanced — Motors & Actuators
🟣 Expert — Security, IoT & Gaming

© 2026 MakeMindz · Raspberry Pi Pico & Arduino Projects for STEM Learners & Makers

Comments

try for free