Skip to content

Architecture Document - PixelRoot32 Game Engine

Executive Summary

PixelRoot32 is a lightweight, modular 2D game engine written in C++17, designed primarily for ESP32 microcontrollers, with a native simulation layer for PC (SDL2) that enables rapid development without hardware.

The engine follows a scene-based architecture inspired by Godot Engine, making it intuitive for developers familiar with modern game development workflows.


1. Architecture Overview

1.1 Design Philosophy

  • Modularity: Each subsystem can be used independently
  • Portability: Same code for ESP32 and PC (SDL2)
  • Performance: Optimized for resource-constrained hardware
  • Extensibility: Plugin architecture for drivers and backends
  • Modern C++: Leverages C++17 features (smart pointers, string_view) for safety and efficiency

What Does "Modularity" Mean in PixelRoot32?

Modularity means that each main subsystem has low coupling and can be instantiated, tested, and used in isolation, without depending on other subsystems. This allows:

  • Independent testing: Each module can be unit tested
  • Selective usage: Use only the modules you need
  • Easy replacement: Change implementations without affecting the rest of the code

Concrete examples of independence:

// 1. AudioEngine works without Renderer or SceneManager
AudioConfig audioConfig;
AudioEngine audio(audioConfig);
audio.init();
audio.playEvent({WaveType::PULSE, 440.0f, 0.5f, 0.8f});

// 2. Renderer can be used without Audio or Input
DisplayConfig displayConfig;
Renderer renderer(displayConfig);
renderer.init();
renderer.beginFrame();
renderer.drawSprite(sprite, 10, 10, Color::White);
renderer.endFrame();

// 3. InputManager is autonomous
InputConfig inputConfig;
InputManager input(inputConfig);
input.init();
input.update(deltaTime);
if (input.isButtonPressed(0)) { /* ... */ }

// 4. CollisionSystem is optional per scene
Scene scene;
// You can update physics only if you need it
scene.collisionSystem.update();

// 5. Interchangeable drivers without changing game code
// Same code works with TFT_eSPI_Drawer, U8G2_Drawer, or SDL2_Drawer

Note: Engine is the only component with tight coupling (orchestrates everything), but each subsystem can exist and function independently.

1.2 Main Architectural Features

  • Stack-based Scene-Entity system
  • Rendering with logical resolution independent of physical resolution
  • NES-style 4-channel audio subsystem
  • UI system with automatic layouts
  • "Flat Solver" physics with specialized Actor types (StaticActor, RigidActor)
  • Circular and AABB collision support
  • Multi-platform support through driver abstraction

2. Layer Hierarchy Diagram

Architecture Diagram


3. Detailed Layer Description

3.1 LAYER 0: Hardware Layer

Responsibility: Underlying physical hardware.

Components:

  • ESP32/ESP32-S3: Main microcontrollers
  • Displays: ST7789, ST7735, SSD1306 (OLED), SH1106
  • Audio: Internal DAC, I2S with PAM8302A amplifiers
  • Input: Physical buttons connected to GPIOs
  • PC/Native: Simulation via SDL2 on Windows/Linux/macOS

3.2 LAYER 1: Driver Layer

Responsibility: Platform-specific hardware abstraction.

Design Patterns: Concrete implementation of abstractions

ESP32 Drivers:

Driver File Description
TFT_eSPI_Drawer drivers/esp32/TFT_eSPI_Drawer.cpp TFT display driver (ST7789, ST7735)
U8G2_Drawer drivers/esp32/U8G2_Drawer.cpp Monochrome OLED driver (SSD1306, SH1106)
ESP32_I2S_AudioBackend drivers/esp32/ESP32_I2S_AudioBackend.cpp I2S audio backend
ESP32_DAC_AudioBackend drivers/esp32/ESP32_DAC_AudioBackend.cpp Internal DAC audio backend
ESP32AudioScheduler audio/ESP32AudioScheduler.cpp Multi-core audio scheduler

Native (PC) Drivers:

Driver File Description
SDL2_Drawer drivers/native/SDL2_Drawer.cpp SDL2 graphics simulation
SDL2_AudioBackend drivers/native/SDL2_AudioBackend.cpp SDL2 audio backend
NativeAudioScheduler audio/NativeAudioScheduler.cpp Native scheduler
MockArduino platforms/mock/MockArduino.cpp Arduino API emulation

