Project 11: Simple Game - Pong or Snake
Build a real-time game loop with button input, animation, scoring, and collision on the RP2350 LCD.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 3: Advanced |
| Time Estimate | 2-3 weeks |
| Main Programming Language | C (Alternatives: Rust, MicroPython) |
| Alternative Programming Languages | Rust, MicroPython |
| Coolness Level | Level 4: Hardcore |
| Business Potential | 2. The “Fun” Level |
| Prerequisites | Projects 2 and 6 (graphics + text) |
| Key Topics | Game loop, input debouncing, collision detection |
1. Learning Objectives
By completing this project, you will:
- Implement a fixed timestep game loop with stable FPS.
- Read and debounce button input for responsive controls.
- Detect collisions reliably with bounding boxes.
- Render game state efficiently using sprites and text.
- Manage game states (menu, play, game over).
2. All Theory Needed (Per-Concept Breakdown)
2.1 Fixed Timestep Game Loop
Fundamentals
A fixed timestep loop updates game logic at a constant interval (e.g., 16.67 ms for 60 FPS). This yields consistent physics and predictable behavior regardless of rendering speed. The loop typically accumulates elapsed time, runs one or more fixed updates, then renders. On microcontrollers, fixed timesteps simplify timing and make input handling consistent.
Deep Dive into the concept
Variable timestep loops can cause unstable gameplay: collision checks and movement depend on frame time, so slower frames lead to jumps and missed collisions. A fixed timestep loop decouples update and render. The core idea is to measure elapsed time and accumulate it in a lag variable. While lag >= dt, you run the update step and subtract dt. Then you render once. This ensures updates happen at a constant rate even if rendering occasionally takes longer. On a microcontroller, you must choose a timestep that matches your LCD update speed. For example, if full-screen DMA updates are ~30 FPS, you might choose 30 Hz updates and render at the same rate.
Another critical consideration is input sampling. If you only sample input once per frame, you may miss short button presses. Use a higher-frequency input sampling (e.g., every update step) and store input state. For a simple game like Pong or Snake, fixed timestep ensures the ball or snake moves smoothly and collisions are detected reliably. You can also include a “frame budget” check: if updates take too long, you can skip rendering to keep game logic correct.
How this fits on projects
Fixed timestep logic is used in Section 3.2 and Section 5.10 Phase 2. It also builds on Project 2 (rendering primitives) and Project 6 (text rendering). Also used in: Project 2, Project 6.
Definitions & key terms
- Fixed timestep -> Constant update interval.
- Lag accumulator -> Stores leftover time.
- Frame budget -> Maximum time per frame.
- Update vs render -> Logic step vs draw step.
Mental model diagram (ASCII)
loop:
lag += elapsed
while (lag >= dt) { update(); lag -= dt; }
render();
How it works (step-by-step)
- Measure elapsed time since last frame.
- Add to lag accumulator.
- Run update steps until lag < dt.
- Render the current game state.
Failure modes:
- Variable timestep -> unstable physics.
- Large lag -> spiral of death (too many updates).
Minimal concrete example
const float dt = 1.0f / 60.0f;
float lag = 0;
while (1) {
float elapsed = timer_elapsed();
lag += elapsed;
while (lag >= dt) {
update_game(dt);
lag -= dt;
}
render_game();
}
Common misconceptions
- “Fixed timestep is only for big games.” -> It’s ideal for embedded.
- “Render and update must be the same rate.” -> They can be decoupled.
Check-your-understanding questions
- Why does variable timestep cause unstable gameplay?
- What is the lag accumulator used for?
- What happens if updates take longer than dt?
Check-your-understanding answers
- Movement depends on frame time, causing jumps.
- It stores extra time to run multiple updates.
- You may need to skip rendering or clamp lag.
Real-world applications
- Game engines on constrained platforms
- Real-time simulations
Where you’ll apply it
- This project: Section 3.2, Section 5.10 Phase 2
- Also used in: Project 5 for stable rendering loops
References
- “Game Programming Patterns” (fixed timestep chapter)
Key insights
Fixed timestep loops are the secret to stable real-time behavior.
Summary
Use a fixed update step and lag accumulator for deterministic gameplay.
Homework/Exercises to practice the concept
- Simulate a moving object at 30 FPS and 60 FPS.
- Add a lag clamp to prevent spiral of death.
- Measure how many updates run per second.
Solutions to the homework/exercises
- Fixed timestep yields consistent speed across frame rates.
- Clamp lag to a max value (e.g., 0.25 s).
- Updates should equal target FPS.
2.2 Input Debouncing and Collision Detection
Fundamentals
Mechanical buttons bounce, producing multiple transitions for a single press. Debouncing filters this noise so your game only registers one input. Collision detection checks whether game objects overlap. For Pong or Snake, axis-aligned bounding boxes (AABB) are sufficient and efficient.
Deep Dive into the concept
Debouncing can be done in software by requiring a stable input state for a minimum time (e.g., 10-20 ms). You read the raw GPIO, compare with the last stable state, and only accept a change if it remains stable for the debounce interval. This prevents double-presses. For games, input latency matters; you must balance filtering with responsiveness. A common approach is a per-button counter that increments when the input is stable and resets when it changes.
Collision detection is about detecting overlaps between objects. For Pong, you check if the ball’s rectangle overlaps the paddle’s rectangle. For Snake, you check if the head intersects any body segment or the walls. AABB collisions are simple: two rectangles overlap if their ranges overlap on both axes. This is O(1) per collision check. For Snake, checking against all segments is O(n); you can optimize using a grid if needed. Deterministic collision logic prevents misses when objects move fast. If objects move more than their size per frame, you may need swept collision checks (continuous collision detection). For Pong, you can reduce ball speed or use smaller timesteps.
How this fits on projects
Debouncing and collision detection are used in Section 3.2 and Section 5.10 Phase 2. They also apply to Project 4 (LED input control) and Project 12 (USB input). Also used in: Project 4, Project 12.
Definitions & key terms
- Debounce -> Filter mechanical button noise.
- AABB -> Axis-aligned bounding box collision.
- Input latency -> Delay between press and action.
- Swept collision -> Continuous collision detection.
Mental model diagram (ASCII)
Button bounce:
Pressed: 1 0 1 0 1 1 1 (debounce -> 1)
AABB overlap:
[ball] overlaps [paddle] -> collision
How it works (step-by-step)
- Sample GPIO input each update.
- Apply debounce filter to produce stable state.
- Update game objects based on input.
- Check collisions with AABB.
Failure modes:
- No debounce -> multiple presses.
- High speed -> collision misses.
Minimal concrete example
bool aabb_overlap(rect a, rect b) {
return (a.x < b.x + b.w) && (a.x + a.w > b.x) &&
(a.y < b.y + b.h) && (a.y + a.h > b.y);
}
Common misconceptions
- “Debounce is only for hardware.” -> Software debounce is typical.
- “Collision is just distance.” -> For rectangles, use AABB overlap.
Check-your-understanding questions
- Why does button bounce occur?
- How do you implement a debounce filter?
- What is the AABB overlap condition?
Check-your-understanding answers
- Mechanical contacts bounce before settling.
- Require stable readings for a minimum time.
- Overlap on both X and Y axes.
Real-world applications
- UI controls in embedded devices
- Industrial controllers with physical buttons
Where you’ll apply it
- This project: Section 3.2, Section 5.10 Phase 2
- Also used in: Project 12
References
- “Making Embedded Systems” (input handling)
Key insights
Reliable input and collision logic make the game feel responsive and fair.
Summary
Debounce inputs and use AABB collision checks for predictable gameplay.
Homework/Exercises to practice the concept
- Implement debounce with a 10 ms stable threshold.
- Test collision with moving rectangles.
- Add wall collision for Pong.
Solutions to the homework/exercises
- Use a counter for stable readings.
- Check AABB overlap after each update.
- Invert velocity when ball hits wall.
3. Project Specification
3.1 What You Will Build
A Pong or Snake game with a fixed timestep loop, button input, collision detection, and a score display. The game includes menu, play, and game-over screens.
3.2 Functional Requirements
- Fixed timestep loop (30 or 60 FPS).
- Input handling with debounce.
- Collision detection for game objects.
- Score display using font engine.
3.3 Non-Functional Requirements
- Performance: Stable FPS with no frame drops.
- Reliability: No missed inputs.
- Usability: Clear controls and UI feedback.
3.4 Example Usage / Output
PONG
Score: 3 - 2
FPS: 60
3.5 Data Formats / Schemas / Protocols
- Game state struct: positions, velocities, scores.
3.6 Edge Cases
- Ball hits paddle corner
- Snake collides with itself
- Input held down vs repeated presses
3.7 Real World Outcome
You can play the game with hardware buttons. The score updates, collisions feel fair, and the frame rate stays stable.
3.7.1 How to Run (Copy/Paste)
cd LEARN_RP2350_LCD_DEEP_DIVE/simple_game
mkdir -p build
cd build
cmake ..
make -j4
cp simple_game.uf2 /Volumes/RP2350
3.7.2 Golden Path Demo (Deterministic)
- Start game, play 10 seconds.
- Score updates correctly.
- Frame time stays within 16.7 ms budget.
3.7.3 Failure Demo (Deterministic)
- Disable debounce.
- Expected: multiple rapid inputs.
- Fix: restore debounce filter.
4. Solution Architecture
4.1 High-Level Design
[Input] -> [Game Logic] -> [Renderer] -> LCD
4.2 Key Components
| Component | Responsibility | Key Decisions | |———–|—————-|—————| | Game loop | Fixed timestep | 60 FPS | | Input | Debounce buttons | 10 ms filter | | Renderer | Draw sprites/text | Use dirty rectangles |
4.3 Data Structures (No Full Code)
typedef struct { int x,y,vx,vy; } ball_t;
4.4 Algorithm Overview
Key Algorithm: Pong Update
- Update ball position.
- Check wall collisions.
- Check paddle collisions.
- Update score if out of bounds.
Complexity Analysis:
- Time: O(1) per frame
- Space: O(1)
5. Implementation Guide
5.1 Development Environment Setup
# Use pico-sdk and button GPIOs
5.2 Project Structure
simple_game/
- src/
- game.c
- input.c
- main.c
5.3 The Core Question You’re Answering
“How do I build a real-time game loop on constrained hardware?”
5.4 Concepts You Must Understand First
- Fixed timestep loop
- Input debouncing
- Collision detection
5.5 Questions to Guide Your Design
- What frame rate is realistic for your display pipeline?
- How will you map buttons to actions?
- How will you avoid collision misses?
5.6 Thinking Exercise
Compute the frame budget for 60 FPS and list tasks that must fit.
5.7 The Interview Questions They’ll Ask
- What is a fixed timestep loop?
- Why does debouncing matter?
- How do you avoid missed collisions?
5.8 Hints in Layers
- Hint 1: Start with a moving ball without input.
- Hint 2: Add paddles and simple collisions.
- Hint 3: Add scoring and game states.
5.9 Books That Will Help
| Topic | Book | Chapter | |——-|——|———| | Game loops | “Game Programming Patterns” | Ch. 3 |
5.10 Implementation Phases
Phase 1: Core Loop (3-4 days)
Goals: Basic update/render loop. Tasks: Fixed timestep and basic drawing. Checkpoint: Ball moves smoothly.
Phase 2: Input + Collision (4-5 days)
Goals: Playable game. Tasks: Debounce input and add collisions. Checkpoint: Pong or Snake playable.
Phase 3: UI Polish (3-4 days)
Goals: Menu and score display. Tasks: Add text and state transitions. Checkpoint: Game feels complete.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale | |———-|———|—————-|———–| | Frame rate | 30 vs 60 | 60 if DMA | Smoother play | | Collision | AABB vs pixel-perfect | AABB | Simpler |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples | |———-|———|———-| | Unit Tests | Collision math | overlap checks | | Integration Tests | Gameplay | full match simulation | | Performance Tests | FPS | 60 FPS stable |
6.2 Critical Test Cases
- Input debounce: no double presses.
- Collision edges: ball hits paddle corner.
- Score updates: increments correctly.
6.3 Test Data
Test paddle: 10x40, ball: 6x6
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution | |———|———|———-| | Variable timestep | Speed changes | Fixed timestep | | No debounce | Multiple presses | Add filter | | Collision miss | Ball passes paddle | Increase update rate |
7.2 Debugging Strategies
- Render collision boxes in debug mode.
- Log input states over UART.
7.3 Performance Traps
- Full-screen redraw each frame without DMA can drop FPS.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add sound effects with a buzzer.
8.2 Intermediate Extensions
- Add AI opponent.
8.3 Advanced Extensions
- Add multiplayer via USB HID.
9. Real-World Connections
9.1 Industry Applications
- Interactive UI demos on embedded devices
9.2 Related Open Source Projects
- Simple embedded game demos
9.3 Interview Relevance
- Real-time loops and input handling are common topics.
10. Resources
10.1 Essential Reading
- “Game Programming Patterns” by Robert Nystrom
10.2 Video Resources
- Fixed timestep loop tutorials
10.3 Tools & Documentation
- Pico SDK GPIO docs
10.4 Related Projects in This Series
- Project 6 for text rendering.
11. Self-Assessment Checklist
11.1 Understanding
- I can explain fixed timestep loops.
- I can implement debounce filters.
11.2 Implementation
- Game runs at stable FPS.
- Inputs feel responsive.
11.3 Growth
- I can describe my game architecture in an interview.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Playable Pong or Snake.
Full Completion:
- Scoreboard, menu, and game-over screen.
Excellence (Going Above & Beyond):
- AI opponent or additional game modes.