Files
basic1/TEMPLATE_USAGE.md
T
2026-01-29 15:58:58 -05:00

14 KiB

Reactive Game Template - Usage Guide

Overview

This template provides a clean, event-driven architecture for building games and interactive applications on Raspberry Pi Pico (RP2350) with displays. It's designed to be efficient, power-conscious, and optimized for e-ink displays.

Architecture

Event-Driven Design

Interrupt (Touch/Button) → Set Flag → Main Loop Wakes → Process Input → Update Game State → Refresh Display → Sleep

The template follows a reactive pattern:

  1. Sleep: CPU waits for interrupts using __wfi() (very power efficient)
  2. Wake: Interrupt handler sets a flag and wakes CPU
  3. Process: Main loop processes the input event
  4. Update: Game logic updates state based on input
  5. Render: Display refreshes only if needed
  6. Repeat: Back to sleep

Key Components

1. Input Event System

  • InputEvent Structure: Unified representation of all inputs
  • InputType Enum: Touch (down/move/up), buttons, gestures
  • Interrupt Handlers: Minimal ISRs that only set flags
  • Processing Functions: process_touch_input() and process_button_input()

2. Game State Management

  • GameState Structure: All game-specific data in one place
  • GameConfig Structure: Configurable parameters (debounce, features, debug)
  • No Global Variables: State is passed to functions explicitly

3. Game Logic Functions

  • game_init(): Initialize game state at startup
  • game_update(): Handle input events and update state
  • game_draw(): Render UI and game graphics

Creating Your Own Game

Step 1: Define Your Game State

Modify the GameState structure to hold your game-specific data:

struct GameState {
    // Example for a Snake game
    int snake_x[100];
    int snake_y[100];
    int snake_length;
    int food_x;
    int food_y;
    int direction;
    uint32_t score;
    bool game_over;
};

Step 2: Initialize Your Game

Implement game_init() to set initial values:

void game_init(GameState* state) {
    // Snake starts in center
    state->snake_x[0] = V_WIDTH / 2;
    state->snake_y[0] = V_HEIGHT / 2;
    state->snake_length = 3;
    state->direction = 0; // Right
    state->score = 0;
    state->game_over = false;
    // Place first food
    place_random_food(state);
}

Step 3: Handle Input

Implement game_update() to respond to input events:

bool game_update(GameState* state, const InputEvent& input, const GameConfig& config, LowLevelRenderer* renderer) {
    bool needs_refresh = false;
    
    switch (input.type) {
        case INPUT_BUTTON_0:
            // Turn left
            state->direction = (state->direction + 3) % 4;
            needs_refresh = true;
            break;
            
        case INPUT_BUTTON_1:
            // Turn right
            state->direction = (state->direction + 1) % 4;
            needs_refresh = true;
            break;
            
        case INPUT_TOUCH_DOWN:
            // Touch to restart if game over
            if (state->game_over) {
                game_init(state);
                needs_refresh = true;
            }
            break;
    }
    
    // Update snake position
    update_snake_position(state);
    check_collisions(state);
    
    return needs_refresh;
}

Step 4: Draw Your Game

Implement game_draw() to render graphics:

void game_draw(const GameState* state, LowLevelRenderer* renderer, LowLevelGUI* gui) {
    // Draw game board
    LowLevelWindow *w1 = gui->draw_new_window(10, 10, V_WIDTH - 20, V_HEIGHT - 20, "Snake Game");
    
    // Draw snake
    for (int i = 0; i < state->snake_length; i++) {
        renderer->draw_filled_rectangle(state->snake_x[i], state->snake_y[i], 10, 10, true, 1);
    }
    
    // Draw food
    renderer->draw_filled_circle(state->food_x, state->food_y, 5, true);
    
    // Draw score
    char score_text[20];
    snprintf(score_text, sizeof(score_text), "Score: %d", state->score);
    renderer->draw_string(20, 30, score_text, true);
    
    // Game over message
    if (state->game_over) {
        renderer->draw_string(V_WIDTH/2 - 40, V_HEIGHT/2, "GAME OVER", true);
        renderer->draw_string(V_WIDTH/2 - 50, V_HEIGHT/2 + 20, "Touch to restart", true);
    }
}

Step 5: Adjust Configuration

Modify GameConfig in main() for your game's needs:

GameConfig config = {
    .touch_debounce_ms = 50,      // Slower for menu navigation
    .button_debounce_ms = 100,    // Longer for game controls
    .enable_gestures = false,     // Not needed for snake
    .enable_continuous_draw = false,
    .debug_verbose = true         // Enable during development
};

Example Game Ideas

