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

By completing this project, you will:

  1. Master Entity-Component-System architecture: Understand data-oriented design and cache-friendly memory layouts
  2. Implement fixed timestep game loops: Decouple physics updates from rendering for deterministic simulation
  3. Understand graphics pipelines: Learn how sprites are rendered using SDL2 or OpenGL
  4. Build collision detection systems: Implement AABB collision with spatial partitioning
  5. Handle input systems: Process keyboard, mouse, and gamepad input efficiently
  6. Manage game resources: Load, cache, and unload textures, sounds, and other assets
  7. Create animation systems: Build sprite animation with frame timing and state machines
  8. 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:

  1. Core engine: Window management, game loop, timing
  2. ECS framework: Entities, components, systems, queries
  3. Rendering: Sprite batching, animations, layers
  4. Physics: AABB collision, velocity, gravity
  5. Input: Keyboard, mouse, action mapping
  6. Assets: Texture loading, caching, hot-reloading (optional)
  7. 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:

  1. Working game demo: A playable platformer demonstrating all engine features
  2. Reusable engine code: Can be used for future game projects
  3. Portfolio piece: Demonstrates systems programming and game development skills
  4. 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:

  1. What is cache locality and why does it matter for games?
    • Reference: “Game Engine Architecture” Chapter 3
  2. Why use fixed timestep instead of variable delta time?
    • Reference: “Game Programming Patterns” - Game Loop chapter
  3. How does AABB collision detection work?
    • Reference: “Real-Time Collision Detection” Chapter 4
  4. What are the tradeoffs of ECS vs OOP for games?
    • Reference: Unity DOTS documentation
  5. 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

  1. “Explain the advantages of ECS over traditional OOP for game development.”
    • Expected: Cache efficiency, composition over inheritance, parallelization potential, flexible entity composition
  2. “How would you handle collision detection with 10,000 entities?”
    • Expected: Spatial partitioning (grid, quadtree), broad phase + narrow phase, sleeping bodies
  3. “Why use fixed timestep for physics simulation?”
    • Expected: Determinism, stability, reproducibility, network sync
  4. “How does sprite batching improve rendering performance?”
    • Expected: Reduce draw calls, batch by texture, minimize state changes
  5. “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:

  1. Sprite Batching: Batch draw calls by texture to reduce GPU overhead
  2. Tilemap System: Efficient rendering of large tilemaps with culling
  3. Particle System: Thousands of particles with pooling
  4. Audio System: Positional audio with SDL_mixer
  5. Scene Serialization: Save/load scenes to JSON or binary
  6. Hot Reloading: Reload assets without restarting
  7. Scripting: Integrate Lua for game logic
  8. Networking: Rollback netcode for multiplayer
  9. Pathfinding: A* for AI movement
  10. 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

Video Resources

Reference Implementations


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:

  1. Core Engine Works
    • Window opens and renders
    • Game loop maintains 60 FPS
    • Input is processed correctly
  2. ECS Is Functional
    • Entities can be created and destroyed
    • Components can be added and removed
    • Systems can query entities efficiently
  3. Physics Works
    • Gravity affects rigid bodies
    • Collisions are detected and resolved
    • No tunneling for reasonable velocities
  4. Demo Game Is Playable
    • Player can move and jump
    • Platforms are solid
    • At least 3 levels/screens
  5. 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