README.md

April 10, 2026 · View on GitHub

pico-game-engine

A lightweight C++ game engine for embedded devices with LCD displays.


About

Pico Game Engine provides a complete framework for building games on microcontrollers such as the Raspberry Pi Pico or any embedded platform with an LCD and input controls. It supports both 2D sprite-based rendering and a software 3D rasterizer with filled triangles, back-face culling, and depth sorting — all without requiring a GPU or standard library allocator.


Connect Online


Features

  • 2D sprite rendering with 8-bit palette and 16-bit RGB565 bitmap support
  • Software 3D rasterizer with perspective projection, back-face culling, and painter's-algorithm depth sorting
  • Pre-built 3D shapes: humanoid, tree, house, pillar, cube, cylinder, sphere, wall, and triangular prism
  • First-person and third-person camera modes
  • Entity-component architecture with typed states, RPG stats, and AABB collision detection
  • Multi-level support with level switching at runtime
  • Blocking and non-blocking (async tick) game loop modes for bare-metal or RTOS environments
  • Fully macro-driven platform abstraction — wire up any LCD driver without modifying engine code
  • Optional file-backed image loading via a configurable storage macro
  • No STL dependency; lightweight callback system without std::function

Architecture

GameEngine            - runs the game loop (blocking or single-tick async)
  └── Game            - top-level state: levels, camera, input, colors, scroll offset
        └── Level     - entity container with update, collision, and render loops
              └── Entity  - game object with position, sprites, callbacks, and stats
                    ├── Image      - 2D bitmap sprite (in-memory or file-backed)
                    └── Sprite3D   - 3D mesh of up to 64 Triangle3D instances

Draw                  - thin C++ wrapper over all LCD macros
Camera                - view parameters for first-person or third-person perspective
Vector                - 3D math primitive (x, y, z) with rotation and scale helpers
Triangle3D            - single colored triangle with depth and wireframe support
callback.hpp          - lightweight function-pointer + void* callback structs
engine_config.hpp     - all platform-specific macros (LCD, memory, font, storage)

Configuration

All platform wiring is done via #define macros in engine_config.hpp. No engine source files need to be modified.

CategoryKey Macros
MemoryENGINE_MEM_MALLOC, ENGINE_MEM_FREE, ENGINE_MEM_NEW, ENGINE_MEM_DELETE
DelayENGINE_DELAY_INCLUDE, ENGINE_DELAY_MS(ms) (required)
FontENGINE_FONT_INCLUDE, ENGINE_FONT_SIZE, ENGINE_FONT_DEFAULT
LCDENGINE_LCD_INIT, ENGINE_LCD_CLEAR, ENGINE_LCD_PIXEL, ENGINE_LCD_LINE, ENGINE_LCD_TRIANGLE, ENGINE_LCD_FILL_TRIANGLE, ENGINE_LCD_BLIT, ENGINE_LCD_BLIT_16BIT, ENGINE_LCD_TEXT, ENGINE_LCD_SWAP, and more
StorageENGINE_STORAGE_INCLUDE, ENGINE_STORAGE_READ (optional, for file-backed images)

ENGINE_LCD_SWAP is optional and enables double-buffered rendering when supported by the display driver.


Core Classes

GameEngine

The top-level runner. Accepts a Game* and a target FPS.

MethodDescription
run()Blocking game loop: start, update, render, delay, stop
runAsync(shouldDelay)Single tick for use with RTOS or cooperative schedulers
updateGameInput(uint8_t)Injects button state into the game
stop()Stops the game, clears the screen, frees memory

Game

Top-level state. Holds up to 10 levels, a Draw instance, a Camera, the current input value, scroll offset (pos), and world size.

MethodDescription
level_add(level)Registers a level
level_switch(name)Stops the current level, starts the named one
setCamera(Camera)Copies camera parameters into the engine
clamp(value, min, max)Utility to bound a value

Level

A scene that owns a dynamic array of entities. Handles the full per-frame pipeline: update all entities, run AABB collision detection between all active pairs, then render.

MethodDescription
entity_add(entity)Adds an entity and calls its start callback
entity_remove(entity)Calls stop, removes from array, and frees non-player entities
clear()Stops and removes all non-player entities
is_collision(a, b)Returns true if two entities' bounding boxes overlap
collision_list(entity, count)Returns all entities currently colliding with the given entity

Entity

A game object with position, size, 2D sprites, an optional 3D mesh, callbacks, and RPG stats.