1. Tic-Tac-Toe

  • Input: Touch to place X/O, buttons to switch players
  • State: 3x3 grid, current player, win condition
  • Drawing: Grid lines, X and O symbols
  • Ideal for: E-ink displays (few updates)

2. Pong

  • Input: Buttons to move paddle up/down
  • State: Paddle positions, ball position/velocity, score
  • Drawing: Paddles, ball, center line, score
  • Note: May need timer-based updates for ball movement

3. Memory Card Game

  • Input: Touch cards to flip, buttons to navigate
  • State: Card positions, flipped states, matches found
  • Drawing: Card grid, symbols when flipped
  • Ideal for: E-ink displays (turn-based)

4. Calculator

  • Input: Touch for number buttons, physical buttons for operations
  • State: Current value, operation mode, history
  • Drawing: Display, button grid
  • Perfect for: E-ink displays (minimal updates)

5. Drawing Board (Current Example)

  • Input: Touch to draw, buttons to clear/undo
  • State: Last position, stroke history
  • Drawing: Lines following touch movement
  • Ideal for: TFT displays (frequent updates)

Input Handling Best Practices

Touch Input

case INPUT_TOUCH_DOWN:
    // First touch - capture position
    state->start_x = input.x;
    state->start_y = input.y;
    break;

case INPUT_TOUCH_MOVE:
    // Continuous drawing/dragging
    if (config.enable_continuous_draw) {
        draw_line(state->last_x, state->last_y, input.x, input.y);
    }
    break;

case INPUT_TOUCH_UP:
    // Touch released - finalize action
    calculate_gesture(state->start_x, state->start_y, input.x, input.y);
    break;

Button Input

case INPUT_BUTTON_0:
    // First button - navigation/cancel
    navigate_menu_prev(state);
    break;

case INPUT_BUTTON_1:
    // Second button - selection/confirm
    select_menu_item(state);
    break;

Gesture Input

case INPUT_GESTURE:
    switch(input.gesture_code) {
        case 0x10: // Move Up
            scroll_up(state);
            break;
        case 0x18: // Move Down
            scroll_down(state);
            break;
        case 0x48: // Zoom In
            increase_scale(state);
            break;
    }
    break;

E-Ink Display Optimization

Minimize Refreshes

  • Only return true from game_update() when display actually changes
  • Batch updates: collect multiple changes before refreshing
  • Use partial refresh when available

Visual Design Tips

  1. High Contrast: Use solid blacks and whites
  2. Clear Shapes: Avoid thin lines (use 2px minimum)
  3. Large Text: Use readable fonts (5x5 or larger)
  4. Simple Graphics: Minimize complex patterns
  5. Static Elements: Redraw only changed areas when possible

Example Pattern

bool game_update(GameState* state, const InputEvent& input, const GameConfig& config, LowLevelRenderer* renderer) {
    bool needs_refresh = false;
    
    // Collect all changes
    if (input.type == INPUT_BUTTON_0) {
        state->menu_index++;
        needs_refresh = true;
    }
    
    // Only redraw if something changed
    return needs_refresh;
}

Power Efficiency

Sleep Between Events

The template uses __wfi() to put CPU to sleep:

while (1) {
    __wfi();  // CPU sleeps here until interrupt
    // Process input...
}

Reduce Polling

  • Use interrupt-driven input (already implemented)
  • Avoid tight loops checking hardware
  • Let ISRs wake the CPU only when needed

Display Power

// For e-ink: Turn off display after inactivity
if (time_since_last_input > SLEEP_TIMEOUT) {
    display->sleep();
}

GUI Components Available

The template includes a full GUI system with these components:

// Windows
gui->draw_new_window(x, y, width, height, "Title");

// Buttons
gui->draw_button(window, x, y, "Label", pressed, rounded);

// Checkboxes
gui->draw_checkbox(window, x, y, "Label", checked);

// Radio Buttons
gui->draw_radio_button(window, x, y, "Label", selected);

// Sliders
gui->draw_slider(window, x, y, width, height, position, "Label");

// Status Bars
gui->draw_status_bar(window, x, y, width, "Label", "Sublabel", percentage, "Value");

// Gauges
gui->draw_circular_gauge(window, x, y, width, "Label", percentage);

// Text Boxes
gui->draw_textbox(window, x, y, width, height, "Content", focused);

// Tabs
gui->draw_tab(window, x, y, width, height, "Label", selected);

// Notifications
gui->draw_notification(window, x, y, width, "Time", "Message");

// Clock
gui->draw_large_clock(window, x, y, "12:30");

// Calendar
gui->draw_calendar(window, x, y, month, year);

Debugging Tips

Enable Verbose Mode

