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 16×16 sprite consumes 32 bytes (16×16 / 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



Source link