Types: ENTITY_PLAYER, ENTITY_ENEMY, ENTITY_NPC, ENTITY_ICON, ENTITY_3D_SPRITE

States: ENTITY_IDLE, ENTITY_MOVING, ENTITY_ATTACKING, ENTITY_ATTACKED, ENTITY_DEAD

Callbacks (all optional):

CallbackSignature
Start / Stopvoid(Entity*, Game*, void*)
Updatevoid(Entity*, Game*, void*)
Rendervoid(Entity*, Draw*, Game*, void*)
Collisionvoid(Entity*, Entity*, Game*, void*)

Built-in stats available on every entity: health, max_health, strength, speed, level, xp, radius, health_regen, attack and move timers.

Sprite3D

A 3D mesh of up to 64 triangles. Pre-built shapes are available via initialization methods.

ShapeMethod
HumanoidinitializeAsHumanoid(position, height, color)
TreeinitializeAsTree(position, height, color)
HouseinitializeAsHouse(position, width, height, color)
PillarinitializeAsPillar(position, height, radius, color)
CustomBuild manually with addTriangle(...)

Colors are RGB565. The engine applies a shading factor to different faces automatically for built-in shapes.

Camera

Controls the 3D view. Supports CAMERA_FIRST_PERSON and CAMERA_THIRD_PERSON perspectives.

FieldDefaultDescription
position(0,0,0)World-space camera location
direction(1,0,0)Look direction
plane(0,0.66,0)Camera plane controlling field of view
height1.6Camera height above ground
distance2.0Follow distance for third-person mode

Draw

A C++ wrapper over all LCD macros. All drawing methods accept either a Vector or raw int16_t coordinates.

Available operations: fillScreen, pixel, line, circle, fillCircle, rectangle, fillRectangle, triangle, fillTriangle, image (8-bit and 16-bit), text, swap.

Image

A 2D bitmap sprite backed by an in-memory pointer or a file path. Supports 8-bit palette and 16-bit RGB565 formats.

Vector

A 3D math type (float x, y, z) with operators (+, -, *, /) and methods for rotation around Y, per-axis scaling, and translation.


Usage Example

Step 1 — Create your config file (one-time setup)

cp engine_config.hpp.example engine_config.hpp

engine_config.hpp is listed in .gitignore, so git pull will never overwrite your changes.

Open engine_config.hpp and uncomment/fill in the macros for your platform.

The memory macros default to standard new/delete/malloc/free and only need changing on custom allocator platforms.

Step 2 onwards — Game code

// 1. Define entity callbacks
void player_update(Entity *self, Game *game) {
    if (game->input == BUTTON_LEFT)
        self->position_set(self->position.x - self->speed, self->position.y);
    if (game->input == BUTTON_RIGHT)
        self->position_set(self->position.x + self->speed, self->position.y);
    game->clamp(self->position.x, 0, game->size.x);
}

void player_collision(Entity *self, Entity *other, Game *game) {
    if (other->type == ENTITY_ENEMY)
        self->health -= other->strength;
}

// 2. Create sprites and entities
Image *sprite = new Image(Vector(16, 16), false, my_bitmap_data);
Entity *player = new Entity(
    "player", ENTITY_PLAYER, Vector(64, 32), Vector(16, 16), sprite,
    nullptr, nullptr,
    {},                            // start callback
    {},                            // stop callback
    {player_update, nullptr},      // update callback
    {},                            // render callback
    {player_collision, nullptr}    // collision callback
);
player->is_player = true;
player->speed     = 1.5f;
player->health    = player->max_health = 100.0f;

// 3. Create the game, level, and add entities
Draw  *draw  = new Draw();
Game  *game  = new Game("MyGame", Vector(256, 128), draw, 0x0000, 0xFFFF);
Level *level = new Level("level1", Vector(256, 128), game);
level->entity_add(player);
game->level_add(level);

// 4a. Blocking loop (bare-metal)
GameEngine engine(game, 30.0f);
engine.run();

// 4b. Non-blocking loop (RTOS / cooperative scheduler)
GameEngine engine(game, 30.0f);
while (true) {
    engine.updateGameInput(read_buttons());
    engine.runAsync();
}

Adding a 3D Entity

Entity *npc = new Entity(
    "soldier", ENTITY_3D_SPRITE, Vector(100, 100), Vector(1, 2),
    nullptr, nullptr, nullptr,
    {}, {}, {npc_update, nullptr}, {}, {},
    false, SPRITE_3D_HUMANOID, 0xF800  // red humanoid
);
npc->set3DSpriteRotation(1.57f);
level->entity_add(npc);