Newer
Older
ESP32-RetroPlay / main / tasks / game / README.md

How to Add a New Game to the ESP32 Game System

This guide outlines the steps to integrate a new game into the existing game selection and display framework on the ESP32.


System Overview

The ESP32 game system is organized as follows:

  • main.c Initializes core tasks (buttons, LEDs, display) and orchestrates the game selection menu and game execution loop.

  • game_menu.h / game_menu.c Manages the game selection menu displayed on the OLED.

  • display_task.h / display_task.c Handles the OLED display: drawing the menu, game graphics, game over screens, and transitions.

  • game_framework.h Defines the game_t interface (a struct of function pointers) that all games must implement, enabling standardized interaction.

  • game_drawing.h / game_drawing.c Provides common drawing utilities (e.g., draw_filled_rect, draw_rectangle_outline) for use across games.

  • button_task.h / button_task.c Handles button inputs and pushes them into a shared input queue.


Steps to Add a New Game

You will mainly work with:

  • Your new game files
  • main.c
  • Optionally game_framework.h (typically not required unless extending the interface)

Step 1: Create New Game Files

Create two new files in tasks/game/:

  • my_new_game.h
  • my_new_game.c

my_new_game.h

Declare your game's external game_t instance:

#ifndef MY_NEW_GAME_H
#define MY_NEW_GAME_H

#include "game_framework.h"  // Required for game_t

extern const game_t MyNewGame;

#endif // MY_NEW_GAME_H

my_new_game.c

Implement your game by adhering to the game_t interface.

Include Headers
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "esp_random.h"
#include "ssd1306.h"
#include "game_framework.h"
#include "game_drawing.h"
State Variables

Declare your game state:

static bool s_game_is_active = false;
static bool s_game_over = false;
static int s_score = 0;
static QueueHandle_t s_game_input_queue_global = NULL;
Implement game_t Functions
  • init_game
static void my_new_game_init_game_impl() {
    // Reset state
    s_game_is_active = true;
    s_game_over = false;
    s_score = 0;

    if (s_game_input_queue_global) {
        xQueueReset(s_game_input_queue_global);
    }
}
  • handle_input
static void my_new_game_handle_input_impl() {
    int input_value;
    if (xQueueReceive(s_game_input_queue_global, &input_value, 0)) {
        // Handle input_value (e.g., move player)
    }
}
  • update_game_state
static bool my_new_game_update_game_state_impl() {
    // Game logic here
    if (/* game over condition */) {
        s_game_over = true;
        return true;
    }
    return false;
}
  • draw_game
static void my_new_game_draw_game_impl(SSD1306_t *dev) {
    draw_filled_rect(dev, 0, GAME_PLAY_AREA_START_Y, OLED_WIDTH - 1, OLED_HEIGHT - 1, false);
    draw_rectangle_outline(dev, 0, GAME_PLAY_AREA_START_Y - 1, OLED_WIDTH - 1, OLED_HEIGHT - 1, true);
    
    // Draw player, enemies, etc. using game_drawing.h functions
}

⚠️ Note: Do not call ssd1306_show_buffer() or draw the score—these are handled by display_task.c.

  • get_score
static int my_new_game_get_score_impl() {
    return s_score;
}
  • is_active
static bool my_new_game_is_active_impl() {
    return s_game_is_active && !s_game_over;
}
  • set_active
static void my_new_game_set_active_impl(bool active) {
    s_game_is_active = active;
}
  • get_input_queue / set_input_queue
static QueueHandle_t my_new_game_get_input_queue_impl(void) {
    return s_game_input_queue_global;
}

static void my_new_game_set_input_queue_impl(QueueHandle_t queue) {
    s_game_input_queue_global = queue;
}
Define the game_t Instance
const game_t MyNewGame = {
    .init_game = my_new_game_init_game_impl,
    .handle_input = my_new_game_handle_input_impl,
    .update_game_state = my_new_game_update_game_state_impl,
    .draw_game = my_new_game_draw_game_impl,
    .get_score = my_new_game_get_score_impl,
    .is_active = my_new_game_is_active_impl,
    .set_active = my_new_game_set_active_impl,
    .get_input_queue = my_new_game_get_input_queue_impl,
    .set_input_queue = my_new_game_set_input_queue_impl,
};

Step 2: Update main.c

Include Your Game

Add at the top of main.c:

#include "tasks/game/my_new_game.h"

Add to the Game Menu

Find the game_menu_items[] array and append your game:

const menu_item_t game_menu_items[] = {
    {.name = "JUMP BIRD", .game_instance = &JumpBirdGame},
    {.name = "ARKANOID",  .game_instance = &ArkanoidGame},
    {.name = "MY NEW GAME", .game_instance = &MyNewGame}, // Your game
};

Step 3: Build and Test

Clean and Build

idf.py clean
idf.py build

Flash to Device

idf.py flash

Monitor Serial Output

idf.py monitor

You should now see "MY NEW GAME" on the OLED menu. Selecting it will run your new game.


✅ Summary

By following this guide, you will:

  • Maintain modular architecture
  • Leverage existing button and display systems
  • Keep all games consistent and interchangeable via the game_t interface