DEV Community

Cover image for Building a Retro Console from Scratch (Part 2): Game Engine Foundations and Hardware Wrestling
Caleb García
Caleb García

Posted on

1

Building a Retro Console from Scratch (Part 2): Game Engine Foundations and Hardware Wrestling

📌 TL;DR:
I've built a complete game engine core for my Pico console - custom sprite system, input handling, and game loop - while fighting hardware limitations. This post covers:

  • The anatomy of a bare-metal game loop
  • Input handling
  • Sprite animation system
  • Hardware deep dive: Why my breadboard looks like a spider's nest

👉 For those who missed: Part 1

Hardware Odyssey: From Datasheets to Reality

The OLED That Wouldn't Speak

When I first wired the SH1106 display, I faced a blank screen of judgment. The problem? I assumed all I2C OLEDs were created equal.
Hardware Truth Bombs:

  1. Address Confusion: Some SH1106 use 0x3C, others 0x3D (mine needed a solder bridge change)
  2. Voltage Sag: Running both screen and Pico from USB caused intermittent resets

OLED Connections

Oled connections

*(War Story Sidebar: "The 20ms Init Delay Mystery" - Recap your datasheet discovery from Part 1)

Button Matrix: Input on a Budget

Buttons conections
Current Minimalist Setup

buttons = {
            'A': Pin(17, Pin.IN, Pin.PULL_DOWN),
            'B': Pin(18, Pin.IN, Pin.PULL_DOWN),
            'UP': Pin(19, Pin.IN, Pin.PULL_DOWN),
            'DOWN': Pin(20, Pin.IN, Pin.PULL_DOWN),
            'LEFT': Pin(21, Pin.IN, Pin.PULL_DOWN),
            'RIGHT': Pin(22, Pin.IN, Pin.PULL_DOWN)
        }
Enter fullscreen mode Exit fullscreen mode

why this matters

  • Pull-ups vs Pull-downs: Chose internal pull-ups to simplify wiring
  • Pin Selection: Avoided ADC-capable pins (GP26-28) for future audio use
  • Current Draw: Each button uses ~0.3mA when pressed

Software Architecture: Building a Console, Not Just Games

1. The Console Mentality

Unlike typical DIY projects that hardcode games, I'm architecting a true development ecosystem:

  • Cartridge System: Games live on separate files (/games/pong.py)
  • Engine API: Standardized methods games must implement

Why this matters?
This brings authentic Retro Experience by using swappable "cartridges" via flash memories, also a developer friendly through letting others create games without touching core code. Future Proof New games won't require firmware updates

2. Core Engine Components

The Game Loop: Heartbeat of the Console

class PicoEngine:
    def _init_(self, screen):
        self.objects = []
        self.input_handler = Input_Handler()
        self.target_fps = 30
        self.frame_duration = 1 / self.target_fps

    def loop(self):
        while True:
            frame_start = time.time()
            self.input_handler.scan()

            for obj in self.objects:
                obj.update(self.input_handler.state)

            self.draw()
            frame_time = time.time() - frame_start
            if frame_time < self.frame_duration:
Enter fullscreen mode Exit fullscreen mode

Key Features:

  • Fixed timestep for consistent gameplay
  • Frame budget tracking (logs dropped frames)
  • Clean separation between engine and game

Input System: Beyond Hardcoded Buttons

class Input_Handler:
    BUTTONS = {
            'A': Pin(17, Pin.IN, Pin.PULL_DOWN),
            'B': Pin(18, Pin.IN, Pin.PULL_DOWN),
            'UP': Pin(19, Pin.IN, Pin.PULL_DOWN),
            'DOWN': Pin(20, Pin.IN, Pin.PULL_DOWN),
            'LEFT': Pin(21, Pin.IN, Pin.PULL_DOWN),
            'RIGHT': Pin(22, Pin.IN, Pin.PULL_DOWN)
    }
    def _init_(self):
        self.state = {
            'A': False,
            #... the rest of the buttons
        }

    def scan(self):
        for button_name, button in self.BUTTONS.items():
            self.state[button_name] = button.value()
Enter fullscreen mode Exit fullscreen mode

Important Design Choices

  • Abstract Physical Layout: Games check input.state['a'] without knowing GPIO pins
  • Future Expansion: Can add joystick support later (maybe)

3. Sprites: Breathing Life into Pixels

1. Why Sprites Define Retro Gaming

In the golden age of consoles, sprites were magic:

  • Hardware-accelerated bitting on vintage systems
  • Memory constraints forced creative solutions
  • Limited palettes birthed iconic art styles

My Pico faces similar challenges with a maximum of 16 sprites and a VRAM of 1KB.

2. The Naked Sprite Class

class Sprite:
    def __init__(self, x, y, sprite, w, h):
        self.x = x
        self.y = y
        self._sprite_data = sprite
        self.width = w
        self.height = h

    def move(self, dx, dy):
        self.x += dx
        self.y += dy
Enter fullscreen mode Exit fullscreen mode

A little bit of Memory Math
A 16x16 sprite consumes 32 bytes (16x16 / 8). My entire VRAM fits: 1024/32 = 32 sprites... but i'll reserve half for UI/fonts.

3. Animation System

Image description

class Animated_Sprite(Sprite):
    def __init__(self, x, y, animations, default_animation, w, h, speed):
        self.animations = animations
        self.curr_animation = 'idle'
        self.frame_index = 0
        self.anim_speed = speed #fps(framse per step)
        self.counter = 0
        super().__init__( x, y, self.animations['idle'][0], w, h)

    def set_animation(self, name):
        if name != self.curr_animation:
            self.curr_animation = name
            self.frame_index = 0
            self.counter = 0
            self.update()

    def update(self):
        self.counter += 1
        if self.counter >= self.anim_speed:
            self.counter = 0
            self.frame_index = (self.frame_index + 1) % len(self.animations[self.curr_animation])
            self._update_frame()


    def _update_frame(self):
        self._sprite_data = self.animations[self.curr_animation][self.frame_index]

Enter fullscreen mode Exit fullscreen mode

🎮 The Pixel Promise Fulfilled

Image description

What Works Today:

✔️ Smooth Animation - 4-frame walk cycles at 30FPS

Where We Go From Here

The roadmap ahead looks exciting:

  1. Game Loop: Creating Game Objects.
  2. Audio Engine: PWM-based sound effects
  3. Cartridge System: SD card game loading

Join Me on This Journey

This is just the beginning. As I continue building:

  • Follow my progress on GitHub and Twitter
  • Try the code yourself - all projects are MIT licensed
  • Share your ideas in the comments below

Top comments (0)

Feature flag article image

Create a feature flag in your IDE in 5 minutes with LaunchDarkly’s MCP server 🏁

How to create, evaluate, and modify flags from within your IDE or AI client using natural language with LaunchDarkly's new MCP server. Follow along with this tutorial for step by step instructions.

Read full post

👋 Kindness is contagious

Explore this practical breakdown on DEV’s open platform, where developers from every background come together to push boundaries. No matter your experience, your viewpoint enriches the conversation.

Dropping a simple “thank you” or question in the comments goes a long way in supporting authors—your feedback helps ideas evolve.

At DEV, shared discovery drives progress and builds lasting bonds. If this post resonated, a quick nod of appreciation can make all the difference.

Okay