Project 9: 2D Game Engine
Build a complete 2D game engine with Entity-Component-System architecture, sprite rendering, physics simulation, and input handling capable of running a platformer or top-down game at 60 FPS.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Expert |
| Time Estimate | 2-3 months |
| Language | C++ |
| Prerequisites | Projects 1-8, linear algebra basics, graphics concepts |
| Key Topics | ECS, SDL2/OpenGL, fixed timestep, collision detection, sprite animation |
Table of Contents
- 1. Learning Objectives
- 2. Theoretical Foundation
- 3. Project Specification
- 4. Solution Architecture
- 5. Implementation Guide
- 6. Testing Strategy
- 7. Common Pitfalls & Debugging
- 8. Extensions & Challenges
- 9. Real-World Connections
- 10. Resources
- 11. Self-Assessment Checklist
- 12. Submission / Completion Criteria
1. Learning Objectives
By completing this project, you will:
- Master Entity-Component-System architecture: Understand data-oriented design and cache-friendly memory layouts
- Implement fixed timestep game loops: Decouple physics updates from rendering for deterministic simulation
- Understand graphics pipelines: Learn how sprites are rendered using SDL2 or OpenGL
- Build collision detection systems: Implement AABB collision with spatial partitioning
- Handle input systems: Process keyboard, mouse, and gamepad input efficiently
- Manage game resources: Load, cache, and unload textures, sounds, and other assets
- Create animation systems: Build sprite animation with frame timing and state machines
- Profile and optimize: Achieve 60 FPS with thousands of entities
2. Theoretical Foundation
2.1 Core Concepts
Entity-Component-System (ECS) Architecture
ECS separates data (components) from behavior (systems), enabling cache-efficient iteration and flexible entity composition:
+-----------------------------------------------------------------------+
| TRADITIONAL OOP VS ECS |
+-----------------------------------------------------------------------+
| |
| Object-Oriented Approach: |
| +-----------------------+ |
| | GameObject | |
| |-----------------------| |
| | - position | Problem: Deep inheritance hierarchies, |
| | - velocity | scattered data in memory, cache misses |
| | - sprite | |
| | - Update() | class Player : public Character |
| | - Render() | class Character : public Entity |
| | - OnCollision() | class Entity : public GameObject |
| +-----------------------+ |
| |
| ECS Approach: |
| +--------+ +--------+ +--------+ +--------+ |
| |Entity 0| |Entity 1| |Entity 2| |Entity 3| |
| | ID | | ID | | ID | | ID | |
| +--------+ +--------+ +--------+ +--------+ |
| | | | | |
| v v v v |
| Components (contiguous arrays): |
| Transform[]: [T0][T1][T2][T3] <- Cache-friendly iteration |
| Velocity[]: [V0][V1][ ][V3] <- Only entities with velocity |
| Sprite[]: [S0][S1][S2][ ] <- Only entities with sprites |
| |
| Systems operate on component arrays: |
| MovementSystem: for all (Transform, Velocity): t.pos += v.vel * dt |
| RenderSystem: for all (Transform, Sprite): draw(s, t.pos) |
| |
+-----------------------------------------------------------------------+
Fixed Timestep Game Loop
Decoupling physics from rendering ensures deterministic behavior:
+-----------------------------------------------------------------------+
| FIXED TIMESTEP GAME LOOP |
+-----------------------------------------------------------------------+
| |
| const double FIXED_DT = 1.0 / 60.0; // 60 Hz physics |
| double accumulator = 0.0; |
| double previousTime = getCurrentTime(); |
| |
| while (running) { |
| double currentTime = getCurrentTime(); |
| double frameTime = currentTime - previousTime; |
| previousTime = currentTime; |
| |
| // Clamp frame time to prevent spiral of death |
| if (frameTime > 0.25) frameTime = 0.25; |
| |
| accumulator += frameTime; |
| |
| // Fixed timestep physics updates |
| while (accumulator >= FIXED_DT) { |
| processInput(); |
| fixedUpdate(FIXED_DT); // Physics, collision |
| accumulator -= FIXED_DT; |
| } |
| |
| // Interpolation factor for smooth rendering |
| double alpha = accumulator / FIXED_DT; |
| render(alpha); // Interpolate between states |
| } |
| |
| Timeline visualization: |
| |
| Time: |-----|-----|-----|-----|-----|-----|-----| |
| Frame: | F1 | F2 | F3 | F4 | |
| Physics:| P | P | P | P | P | P | P | |
| |
| F = Frame (variable timing) |
| P = Physics update (fixed 1/60th second) |
| |
+-----------------------------------------------------------------------+
AABB Collision Detection
Axis-Aligned Bounding Box is the foundation of 2D collision:
+-----------------------------------------------------------------------+
| AABB COLLISION DETECTION |
+-----------------------------------------------------------------------+
| |
| AABB Structure: |
| +------------------+ |
| | min.x, min.y | Top-left corner |
| | max.x, max.y | Bottom-right corner |
| +------------------+ |
| |
| Intersection Test: |
| |
| +-------+ |
| | A | +-------+ |
| | | | B | |
| +-------+ +-------+ |
| |
| A and B intersect if and only if: |
| A.max.x > B.min.x AND A.min.x < B.max.x AND |
| A.max.y > B.min.y AND A.min.y < B.max.y |
| |
| Or equivalently, they DON'T intersect if: |
| A.max.x <= B.min.x OR A.min.x >= B.max.x OR |
| A.max.y <= B.min.y OR A.min.y >= B.max.y |
| |
| Collision Response (simple push-out): |
| |
| 1. Calculate overlap on each axis: |
| overlap_x = min(A.max.x - B.min.x, B.max.x - A.min.x) |
| overlap_y = min(A.max.y - B.min.y, B.max.y - A.min.y) |
| |
| 2. Push out along minimum overlap axis: |
| if (overlap_x < overlap_y) |
| push horizontally |
| else |
| push vertically |
| |
+-----------------------------------------------------------------------+
2.2 Why This Matters
Game engines represent the pinnacle of real-time systems programming:
- Performance-critical: Must maintain 60 FPS with thousands of objects
- Multi-domain integration: Graphics, physics, audio, input, networking
- Resource management: Efficient loading and memory usage
- Industry foundation: Unity, Unreal, and custom engines power billions in revenue
Real-world impact:
- Game industry: $200+ billion annually
- Simulation: Training, scientific visualization, architecture
- Interactive media: Education, art installations, VR/AR
2.3 Historical Context
The evolution of game engines:
1970s-80s: Hardware-specific, per-game engines
1990s: Doom, Quake engines - licensable technology
2000s: Unity, Unreal - democratized game development
2010s: ECS adoption (Unity DOTS, Bevy, Flecs)
2020s: Data-oriented design mainstream
Key milestones:
- id Tech 1 (1993): First widely licensed engine (Doom)
- Unreal Engine (1998): Visual scripting, cross-platform
- Unity (2005): Accessible to indie developers
- EnTT (2017): Modern C++ ECS library
- Bevy (2020): Rust ECS with parallelism
2.4 Common Misconceptions
| Misconception | Reality |
|---|---|
| “OOP is best for games” | ECS provides better cache locality and flexibility |
| “Fixed timestep wastes CPU” | Variable physics causes bugs and non-determinism |
| “SDL2 is slow” | SDL2 uses hardware acceleration; it’s very fast |
| “You need a physics library” | Simple 2D physics is straightforward to implement |
| “Games need inheritance” | Composition (ECS) is more flexible for games |
3. Project Specification
3.1 What You Will Build
A complete 2D game engine with:
- Core engine: Window management, game loop, timing
- ECS framework: Entities, components, systems, queries
- Rendering: Sprite batching, animations, layers
- Physics: AABB collision, velocity, gravity
- Input: Keyboard, mouse, action mapping
- Assets: Texture loading, caching, hot-reloading (optional)
- Demo game: Platformer or top-down game showcasing all features
3.2 Functional Requirements
| Requirement | Description |
|---|---|
| FR-1 | Create/destroy entities with arbitrary component combinations |
| FR-2 | Query entities by component types efficiently |
| FR-3 | Render sprites with position, rotation, scale, and layers |
| FR-4 | Animate sprites with configurable frame timing |
| FR-5 | Detect and resolve AABB collisions |
| FR-6 | Process keyboard/mouse input with action mapping |
| FR-7 | Maintain 60 FPS with 1000+ entities |
| FR-8 | Support multiple scenes/levels |
3.3 Non-Functional Requirements
| Requirement | Description |
|---|---|
| NFR-1 | Fixed timestep physics at 60 Hz |
| NFR-2 | Deterministic simulation (same input = same output) |
| NFR-3 | Memory-efficient component storage |
| NFR-4 | Cross-platform (Linux, macOS, Windows) |
| NFR-5 | Clean API for game code |
3.4 Example Usage / Output
// Game code using your engine
class PlatformerGame : public Game {
Entity player;
void onCreate() override {
// Load assets
auto playerTexture = assets.loadTexture("player.png");
auto platformTexture = assets.loadTexture("platform.png");
// Create player
player = world.createEntity();
player.addComponent<Transform>(100.0f, 100.0f);
player.addComponent<Sprite>(playerTexture, 32, 32);
player.addComponent<RigidBody>(1.0f); // mass
player.addComponent<BoxCollider>(32, 32);
player.addComponent<Animator>(playerAnimations);
player.addComponent<PlayerController>();
// Create platforms
for (int i = 0; i < 10; i++) {
auto platform = world.createEntity();
platform.addComponent<Transform>(i * 100.0f, 400.0f);
platform.addComponent<Sprite>(platformTexture, 100, 20);
platform.addComponent<BoxCollider>(100, 20);
platform.addComponent<StaticBody>();
}
// Register systems
world.addSystem<GravitySystem>();
world.addSystem<MovementSystem>();
world.addSystem<CollisionSystem>();
world.addSystem<AnimationSystem>();
world.addSystem<RenderSystem>();
}
void onUpdate(float dt) override {
// Input handling
if (input.isPressed(Action::Jump) && player.get<RigidBody>().grounded) {
player.get<RigidBody>().velocity.y = -500.0f;
}
float moveX = input.getAxis(Axis::Horizontal);
player.get<RigidBody>().velocity.x = moveX * 200.0f;
}
};
int main() {
Engine engine("My Platformer", 800, 600);
engine.run<PlatformerGame>();
return 0;
}
Expected terminal output:
[Engine] Initializing SDL2...
[Engine] Created window: 800x600
[Engine] OpenGL 3.3 Core Profile
[Assets] Loaded: player.png (32x32, 4 frames)
[Assets] Loaded: platform.png (100x20)
[Engine] Starting game loop...
[Engine] FPS: 60.0 | Entities: 11 | Draw calls: 11
[Engine] FPS: 60.0 | Entities: 11 | Draw calls: 11
...
[Input] Jump pressed
[Physics] Player grounded: false
[Collision] Player <-> Platform[3]
[Physics] Player grounded: true
3.5 Real World Outcome
After completing this project, you will have:
- Working game demo: A playable platformer demonstrating all engine features
- Reusable engine code: Can be used for future game projects
- Portfolio piece: Demonstrates systems programming and game development skills
- Deep understanding: Real-time systems, data-oriented design, graphics programming
You should be able to:
- Explain ECS advantages over OOP for games
- Debug rendering and physics issues
- Profile and optimize game performance
- Extend the engine with new features
4. Solution Architecture
4.1 High-Level Design
+-----------------------------------------------------------------------+
| ENGINE ARCHITECTURE |
+-----------------------------------------------------------------------+
| |
| +------------------+ |
| | Game Code | (User's game, inherits from Game base class) |
| +------------------+ |
| | |
| v |
| +------------------------------------------------------------------+ |
| | Engine Core | |
| | +------------+ +------------+ +------------+ +------------+ | |
| | | Window | | Input | | Audio | | Assets | | |
| | | Manager | | Manager | | Manager | | Manager | | |
| | +------------+ +------------+ +------------+ +------------+ | |
| +------------------------------------------------------------------+ |
| | |
| v |
| +------------------------------------------------------------------+ |
| | ECS Framework | |
| | +------------+ +------------+ +------------+ | |
| | | Entities | | Components | | Systems | | |
| | | (IDs) | | (Arrays) | | (Queries) | | |
| | +------------+ +------------+ +------------+ | |
| +------------------------------------------------------------------+ |
| | |
| v |
| +------------------------------------------------------------------+ |
| | Built-in Systems | |
| | +--------+ +--------+ +--------+ +--------+ +--------+ | |
| | |Physics | |Collision| |Animate | | Render | | Script | | |
| | |System | | System | | System | | System | | System | | |
| | +--------+ +--------+ +--------+ +--------+ +--------+ | |
| +------------------------------------------------------------------+ |
| | |
| v |
| +------------------------------------------------------------------+ |
| | Platform Layer (SDL2/OpenGL) | |
| +------------------------------------------------------------------+ |
| |
+-----------------------------------------------------------------------+
4.2 Key Components
Core Components (Data)
// Transform - position, rotation, scale
struct Transform {
Vec2 position{0.0f, 0.0f};
float rotation{0.0f}; // radians
Vec2 scale{1.0f, 1.0f};
// For interpolation
Vec2 previousPosition{0.0f, 0.0f};
};
// RigidBody - physics properties
struct RigidBody {
Vec2 velocity{0.0f, 0.0f};
Vec2 acceleration{0.0f, 0.0f};
float mass{1.0f};
float friction{0.1f};
float bounciness{0.0f};
bool grounded{false};
};
// Sprite - rendering properties
struct Sprite {
TextureId texture;
int width, height;
int frameX{0}, frameY{0}; // For spritesheets
int layer{0}; // Render order
Color tint{255, 255, 255, 255};
bool flipX{false}, flipY{false};
};
// BoxCollider - collision bounds
struct BoxCollider {
float width, height;
Vec2 offset{0.0f, 0.0f}; // Relative to transform
bool isTrigger{false}; // Trigger vs solid
};
// Animator - animation state
struct Animator {
std::vector<Animation> animations;
int currentAnimation{0};
int currentFrame{0};
float frameTimer{0.0f};
bool playing{true};
bool loop{true};
};
4.3 Data Structures
Entity Management
// Sparse set for O(1) component access
template<typename T>
class ComponentArray {
std::vector<T> dense; // Contiguous component data
std::vector<size_t> sparse; // Entity ID -> dense index
std::vector<Entity> entities; // Dense index -> Entity ID
public:
void insert(Entity e, T component);
void remove(Entity e);
T& get(Entity e);
bool has(Entity e) const;
// For iteration
auto begin() { return dense.begin(); }
auto end() { return dense.end(); }
};
// Component type registration
using ComponentType = std::size_t;
template<typename T>
ComponentType getComponentType() {
static ComponentType type = nextComponentType++;
return type;
}
Spatial Partitioning (Grid)
// Uniform grid for broad-phase collision
class SpatialGrid {
float cellSize;
std::unordered_map<int64_t, std::vector<Entity>> cells;
int64_t getCellKey(float x, float y) const {
int cx = static_cast<int>(x / cellSize);
int cy = static_cast<int>(y / cellSize);
return (static_cast<int64_t>(cx) << 32) | cy;
}
public:
void insert(Entity e, const AABB& bounds);
void remove(Entity e, const AABB& bounds);
std::vector<Entity> query(const AABB& bounds);
void clear();
};
4.4 Algorithm Overview
Collision Detection Pipeline
+-----------------------------------------------------------------------+
| COLLISION DETECTION PIPELINE |
+-----------------------------------------------------------------------+
| |
| 1. Broad Phase (Spatial Grid) |
| - Update grid with entity positions |
| - Query potential collision pairs |
| - O(n) average, O(n^2) worst case |
| |
| 2. Narrow Phase (AABB Test) |
| - Test each potential pair |
| - Calculate penetration depth and normal |
| |
| 3. Collision Response |
| - Separate overlapping bodies |
| - Apply impulse for bouncing |
| - Trigger callbacks for game logic |
| |
| Pseudocode: |
| |
| void CollisionSystem::update(float dt) { |
| grid.clear(); |
| |
| // Insert all colliders into grid |
| for (auto [entity, transform, collider] : |
| world.query<Transform, BoxCollider>()) { |
| AABB bounds = getWorldBounds(transform, collider); |
| grid.insert(entity, bounds); |
| } |
| |
| // Check all potential pairs |
| for (auto [entity, transform, collider] : |
| world.query<Transform, BoxCollider>()) { |
| AABB bounds = getWorldBounds(transform, collider); |
| auto nearby = grid.query(bounds); |
| |
| for (Entity other : nearby) { |
| if (entity >= other) continue; // Avoid duplicates |
| |
| if (intersects(bounds, getBounds(other))) { |
| resolveCollision(entity, other); |
| } |
| } |
| } |
| } |
| |
+-----------------------------------------------------------------------+
5. Implementation Guide
5.1 Development Environment Setup
# Install dependencies
# Ubuntu/Debian
sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev cmake
# macOS
brew install sdl2 sdl2_image sdl2_mixer cmake
# Create project structure
mkdir -p 2d-engine/{src,include,assets,build}
cd 2d-engine
# CMakeLists.txt
cat > CMakeLists.txt << 'EOF'
cmake_minimum_required(VERSION 3.16)
project(Engine2D CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(SDL2 REQUIRED)
find_package(SDL2_image REQUIRED)
add_executable(engine
src/main.cpp
src/engine.cpp
src/world.cpp
src/renderer.cpp
src/input.cpp
src/physics.cpp
src/assets.cpp
)
target_include_directories(engine PRIVATE include)
target_link_libraries(engine SDL2::SDL2 SDL2_image::SDL2_image)
EOF
# Build
mkdir build && cd build
cmake ..
make
5.2 Project Structure
2d-engine/
├── CMakeLists.txt
├── include/
│ ├── engine.hpp # Main engine class
│ ├── world.hpp # ECS world
│ ├── entity.hpp # Entity handle
│ ├── components.hpp # Built-in components
│ ├── systems.hpp # System base class
│ ├── renderer.hpp # SDL2/OpenGL rendering
│ ├── input.hpp # Input handling
│ ├── assets.hpp # Asset management
│ ├── math.hpp # Vec2, AABB, etc.
│ └── game.hpp # Game base class
├── src/
│ ├── main.cpp
│ ├── engine.cpp
│ ├── world.cpp
│ ├── renderer.cpp
│ ├── input.cpp
│ ├── physics.cpp
│ └── assets.cpp
├── assets/
│ ├── player.png
│ ├── tiles.png
│ └── sounds/
└── games/
└── platformer/
└── platformer.cpp
5.3 The Core Question You’re Answering
How do you structure a real-time simulation with thousands of interacting objects while maintaining consistent 60 FPS performance?
This question encompasses:
- Data organization for cache efficiency
- Decoupling simulation from rendering
- Efficient spatial queries
- Resource management
5.4 Concepts You Must Understand First
Before implementing, verify you can answer:
- What is cache locality and why does it matter for games?
- Reference: “Game Engine Architecture” Chapter 3
- Why use fixed timestep instead of variable delta time?
- Reference: “Game Programming Patterns” - Game Loop chapter
- How does AABB collision detection work?
- Reference: “Real-Time Collision Detection” Chapter 4
- What are the tradeoffs of ECS vs OOP for games?
- Reference: Unity DOTS documentation
- How does SDL2 hardware acceleration work?
- Reference: SDL2 Wiki - Rendering
5.5 Questions to Guide Your Design
ECS Design:
- How will you store components for cache efficiency?
- How will you handle component addition/removal efficiently?
- How will systems query for entities with specific components?
Game Loop:
- How will you handle frame timing variations?
- What happens when the game can’t keep up with 60 FPS?
- How will you interpolate for smooth rendering?
Collision:
- How will you avoid O(n^2) collision checks?
- How will you handle collision response for different body types?
- How will you report collisions to game code?
Rendering:
- How will you batch draw calls?
- How will you handle render layers/sorting?
- How will you implement sprite animations?
5.6 Thinking Exercise
Before coding, trace through this scenario:
Frame 1 (16.7ms frame time):
- Player at (100, 200) moving right at 200 px/s
- Platform at (150, 250) static
What happens during:
1. Input processing?
2. Physics update (fixed dt = 1/60)?
3. Collision detection?
4. Collision resolution?
5. Rendering with interpolation?
Draw the physics state before and after each step.
5.7 Hints in Layers
Hint 1 - Getting Started: Start with the game loop and a simple window. Get a sprite rendering before adding ECS complexity.
Hint 2 - ECS Implementation: Use sparse sets for component storage. The key insight is that entities are just IDs, and components are stored in contiguous arrays indexed by those IDs.
Hint 3 - Collision System: Implement broad-phase (spatial grid) before narrow-phase (AABB). Without spatial partitioning, collision checking becomes O(n^2).
Hint 4 - Debugging Physics: Render collision bounds as colored rectangles. Render velocity vectors as lines. This makes physics issues immediately visible.
5.8 The Interview Questions They’ll Ask
- “Explain the advantages of ECS over traditional OOP for game development.”
- Expected: Cache efficiency, composition over inheritance, parallelization potential, flexible entity composition
- “How would you handle collision detection with 10,000 entities?”
- Expected: Spatial partitioning (grid, quadtree), broad phase + narrow phase, sleeping bodies
- “Why use fixed timestep for physics simulation?”
- Expected: Determinism, stability, reproducibility, network sync
- “How does sprite batching improve rendering performance?”
- Expected: Reduce draw calls, batch by texture, minimize state changes
- “What happens when your game can’t maintain 60 FPS?”
- Expected: Spiral of death, frame time clamping, graceful degradation
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Game Loop | “Game Programming Patterns” by Robert Nystrom | Game Loop |
| ECS | “Game Programming Patterns” by Robert Nystrom | Component |
| Collision | “Real-Time Collision Detection” by Christer Ericson | Chapters 4-5 |
| Architecture | “Game Engine Architecture” by Jason Gregory | Chapters 1-8 |
| Rendering | “Game Engine Architecture” by Jason Gregory | Chapter 11 |
| Physics | “Game Physics Engine Development” by Ian Millington | Chapters 1-5 |
5.10 Implementation Phases
Phase 1: Core (Week 1-2)
- Window creation with SDL2
- Basic game loop with timing
- Sprite rendering
- Keyboard input
Phase 2: ECS (Week 3-4)
- Entity management
- Component arrays
- System queries
- Transform, Sprite, RigidBody components
Phase 3: Physics (Week 5-6)
- Velocity integration
- AABB collision detection
- Collision resolution
- Spatial grid
Phase 4: Animation & Polish (Week 7-8)
- Sprite animation
- Layer sorting
- Input action mapping
- Asset management
Phase 5: Demo Game (Week 9-10)
- Platformer mechanics
- Level loading
- Polish and optimization
5.11 Key Implementation Decisions
| Decision | Options | Recommendation |
|---|---|---|
| Graphics API | SDL2 Renderer, OpenGL, Vulkan | SDL2 for simplicity, OpenGL for learning |
| ECS Storage | Sparse set, Archetype | Sparse set is simpler to implement |
| Collision | Grid, Quadtree, BVH | Grid for uniform entity distribution |
| Memory | Custom allocators, std::allocator | std::allocator initially, optimize later |
6. Testing Strategy
Unit Tests
// Test AABB intersection
TEST(Collision, AABBIntersection) {
AABB a{0, 0, 10, 10};
AABB b{5, 5, 15, 15};
AABB c{20, 20, 30, 30};
EXPECT_TRUE(intersects(a, b));
EXPECT_TRUE(intersects(b, a));
EXPECT_FALSE(intersects(a, c));
}
// Test component storage
TEST(ECS, ComponentStorage) {
World world;
Entity e = world.createEntity();
e.addComponent<Transform>(10.0f, 20.0f);
EXPECT_TRUE(e.hasComponent<Transform>());
EXPECT_FLOAT_EQ(e.get<Transform>().position.x, 10.0f);
e.removeComponent<Transform>();
EXPECT_FALSE(e.hasComponent<Transform>());
}
// Test entity queries
TEST(ECS, SystemQuery) {
World world;
Entity e1 = world.createEntity();
Entity e2 = world.createEntity();
e1.addComponent<Transform>();
e1.addComponent<Sprite>();
e2.addComponent<Transform>();
// e2 has no Sprite
int count = 0;
for (auto [entity, transform, sprite] :
world.query<Transform, Sprite>()) {
count++;
}
EXPECT_EQ(count, 1);
}
Integration Tests
// Test physics simulation
TEST(Physics, GravityAndCollision) {
Engine engine;
World& world = engine.getWorld();
// Create falling entity
Entity ball = world.createEntity();
ball.addComponent<Transform>(100.0f, 0.0f);
ball.addComponent<RigidBody>();
ball.addComponent<BoxCollider>(10, 10);
// Create ground
Entity ground = world.createEntity();
ground.addComponent<Transform>(0.0f, 500.0f);
ground.addComponent<BoxCollider>(1000, 20);
ground.addComponent<StaticBody>();
// Simulate 5 seconds
for (int i = 0; i < 300; i++) {
engine.fixedUpdate(1.0f / 60.0f);
}
// Ball should have fallen and stopped on ground
auto& t = ball.get<Transform>();
EXPECT_GT(t.position.y, 400.0f);
EXPECT_LT(t.position.y, 510.0f);
EXPECT_TRUE(ball.get<RigidBody>().grounded);
}
Performance Tests
# Run with performance profiling
./engine --benchmark
# Expected output:
# Entities: 10000
# Systems Update: 2.3ms
# Collision Broad Phase: 0.8ms
# Collision Narrow Phase: 1.1ms
# Render: 1.5ms
# Total Frame: 5.7ms (175 FPS theoretical)
7. Common Pitfalls & Debugging
| Problem | Symptom | Root Cause | Fix |
|---|---|---|---|
| Stuttering | Periodic hitches | Variable timestep physics | Use fixed timestep with accumulator |
| Tunneling | Objects pass through each other | High velocity, thin colliders | Swept collision or CCD |
| Jitter | Objects vibrate | Collision resolution fighting | Add position correction threshold |
| Slow collision | FPS drops with many entities | O(n^2) collision checks | Add spatial partitioning |
| Memory leak | RAM grows over time | Entity deletion not cleaning components | Ensure component cleanup on destroy |
| Render order wrong | Sprites overlap incorrectly | No layer sorting | Sort by layer before rendering |
Debugging Techniques
// Visual debugging
void DebugRenderer::drawCollider(const Transform& t, const BoxCollider& c) {
SDL_Rect rect = {
static_cast<int>(t.position.x + c.offset.x),
static_cast<int>(t.position.y + c.offset.y),
static_cast<int>(c.width),
static_cast<int>(c.height)
};
SDL_SetRenderDrawColor(renderer, 0, 255, 0, 128);
SDL_RenderDrawRect(renderer, &rect);
}
void DebugRenderer::drawVelocity(const Transform& t, const RigidBody& rb) {
SDL_SetRenderDrawColor(renderer, 255, 0, 0, 255);
SDL_RenderDrawLine(renderer,
t.position.x, t.position.y,
t.position.x + rb.velocity.x * 0.1f,
t.position.y + rb.velocity.y * 0.1f);
}
8. Extensions & Challenges
After completing the base project:
- Sprite Batching: Batch draw calls by texture to reduce GPU overhead
- Tilemap System: Efficient rendering of large tilemaps with culling
- Particle System: Thousands of particles with pooling
- Audio System: Positional audio with SDL_mixer
- Scene Serialization: Save/load scenes to JSON or binary
- Hot Reloading: Reload assets without restarting
- Scripting: Integrate Lua for game logic
- Networking: Rollback netcode for multiplayer
- Pathfinding: A* for AI movement
- Quadtree: Replace grid with quadtree for non-uniform entity distribution
9. Real-World Connections
| Your Implementation | Production Engine |
|---|---|
| Sparse set components | Unity DOTS, EnTT |
| Fixed timestep loop | All major engines |
| Spatial grid | Simple games, Bullet Physics |
| AABB collision | Box2D, Unity 2D |
| SDL2 rendering | Many indie games |
Industry Examples:
- Celeste: Custom C# engine with precise platformer physics
- Hollow Knight: Unity with custom physics
- Factorio: Custom engine with heavy ECS optimization
- Noita: Custom engine with cellular automata physics
10. Resources
Primary Resources
- Game Programming Patterns - Free online book
- EnTT Library - Modern C++ ECS reference
- SDL2 Wiki - Official SDL documentation
- Fix Your Timestep - Essential game loop article
Video Resources
- Handmade Hero - Building a game from scratch
- The Cherno - Game Engine Series
Reference Implementations
- Bevy Engine - Rust ECS game engine
- Raylib - Simple games programming library
11. Self-Assessment Checklist
Before considering this project complete:
- Can you explain ECS advantages over OOP for games?
- Can you trace the game loop step by step?
- Can you debug collision detection issues visually?
- Can you profile and identify performance bottlenecks?
- Does the demo game run at stable 60 FPS?
- Have you handled edge cases (entity deletion during iteration, etc.)?
- Can you add a new component type in under 10 lines of code?
- Can you explain the fixed timestep interpolation formula?
12. Submission / Completion Criteria
Your project is complete when:
- Core Engine Works
- Window opens and renders
- Game loop maintains 60 FPS
- Input is processed correctly
- ECS Is Functional
- Entities can be created and destroyed
- Components can be added and removed
- Systems can query entities efficiently
- Physics Works
- Gravity affects rigid bodies
- Collisions are detected and resolved
- No tunneling for reasonable velocities
- Demo Game Is Playable
- Player can move and jump
- Platforms are solid
- At least 3 levels/screens
- Performance Meets Targets
- 60 FPS with 1000+ entities
- No memory leaks
- Deterministic physics
Deliverables:
- Source code with clear organization
- README with build instructions
- Demo video or GIF showing gameplay
- Performance profiling results