3.3 LAYER 2: Abstraction Layer

Responsibility: Abstract interfaces that decouple subsystems from concrete implementations.

Design Patterns:

  • Bridge Pattern: DrawSurface decouples Renderer from specific drivers
  • Strategy Pattern: AudioScheduler allows different scheduling implementations

Main Components:

DrawSurface (Bridge Pattern)

class DrawSurface {
    virtual void init() = 0;
    virtual void drawPixel(int x, int y, uint16_t color) = 0;
    virtual void drawLine(int x1, int y1, int x2, int y2, uint16_t color) = 0;
    virtual void sendBuffer() = 0;
    // ... more drawing methods
};

AudioScheduler (Strategy Pattern)

class AudioScheduler {
    virtual void init() = 0;
    virtual void submitCommand(const AudioCommand& cmd) = 0;
    virtual void generateSamples(int16_t* stream, int length) = 0;
};

PlatformCapabilities

Structure that detects and exposes hardware capabilities:

  • hasDualCore: Multi-core support
  • audioCoreId: Recommended core for audio
  • mainCoreId: Recommended core for game loop

3.4 LAYER 3: System Layer

Responsibility: Game engine subsystems that implement high-level functionality.

3.4.1 Renderer

Files: include/graphics/Renderer.h, src/graphics/Renderer.cpp

Responsibility: High-level rendering system that abstracts graphics operations.

Features:

  • Logical resolution independent of physical resolution
  • Support for 1bpp, 2bpp, 4bpp sprites
  • Sprite animation system
  • Tilemaps with viewport culling
  • Native bitmap font system
  • Render contexts for dual palettes

Main API:

class Renderer {
    void beginFrame();
    void endFrame();
    void drawSprite(const Sprite& sprite, int x, int y, Color color);
    void drawText(std::string_view text, int x, int y, Color color, uint8_t size);
    void drawTileMap(const TileMap& map, int originX, int originY);
    void setDisplaySize(int w, int h);
    void setDisplayOffset(int x, int y);
};

3.4.2 InputManager

Files: include/input/InputManager.h, src/input/InputManager.cpp

Responsibility: Input management from physical buttons or keyboard (PC).

Features:

  • Debouncing support
  • States: Pressed, Released, Down, Clicked
  • Configurable via InputConfig
  • Hardware abstraction through polling

Button States:

  • isButtonPressed(): UP → DOWN transition
  • isButtonReleased(): DOWN → UP transition
  • isButtonDown(): Current DOWN state
  • isButtonClicked(): Complete click

3.4.3 AudioEngine

Files: include/audio/AudioEngine.h, src/audio/AudioEngine.cpp

Responsibility: NES-style 4-channel audio system.

Audio Architecture:

AudioEngine (Facade)
    └── AudioScheduler (Strategy)
            ├── AudioCommandQueue
            ├── Channel Generators (Pulse, Triangle, Noise)
            └── Mixer with LUT

Wave Types:

  • PULSE: Square wave with variable duty cycle
  • TRIANGLE: Triangle wave
  • NOISE: Pseudo-random noise

Components:

  • AudioCommandQueue: Thread-safe command queue
  • MusicPlayer: Music sequencing system
  • AudioMixerLUT: Optimized mixer with lookup tables

3.4.4 CollisionSystem

Files: include/physics/CollisionSystem.h, src/physics/CollisionSystem.cpp

Responsibility: High-performance physics engine (Flat Solver) for 2D collisions.

Features:

  • Solver: Impulse-based velocity solver with Baumgarte stabilization
  • Shapes: AABB (Box) and Circle collision support
  • Optimization: Spatial Grid (Broadphase) for $O(1)$ lookup
  • Pipeline: Detect → Velocity → Position → Penetration
  • Collision Layers: Bitmask-based filtering

Collision Layers:

enum DefaultLayers {
    kNone = 0,
    kPlayer = 1 << 0,
    kEnemy = 1 << 1,
    kProjectile = 1 << 2,
    kWall = 1 << 3,
    // ... up to 16 layers
};

3.4.5 UI System

