initial game template
This commit is contained in:
543
TEMPLATE_USAGE.md
Normal file
543
TEMPLATE_USAGE.md
Normal file
@@ -0,0 +1,543 @@
|
||||
# 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! 🎮
|
||||
Reference in New Issue
Block a user