A real-time embedded arcade game developed on the ESP32 using the ESP-IDF framework and FreeRTOS. The project demonstrates multitasking, interrupt-driven input handling, frame-buffer graphics rendering, collision detection, dynamic memory management, and custom game-state architecture in an embedded environment.
This game is about the player controlling the platform to avoid falling balls. It progressively increases difficulty by spawning additional balls and increasing movement speed over time.
The system design:
- Real-time task scheduling with FreeRTOS
- Hardware interrupt handling
- Embedded graphics rendering
- Dynamic object management
- Collision detection algorithms
- State-machine architecture
- Low-level GPIO interaction
- Timing and delta-time game loops
- Memory management in constrained systems
Avoid colliding with falling balls for as long as possible.
- Left button → Move platform left
- Right button → Move platform right
- Dynamic difficulty scaling
- Increasing ball speed over time
- Real-time score tracking
- Persistent top-5 scoreboard during runtime
- Start menu and end-game screen
- Smooth rendering using frame-buffer flipping
The application is divided into two concurrent FreeRTOS tasks:
| Task | Purpose |
|---|---|
process_game() |
Handles game logic, physics, input, collisions, spawning |
render_game() |
Handles graphics rendering and frame updates |
The game logic and rendering engine execute independently using FreeRTOS tasks:
xTaskCreate(process_game, "Process Task", 1024*32, NULL, 1, NULL);
xTaskCreate(render_game, "Render Task", 1024*32, NULL, 1, NULL);Hardware interrupts are configured for responsive button handling:
gpio_set_intr_type(0, GPIO_INTR_POSEDGE);
gpio_set_intr_type(35, GPIO_INTR_ANYEDGE);Movement and timing are frame-rate independent using delta timing:
game_config.delta =
(float)(game_config.time_before_loop - game_config.last_time) / 1000000;Custom circle-vs-rectangle collision detection was implemented for gameplay physics.
Balls are dynamically allocated and stored in a linked list structure:
BALL_CONFIG *new_ball = create_ball(x, -y, size, speed);
add_ball(new_ball);Graphics are rendered using a double-buffer/frame-buffer style architecture:
The game operates using a finite state machine:
INIT → RUNNING → STOPPED → INIT
| State | Description |
|---|---|
INIT |
Menu and initialization |
RUNNING |
Active gameplay |
STOPPED |
Game over and scoreboard |
Dynamic linked lists are used to manage active game objects efficiently.
A sorted doubly linked list stores the top scores during runtime.
- ESP32 Tdisplay (TTGO) Development Board
- GPIO buttons
- LCD/TFT Display
- Hardware timer
- Interrupt controller
- Embedded C
- ESP-IDF
- FreeRTOS
- GPIO Drivers
- ESP Timer APIs
- Frame-buffer Graphics
- Dynamic Memory Allocation
- Deterministic task execution
- Cooperative multitasking
- Time-sensitive rendering
- Heap allocation
- Runtime object creation/destruction
- Linked list traversal
- GPIO register access
- Interrupt service routines
- Hardware timing
- Modular architecture
- Separation of concerns
- Scalable game-state design
for(;;)
{
process_platform(game_platform, game_config);
for (int i = 0; i < get_ball_amount(); i++)
{
BALL_CONFIG *ball = get_ball(i);
process_ball(ball, &game_config);
}
vTaskDelay(2);
}Potential future enhancements include:
- Better time loop delta calculation
- Add more tasks for physics and other processing
- Use FreeRTOS queues with interrupt handlers to add which button was pressed
- Deallocate resources after the game exit
- Embedded software architecture
- FreeRTOS task management
- Interrupt-driven systems
- Real-time graphics programming
- Low-level hardware interaction
- Algorithm implementation
- Memory-safe dynamic allocation
- Debugging embedded applications
- Modular firmware design
- Concurrent execution
- Hardware abstraction
- Event-driven systems
- Efficient rendering pipelines
- Scalable code organization
- ESP-IDF
- GCC for Xtensa
- PlatformIO
- FreeRTOS
- Ubuntu/Linux development environment
- Creating different tasks for rendering and processing made the balls halt in one place. Tried adding semaphores, but it didn't fix the issue. It turned out that there needed to be a task delay for each task.
- Slowed game loop after the semaphore. The semaphore was barring the two tasks, but it wasn't needed.
- The right button didn't register until the interrupt handlers were introduced.
- The platform was slowed heavily due to increased task delay. It was fixed by tuning the correct number of ticks to delay.