Files: include/graphics/ui/*.h, src/graphics/ui/*.cpp

Responsibility: User interface system with automatic layouts.

Class Hierarchy:

Entity
└── UIElement
    ├── UILabel
    ├── UIButton
    ├── UICheckbox
    └── UIPanel
        └── UILayout
            ├── UIHorizontalLayout
            ├── UIVerticalLayout
            ├── UIGridLayout
            ├── UIAnchorLayout
            └── UIPaddingContainer

Available Layouts:

  • UIHorizontalLayout: Horizontal arrangement
  • UIVerticalLayout: Vertical arrangement
  • UIGridLayout: Grid arrangement
  • UIAnchorLayout: Edge anchoring
  • UIPaddingContainer: Internal margins

3.4.6 Particle System

Files: include/graphics/particles/*.h, src/graphics/particles/*.cpp

Components:

  • Particle: Individual particle with position, velocity, life
  • ParticleEmitter: Configurable emitter with presets
  • ParticleConfig: Emission configuration

3.4.7 Camera2D

Files: include/graphics/Camera2D.h, src/graphics/Camera2D.cpp

Responsibility: 2D camera with viewport transformations.

Features:

  • Position and zoom
  • Automatic offset for Renderer
  • Support for fixed-position UI elements

3.4.8 Math Policy Layer

Files: include/math/Scalar.h, include/math/Fixed16.h, include/math/MathUtil.h

Responsibility: Platform-agnostic numerical abstraction layer.

Features:

  • Automatic Type Selection: Selects float for FPU-capable platforms (ESP32, S3) and Fixed16 for integer-only platforms (ESP32-C3, S2).
  • Unified API: Provides a consistent Scalar type and MathUtil functions regardless of the underlying representation.
  • Performance Optimization: Ensures optimal performance on all supported hardware without code changes.

Components:

  • Scalar: Type alias (float or Fixed16).
  • Fixed16: 16.16 fixed-point implementation.
  • MathUtil: Mathematical helper functions (abs, min, max, sqrt, etc.) compatible with Scalar.

3.5 LAYER 4: Scene Layer

Responsibility: Game scene and entity management.

3.5.1 Engine

Files: include/core/Engine.h, src/core/Engine.cpp

Responsibility: Central class that orchestrates all subsystems.

Game Loop:

void Engine::run() {
    while (true) {
        // 1. Calculate delta time
        deltaTime = currentMillis - previousMillis;

        // 2. Update
        update();

        // 3. Draw
        draw();
    }
}

void Engine::update() {
    inputManager.update(deltaTime);
    sceneManager.update(deltaTime);
}

void Engine::draw() {
    renderer.beginFrame();
    sceneManager.draw(renderer);
    renderer.endFrame();
}

Managed Subsystems:

  • SceneManager: Scene stack
  • Renderer: Graphics system
  • InputManager: User input
  • AudioEngine: Audio system
  • MusicPlayer: Music player
  • PlatformCapabilities: Hardware capabilities (pixelroot32::platforms)

3.5.2 SceneManager

Files: include/core/SceneManager.h, src/core/SceneManager.cpp

Responsibility: Scene stack management (push/pop).

Operations:

  • setCurrentScene(): Replace current scene
  • pushScene(): Push new scene (pauses previous)
  • popScene(): Pop scene (resumes previous)

Scene Stack:

Scene* sceneStack[MAX_SCENES];  // Maximum 5 scenes by default
int sceneCount;

3.5.3 Scene

Files: include/core/Scene.h, src/core/Scene.cpp

Responsibility: Entity container representing a level or screen.

Memory Management: The Scene follows a non-owning model for entities. When you call addEntity(Entity*), the scene stores a reference to the entity but does not take ownership. - You are responsible for the entity's lifetime (typically using std::unique_ptr in your Scene subclass). - The Scene will NOT delete entities when it is destroyed or when clearEntities() is called.

Features:

  • Entity array (MAX_ENTITIES = 32)
  • Render layer system (MAX_LAYERS = 3)
  • Integrated CollisionSystem
  • Viewport culling
  • Optional: SceneArena for custom allocators

Lifecycle:

virtual void init();                    // When entering scene
virtual void update(unsigned long dt);  // Every frame
virtual void draw(Renderer& r);         // Every frame

3.5.4 Entity

Files: include/core/Entity.h

Responsibility: Abstract base class for all game objects.

Properties:

  • Position (x, y)
  • Dimensions (width, height)
  • EntityType: GENERIC, ACTOR, UI_ELEMENT
  • renderLayer: Render layer (0-255)
  • isVisible: Visibility control
  • isEnabled: Update control

Virtual Methods:

virtual void update(unsigned long deltaTime) = 0;
virtual void draw(Renderer& renderer) = 0;

3.5.5 Actor

Files: include/core/Actor.h

Responsibility: Entity with physical collision capabilities.

Features:

  • Inherits from Entity
  • CollisionLayer layer: Own collision layer
  • CollisionLayer mask: Layers it collides with
  • getHitBox(): Gets bounding box for collision
  • onCollision(Actor* other): Collision callback

3.6 LAYER 5: Game Layer

Responsibility: Game-specific code implemented by the user.

Implementation Example:

class Player : public Actor {
public:
    void update(unsigned long deltaTime) override {
        // Movement logic
        if (engine.getInputManager().isButtonPressed(BTN_A)) {
            jump();
        }
    }

    void draw(Renderer& r) override {
        r.drawSprite(playerSprite, x, y, Color::White);
    }

    void onCollision(Actor* other) override {
        if (other->isInLayer(Layers::kEnemy)) {
            takeDamage();
        }
    }
};

class GameScene : public Scene {
    std::unique_ptr<Player> player;

public:
    void init() override {
        player = std::make_unique<Player>(100, 100, 16, 16);
        addEntity(player.get());
    }
};

4. Data Flow and Dependencies

4.1 Game Loop Flow

┌──────────┐     ┌──────────────┐     ┌──────────────┐     ┌──────────┐
│   Init   │────▶│  Game Loop   │────▶│    Exit      │────▶│ Cleanup  │
└──────────┘     └──────────────┘     └──────────────┘     └──────────┘
         ┌──────────────┼──────────────┐
         ▼              ▼              ▼
   ┌──────────┐   ┌──────────┐   ┌──────────┐
   │  Input   │   │  Update  │   │   Draw   │
   │  Poll    │   │  Logic   │   │  Render  │
   └──────────┘   └──────────┘   └──────────┘
         ┌──────────────┼──────────────┐
         ▼              ▼              ▼
   ┌──────────┐   ┌──────────┐   ┌──────────┐
   │  Audio   │   │ Physics  │   │   UI     │
   │ Generate │   │  Update  │   │  Draw    │
   └──────────┘   └──────────┘   └──────────┘

4.2 Module Dependencies

Engine
├── SceneManager
│   └── Scene
│       ├── Entity
│       │   ├── Actor
│       │   └── UIElement
│       └── CollisionSystem
├── Renderer
│   ├── DrawSurface (abstract)
│   │   ├── TFT_eSPI_Drawer
│   │   ├── U8G2_Drawer

6. Performance Optimization Strategies

To achieve stable 60 FPS on microcontrollers, the engine implements several low-level strategies:

6.1 Rendering Pipeline (v1.0.0)

  1. Independent Resolution Scaling: Rendering at low logical resolutions (e.g., 128x128) and scaling to physical hardware.
  2. Fast-Path Kernels: Specialized routines for 1:1 and 2x integer scaling.
    • OLED (U8G2): Horizontal expansion via 16-entry Bit-Expansion Lookup Tables.
    • TFT (TFT_eSPI): Vertical duplication via 32-bit register writes and optimized memcpy.
  3. DMA Pipelining: Double-buffering for DMA transfers. While DMA sent the current block, the CPU calculates the next one, maximizing the 40MHz SPI bus throughput.
  4. I2C 1MHz Support: Bus overclocking for monochromatic OLEDs, doubling framerate from 30 to 60 FPS.

6.2 Execution & Memory

  1. IRAM-Cached Functions: Critical rendering and math functions stay in Internal RAM to avoid Flash latency.
  2. Multi-Core Audio: ESP32 core 0 handles sample generation, while core 1 runs the game logic.
  3. Static Allocation: Subsystems are pre-allocated in init() to avoid heap fragmentation during gameplay.
  4. Moving toward C++17: Using std::unique_ptr and std::string_view for safer and faster memory/string handling.

PixelRoot32 - Performance Driven Architecture