GameConfig config = {
    .debug_verbose = true  // Print debug messages
};

Monitor Input Events

if (config.debug_verbose) {
    printf("Input: type=%d x=%d y=%d\n", input.type, input.x, input.y);
}

Check State Changes

if (config.debug_verbose) {
    printf("Score: %d, Lives: %d\n", state->score, state->lives);
}

Serial Monitor

Connect via USB and monitor output:

screen /dev/cu.usbmodem101

Multi-Board Support

The template works across different board configurations:

  • Pico 2 with TFT + Touch: Full interactive drawing
  • Pico 2 with E-ink + Buttons: Button-based navigation
  • Feather boards: Various display combinations

Board-specific configuration is handled automatically through board_config.h.

Advanced Patterns

Timer-Based Updates

For games needing periodic updates (not just reactive):

// In main(), before loop:
uint32_t last_game_tick = 0;
const uint32_t TICK_INTERVAL_MS = 100;

// In main loop:
uint32_t now = to_ms_since_boot(get_absolute_time());
bool needs_tick = (now - last_game_tick >= TICK_INTERVAL_MS);

if (needs_tick) {
    // Update game logic (physics, AI, etc.)
    update_game_tick(&game_state);
    last_game_tick = now;
    refresh_screen(bit_buffer, display);
}

Animation

// Smooth movement over multiple frames
void animate_sprite(GameState* state) {
    state->sprite_x += state->velocity_x;
    state->sprite_y += state->velocity_y;
    state->frame_count++;
}

State Machines

enum GameMode {
    MODE_MENU,
    MODE_PLAYING,
    MODE_PAUSED,
    MODE_GAME_OVER
};

struct GameState {
    GameMode mode;
    // ... other fields
};

bool game_update(GameState* state, const InputEvent& input, ...) {
    switch(state->mode) {
        case MODE_MENU:
            return handle_menu_input(state, input);
        case MODE_PLAYING:
            return handle_game_input(state, input);
        case MODE_PAUSED:
            return handle_pause_input(state, input);
        case MODE_GAME_OVER:
            return handle_gameover_input(state, input);
    }
}

Common Pitfalls

1. Forgetting to Return True

// Wrong:
bool game_update(...) {
    state->score++;
    // No return - screen won't refresh!
}

// Right:
bool game_update(...) {
    state->score++;
    return true;  // Signal refresh needed
}

2. Drawing in game_update()

// Wrong:
bool game_update(...) {
    renderer->draw_line(...);  // Drawing here!
    return true;
}

// Right:
bool game_update(...) {
    state->line_end_x = input.x;  // Update state only
    return true;
}
// Drawing happens in main loop when refresh is needed

3. Blocking in ISR

// Wrong:
void touch_interrupt_handler(...) {
    touch->read_touch(&data);  // Slow I2C operation in ISR!
    printf("Touch!\n");         // Serial output in ISR!
}

// Right:
void touch_interrupt_handler(...) {
    touch_interrupt_flag = true;  // Just set flag
}

4. Not Clearing Buffer Before Redraw

// When redrawing entire UI:
memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8);  // Clear first
game_draw(&game_state, &renderer, &gui);        // Then draw

Performance Considerations

Memory Usage

  • Frame buffer: V_WIDTH * V_HEIGHT / 8 bytes (e.g., 13KB for 296x128)
  • Keep GameState small for fast copying
  • Use uint8_t instead of int where possible

CPU Usage

  • Interrupt-driven design minimizes CPU usage
  • __wfi() puts CPU to sleep between events
  • Typical power draw: < 1mA while sleeping

Display Refresh Times

  • E-ink: 1-4 seconds for full refresh
  • TFT: < 50ms for full screen
  • Partial updates much faster

Next Steps

  1. Start Simple: Begin with basic button navigation
  2. Add Features: Gradually add game mechanics
  3. Test on Hardware: Verify on your target board
  4. Optimize: Tune debounce, refresh strategy for your needs
  5. Polish: Add animations, sounds, save states

Example Projects

Check out these example implementations:

  • Button Game (current): Focus switching and click counting
  • Drawing Board: Touch-based freehand drawing
  • Snake Game: Classic snake with button controls
  • Calculator: Touch-based number pad with operations

Resources

  • Board Configs: board_configs/ directory
  • Display Drivers: display/ directory
  • Font Files: fonts/ directory
  • Refactoring Plan: REFACTORING_PLAN.md - Implementation details

Support

For issues or questions:

  1. Check serial output with screen /dev/cu.usbmodem101
  2. Enable debug_verbose in GameConfig
  3. Review the refactoring plan for architecture details
  4. Test with minimal game logic first

Happy coding! 🎮