# 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: ```cpp 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: ```cpp 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: ```cpp 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: ```cpp 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: ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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 ```cpp 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: ```cpp 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 ```cpp // 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: ```cpp // 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 ```cpp GameConfig config = { .debug_verbose = true // Print debug messages }; ``` ### Monitor Input Events ```cpp if (config.debug_verbose) { printf("Input: type=%d x=%d y=%d\n", input.type, input.x, input.y); } ``` ### Check State Changes ```cpp if (config.debug_verbose) { printf("Score: %d, Lives: %d\n", state->score, state->lives); } ``` ### Serial Monitor Connect via USB and monitor output: ```bash 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): ```cpp // 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 ```cpp // 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 ```cpp 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 ```cpp // 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() ```cpp // 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 ```cpp // 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 ```cpp // 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! 🎮