initial game template

This commit is contained in:
Adolfo Reyna
2026-01-29 15:58:58 -05:00
parent de566223b9
commit 372895fa08
5 changed files with 1076 additions and 594 deletions

View File

@@ -3,7 +3,35 @@
*
* SPDX-License-Identifier: Apache-2.0
*
* 4.0" TFT ST7796 with Touch Screen and SD Card Demo
* ============================================================================
* REACTIVE GAME TEMPLATE - Event-Driven Architecture for RP2350
* ============================================================================
*
* This template provides a clean, reactive architecture for building games
* and interactive applications on Raspberry Pi Pico with displays.
*
* KEY FEATURES:
* - Event-driven: Display only updates when input is received
* - Power efficient: Uses __wfi() to sleep between inputs
* - E-ink optimized: Minimizes screen refreshes
* - Interrupt-driven: Touch and button handling via interrupts
* - Modular: Clear separation of input, game logic, and rendering
*
* ARCHITECTURE:
* 1. Interrupt handlers set flags (kept minimal)
* 2. Main loop processes input events
* 3. Game logic updates state based on events
* 4. Screen refreshes only when changes occur
*
* HOW TO CREATE YOUR OWN GAME:
* ============================================================================
* 1. Modify GameState structure with your game variables
* 2. Implement game_init() to set initial values
* 3. Implement game_update() to handle input and update state
* 4. Implement game_draw() to render your game graphics
* 5. Adjust GameConfig for your game's needs
* 6. The reactive loop and input system work automatically!
* ============================================================================
*/
#include "pico/stdlib.h"
@@ -26,6 +54,72 @@ bi_decl(bi_program_description("4.0\" TFT ST7796 with Touch and SD Card Demo"));
bi_decl(bi_program_version_string("0.1"));
bi_decl(bi_program_build_date_string(__DATE__));
// ============================================================================
// INPUT EVENT STRUCTURES
// ============================================================================
// Input event types
enum InputType {
INPUT_NONE = 0,
INPUT_TOUCH_DOWN,
INPUT_TOUCH_MOVE,
INPUT_TOUCH_UP,
INPUT_BUTTON_0,
INPUT_BUTTON_1,
INPUT_GESTURE
};
// Unified input event structure
struct InputEvent {
InputType type;
int16_t x;
int16_t y;
uint8_t gesture_code; // For gesture events
uint8_t button_id; // For button events
uint8_t pressure; // Touch pressure/weight
bool valid; // Set to true if event is valid
};
// ============================================================================
// GAME STATE AND CONFIGURATION
// ============================================================================
// Game state - customize this for your game
struct GameState {
// Drawing game state
int16_t last_x;
int16_t last_y;
bool is_drawing;
// General game state
uint32_t score;
bool game_over;
uint32_t frame_count;
// UI state
uint8_t progress_value; // Progress bar value (0-100)
uint8_t focused_button; // Which button has focus (0 or 1)
uint32_t button1_clicks; // Count clicks on button 1
uint32_t button2_clicks; // Count clicks on button 2
// Statistics
uint32_t touch_success_count;
uint32_t touch_fail_count;
};
// Game configuration - adjust these for your game
struct GameConfig {
uint32_t touch_debounce_ms; // Touch polling rate
uint32_t button_debounce_ms; // Button debounce delay
bool enable_gestures; // Enable gesture recognition
bool enable_continuous_draw; // Allow continuous drawing while touched
bool debug_verbose; // Print debug messages
};
// ============================================================================
// INTERRUPT HANDLERS (Keep these minimal!)
// ============================================================================
// Touch interrupt handling
volatile bool touch_interrupt_flag = false;
volatile bool touch_event_down = false;
@@ -54,42 +148,12 @@ void touch_interrupt_handler(uint gpio, uint32_t events) {
// Main loop will handle the actual touch reading
touch_interrupt_flag = true;
// Optional: track which edge triggered (for debugging)
// Track which edge triggered (down vs up)
if (events & GPIO_IRQ_EDGE_FALL) {
touch_event_down = true;
printf("Touch DOWN event detected\n");
}
if (events & GPIO_IRQ_EDGE_RISE) {
touch_event_down = false;
printf("Touch UP event detected\n");
}
TouchData touch_data;
touch->read_touch(&touch_data);
int16_t x = touch_data.points[0].x;
int16_t y = touch_data.points[0].y;
uint8_t event = touch_data.points[0].event;
uint8_t id = touch_data.points[0].id;
uint8_t weight = touch_data.points[0].pressure;
uint8_t gesture = touch_data.gesture;
// Display detailed touch information including weight and gesture
printf("Touch: X=%d Y=%d Event=%d ID=%d Weight=%d\n",
x, y, event, id, weight);
// Display gesture if detected (non-zero)
if (gesture != 0) {
const char* gesture_name = "Unknown";
switch(gesture) {
case 0x10: gesture_name = "Move Up"; break;
case 0x14: gesture_name = "Move Right"; break;
case 0x18: gesture_name = "Move Down"; break;
case 0x1C: gesture_name = "Move Left"; break;
case 0x48: gesture_name = "Zoom In"; break;
case 0x49: gesture_name = "Zoom Out"; break;
}
printf(" Gesture=0x%02X (%s)\n", gesture, gesture_name);
}
}
@@ -110,12 +174,10 @@ void button_interrupt_handler(uint gpio, uint32_t events) {
if (events & GPIO_IRQ_EDGE_FALL) {
if (gpio == BUTTON_KEY0_PIN) {
button_key0_pressed = true;
printf("Button KEY0 pressed\n");
}
#ifdef BUTTON_KEY1_PIN
else if (gpio == BUTTON_KEY1_PIN) {
button_key1_pressed = true;
printf("Button KEY1 pressed\n");
}
#endif
}
@@ -145,6 +207,322 @@ void refresh_screen(const uint8_t *buffer, LowLevelDisplay* display) {
display->refresh();
}
// ============================================================================
// INPUT PROCESSING
// ============================================================================
/**
* @brief Get human-readable gesture name
*
* @param gesture_code Gesture code from touch controller
* @return Constant string with gesture name
*/
const char* get_gesture_name(uint8_t gesture_code) {
switch(gesture_code) {
case 0x10: return "Move Up";
case 0x14: return "Move Right";
case 0x18: return "Move Down";
case 0x1C: return "Move Left";
case 0x48: return "Zoom In";
case 0x49: return "Zoom Out";
default: return "Unknown";
}
}
/**
* @brief Process touch input and convert to InputEvent
*
* Reads touch data from controller and creates appropriate InputEvent.
* Handles debouncing and filtering internally.
*
* @param config Game configuration
* @param last_time Pointer to last touch time for debouncing
* @return InputEvent structure (valid=false if no valid input)
*/
InputEvent process_touch_input(const GameConfig& config, uint32_t* last_time) {
InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false};
// Check if touch interrupt flag is set
if (!touch_interrupt_flag) {
return event; // No touch event
}
// Don't clear the flag yet - we may still be processing continuous touch
// Check if touch is active
if (!touch_event_down) {
// Touch released
touch_interrupt_flag = false;
event.type = INPUT_TOUCH_UP;
event.valid = true;
return event;
}
// Touch is down - check debounce timing
uint32_t now = to_ms_since_boot(get_absolute_time());
if (now - *last_time < config.touch_debounce_ms) {
return event; // Too soon, skip
}
// Read touch data
TouchData touch_data;
if (!touch || !touch->read_touch(&touch_data)) {
return event; // Read failed
}
// Populate event structure
event.x = touch_data.points[0].x;
event.y = touch_data.points[0].y;
event.pressure = touch_data.points[0].pressure;
event.gesture_code = touch_data.gesture;
event.valid = true;
// Determine event type
if (*last_time == 0) {
event.type = INPUT_TOUCH_DOWN;
} else {
event.type = INPUT_TOUCH_MOVE;
}
// Handle gesture events
if (config.enable_gestures && touch_data.gesture != 0) {
event.type = INPUT_GESTURE;
if (config.debug_verbose) {
printf("Gesture: 0x%02X (%s)\n", event.gesture_code, get_gesture_name(event.gesture_code));
}
}
*last_time = now;
return event;
}
/**
* @brief Process button input and convert to InputEvent
*
* Checks button flags and verifies button state with debouncing.
* Clears flags after processing.
*
* @param config Game configuration
* @return InputEvent structure (valid=false if no valid input)
*/
InputEvent process_button_input(const GameConfig& config) {
InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false};
#ifdef BUTTON_KEY0_PIN
// Check KEY0
if (button_key0_pressed) {
button_key0_pressed = false;
sleep_ms(config.button_debounce_ms);
if (gpio_get(BUTTON_KEY0_PIN) == 0) { // Verify still pressed
event.type = INPUT_BUTTON_0;
event.button_id = 0;
event.valid = true;
if (config.debug_verbose) {
printf("Button KEY0 action triggered\n");
}
return event;
}
}
#ifdef BUTTON_KEY1_PIN
// Check KEY1
if (button_key1_pressed) {
button_key1_pressed = false;
sleep_ms(config.button_debounce_ms);
if (gpio_get(BUTTON_KEY1_PIN) == 0) { // Verify still pressed
event.type = INPUT_BUTTON_1;
event.button_id = 1;
event.valid = true;
if (config.debug_verbose) {
printf("Button KEY1 action triggered\n");
}
return event;
}
}
#endif
#endif
return event;
}
// ============================================================================
// GAME LOGIC (Customize this section for your game!)
// ============================================================================
/**
* @brief Initialize game state
*
* Called once at startup to set initial game values.
* Customize this for your game.
*
* @param state Pointer to GameState to initialize
*/
void game_init(GameState* state) {
state->last_x = -1;
state->last_y = -1;
state->is_drawing = false;
state->score = 0;
state->game_over = false;
state->frame_count = 0;
state->progress_value = 50; // Start at 50%
state->focused_button = 0; // Start with first button focused
state->button1_clicks = 0;
state->button2_clicks = 0;
state->touch_success_count = 0;
state->touch_fail_count = 0;
}
/**
* @brief Update game state based on input event
*
* This is where your game logic goes.
* Called whenever an input event occurs.
*
* @param state Pointer to GameState to update
* @param input Input event to process
* @param config Game configuration
* @param renderer Renderer for drawing operations
* @return true if screen needs refresh (drawing occurred)
*/
bool game_update(GameState* state, const InputEvent& input, const GameConfig& config, LowLevelRenderer* renderer) {
bool needs_refresh = false;
switch (input.type) {
case INPUT_TOUCH_DOWN:
// Start new drawing stroke
state->last_x = input.x;
state->last_y = input.y;
state->is_drawing = true;
state->touch_success_count++;
break;
case INPUT_TOUCH_MOVE:
// Continue drawing stroke
if (config.enable_continuous_draw && state->is_drawing) {
if (state->last_x >= 0 && state->last_y >= 0) {
// Draw line from last position
renderer->draw_line(state->last_x, state->last_y, input.x, input.y, true);
needs_refresh = true;
}
state->last_x = input.x;
state->last_y = input.y;
state->touch_success_count++;
}
break;
case INPUT_TOUCH_UP:
// End drawing stroke
state->is_drawing = false;
state->last_x = -1;
state->last_y = -1;
needs_refresh = true; // Final refresh to show complete stroke
break;
case INPUT_BUTTON_0:
// KEY0: Switch focus between buttons
state->focused_button = (state->focused_button == 0) ? 1 : 0;
needs_refresh = true;
if (config.debug_verbose) {
printf("Focus switched to button %d\n", state->focused_button);
}
break;
case INPUT_BUTTON_1:
// KEY1: Activate the focused button
if (state->focused_button == 0) {
state->button1_clicks++;
if (config.debug_verbose) {
printf("Button 1 clicked! Total: %d\n", state->button1_clicks);
}
} else {
state->button2_clicks++;
if (config.debug_verbose) {
printf("Button 2 clicked! Total: %d\n", state->button2_clicks);
}
}
needs_refresh = true;
break;
case INPUT_GESTURE:
// Handle gesture
if (config.debug_verbose) {
printf("Gesture detected: %s\n", get_gesture_name(input.gesture_code));
}
// Add gesture-specific actions here
break;
default:
break;
}
state->frame_count++;
return needs_refresh;
}
/**
* @brief Draw game graphics to screen buffer
*
* All initial UI drawing operations go here.
* Called once at startup to create the initial screen.
*
* @param state Pointer to current GameState
* @param renderer Renderer for drawing primitives
* @param gui GUI system for widgets (optional)
*/
void game_draw(const GameState* state, LowLevelRenderer* renderer, LowLevelGUI* gui) {
// Draw main window
LowLevelWindow *w1 = gui->draw_new_window(10, 10, V_WIDTH - 20, V_HEIGHT - 20, "Button Game");
// Draw instructions using text
renderer->set_font(&font_5x5_obj);
renderer->draw_string(20, 50, "KEY0: Switch Focus", true);
renderer->draw_string(20, 65, "KEY1: Click Button", true);
// Create button labels with click counts
char btn1_label[30];
snprintf(btn1_label, sizeof(btn1_label), "BTN 1 (%d)", state->button1_clicks);
char btn2_label[30];
snprintf(btn2_label, sizeof(btn2_label), "BTN 2 (%d)", state->button2_clicks);
// Draw Button 1 using GUI button element
// pressed=true shows it's focused/selected
gui->draw_button(w1, 10, 90, btn1_label, state->focused_button == 0, true);
// Draw Button 2 using GUI button element
gui->draw_button(w1, 10, 140, btn2_label, state->focused_button == 1, true);
// Draw status indicators using GUI elements
// Show which button is focused
if (state->focused_button == 0) {
gui->draw_radio_button(w1, 200, 100, "Active", true);
} else {
gui->draw_radio_button(w1, 200, 100, "Active", false);
}
if (state->focused_button == 1) {
gui->draw_radio_button(w1, 200, 150, "Active", true);
} else {
gui->draw_radio_button(w1, 200, 150, "Active", false);
}
// Show total interactions with a status bar
uint32_t total_clicks = state->button1_clicks + state->button2_clicks;
int percentage = (total_clicks > 0) ? ((state->button1_clicks * 100) / total_clicks) : 50;
char total_str[20];
snprintf(total_str, sizeof(total_str), "%d", total_clicks);
gui->draw_status_bar(w1, 10, 200, 270, "TOTAL CLICKS", "BTN1 vs BTN2 Ratio", percentage, total_str);
}
// ============================================================================
// MAIN PROGRAM
// ============================================================================
@@ -191,13 +569,26 @@ int main()
// Now clear to black for drawing
display->clear(false); // Clear to black
// Initialize renderer and GUI system
LowLevelRenderer renderer(bit_buffer, V_WIDTH, V_HEIGHT);
renderer.set_font(&font_5x5_obj);
LowLevelGUI gui = LowLevelGUI(&renderer, font_BMplain_obj);
LowLevelWindow *w1 = gui.draw_new_window(15, 15, V_WIDTH - 30, V_HEIGHT - 30, "Main Window");
gui.draw_status_bar(w1, 10, 40, 200,
"PANELS", "Weekly Average Charge", 65, "190KWH");
gui.draw_circular_gauge(w1, 10, 100 - 10, 200, "SYSTEM EFF.", 68);
// Initialize game configuration
GameConfig config = {
.touch_debounce_ms = 10,
.button_debounce_ms = 50,
.enable_gestures = true,
.enable_continuous_draw = true,
.debug_verbose = false
};
// Initialize game state
GameState game_state;
game_init(&game_state);
// Draw initial game graphics
game_draw(&game_state, &renderer, &gui);
// Refresh the screen with the rendered GUI
refresh_screen(bit_buffer, display);
@@ -260,131 +651,47 @@ int main()
// printf("SD Card initialization failed or no card present\n");
// }
// Main loop - handle touch events
int last_x = -1, last_y = -1;
// ========================================================================
// REACTIVE GAME LOOP
// ========================================================================
// The loop sleeps until an interrupt occurs, then:
// 1. Process input (button or touch)
// 2. Update game state based on input
// 3. Redraw only if game_update() indicates changes occurred
// This is ideal for e-ink displays (minimal refreshes) and power efficiency
// ========================================================================
// Touch debouncing
uint32_t last_touch_time = 0;
const uint32_t debounce_ms = 10; // Poll touch every 10ms (100 times per second)
bool was_touched = false;
int touch_fail_count = 0;
int touch_success_count = 0;
while (1) {
// Sleep until interrupt wakes us up (very power efficient!)
// Te(); // Wait For Event - CPU sleeps until interrupt or evenurs
__wfi(); // Wait For Interrupt - CPU sleeps until any interrupt
__wfi(); // Wait For Interrupt - CPU sleeps until any interrupt occurs
#ifdef BUTTON_KEY0_PIN
// Handle button presses with debouncing
if (button_key0_pressed) {
button_key0_pressed = false;
sleep_ms(50); // Debounce delay
if (gpio_get(BUTTON_KEY0_PIN) == 0) { // Verify button still pressed
printf("Button KEY0 action triggered\n");
// TODO: Add your KEY0 action here (e.g., focus next widget)
InputEvent input = {INPUT_NONE, 0, 0, 0, 0, 0, false};
bool needs_refresh = false;
// 1. Process button input first (higher priority)
input = process_button_input(config);
if (input.valid) {
needs_refresh = game_update(&game_state, input, config, &renderer);
}
// 2. Process touch input (if no button was pressed)
if (!input.valid) {
input = process_touch_input(config, &last_touch_time);
if (input.valid) {
needs_refresh = game_update(&game_state, input, config, &renderer);
}
}
if (button_key1_pressed) {
button_key1_pressed = false;
sleep_ms(50); // Debounce delay
if (gpio_get(BUTTON_KEY1_PIN) == 0) { // Verify button still pressed
printf("Button KEY1 action triggered\n");
// TODO: Add your KEY1 action here (e.g., activate focused widget)
// 3. Redraw and refresh screen only if needed
if (needs_refresh) {
// For button presses or touch release, redraw entire UI
if (input.type == INPUT_BUTTON_0 || input.type == INPUT_BUTTON_1 || input.type == INPUT_TOUCH_UP) {
// Clear buffer and redraw entire UI with updated state
memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8);
game_draw(&game_state, &renderer, &gui);
}
}
#endif
// Check if our touch interrupt flag was set
if (!touch_interrupt_flag) {
continue; // Woken by different interrupt, go back to sleep
}
// Clear the flag
touch_interrupt_flag = false;
while(touch_event_down){
uint32_t now = to_ms_since_boot(get_absolute_time());
// Check if enough time has passed since last touch check (debounce)
if (now - last_touch_time < debounce_ms) {
//continue;
}
//printf("Touch interrupt event detected (event_down=%d)\n", touch_event_down);
// Touch interrupt occurred - read the data
// is_touched() will check INT pin and confirm via I2C if needed
//if (touch && touch->is_touched()) {
// Now read full touch data via I2C (already confirmed by INT pin)
TouchData touch_data;
if (!touch->read_touch(&touch_data)) {
// Read failed or no actual touch data
touch_fail_count++;
//was_touched = false;
//last_x = -1;
//last_y = -1;
//last_touch_time = now;
continue;
}
touch_success_count++;
int16_t x = touch_data.points[0].x;
int16_t y = touch_data.points[0].y;
uint8_t event = touch_data.points[0].event;
uint8_t id = touch_data.points[0].id;
uint8_t weight = touch_data.points[0].pressure;
uint8_t gesture = touch_data.gesture;
// Display detailed touch information including weight and gesture
// printf("Touch: X=%d Y=%d Event=%d ID=%d Weight=%d",
// x, y, event, id, weight);
// Display gesture if detected (non-zero)
if (gesture != 0) {
const char* gesture_name = "Unknown";
switch(gesture) {
case 0x10: gesture_name = "Move Up"; break;
case 0x14: gesture_name = "Move Right"; break;
case 0x18: gesture_name = "Move Down"; break;
case 0x1C: gesture_name = "Move Left"; break;
case 0x48: gesture_name = "Zoom In"; break;
case 0x49: gesture_name = "Zoom Out"; break;
}
printf(" Gesture=0x%02X (%s)", gesture, gesture_name);
}
// printf(" [Success:%d Fail:%d]\n", touch_success_count, touch_fail_count);
// Check if touch is in title area to clear screen
// Draw line from last position (for smooth drawing)
if (last_x >= 0 && last_y >= 0) {
int dx = abs(x - last_x);
int dy = abs(y - last_y);
// Only draw line if movement is reasonable (filter noise)
//if (dx < 50 && dy < 50) {
renderer.draw_line(last_x, last_y, x, y, true);
//}
}
last_x = x;
last_y = y;
was_touched = true;
last_touch_time = now;
//} else {
// INT pin triggered but no touch data (likely release event)
//}
}
if (was_touched) {
last_x = -1;
last_y = -1;
was_touched = false;
refresh_screen(bit_buffer, display);
}
}