Capstone Project 4

Simple Game Engine

Build a simple yet powerful game engine using modern C++17/20 features. Implement game loop architecture and design patterns, entity-component system design, input handling, and game state management. Create a complete framework demonstrating core game development concepts.

25-35 hours
Advanced
750 Points
What You Will Build
  • Entity Component System (ECS)
  • 2D Physics Engine
  • Sprite Renderer & Animations
  • Input & Event System
  • Audio Manager
  • Resource Management
Contents
01

Project Overview

This advanced capstone project challenges you to build a simple yet powerful game engine from scratch. You will work with the Predict Online Gaming Behavior Dataset from Kaggle containing player engagement metrics, session performance data, and gaming behavior patterns for understanding game performance optimization. The engine must demonstrate proficiency in game loop architecture, entity-component system design, input handling, and game state management using modern C++17/20 features.

Skills Applied: This project tests your proficiency in C++ templates, smart pointers, move semantics, multithreading, design patterns (Observer, Factory, Object Pool), and integration with graphics APIs (SDL2/OpenGL).
ECS Core

Entity, Component, System architecture with cache-friendly design

Physics

Rigid body dynamics, collision detection, spatial partitioning

Rendering

Sprite batching, texture atlases, animation state machines

Audio

Sound effects, music playback, 3D positional audio

Learning Objectives

Technical Skills
  • Master Entity Component System architecture patterns
  • Implement real-time 2D physics with collision response
  • Build efficient sprite rendering with GPU batching
  • Design thread-safe resource loading systems
  • Create flexible event-driven input handling
Game Dev Skills
  • Understand game loop timing and frame rate management
  • Implement scene management and state machines
  • Optimize for consistent 60 FPS performance
  • Handle multi-platform input devices
  • Design extensible and modular engine architecture
Ready to submit? Already completed the project? Submit your work now!
Submit Now
02

Company Scenario

PixelForge Studios

You have been hired as a Game Engine Developer at PixelForge Studios, an indie game development company specializing in 2D pixel art games. The studio has been using a third-party engine but wants to build their own custom engine for better control over performance and features. They need an engine that can handle their flagship title: a fast-paced action platformer with hundreds of entities on screen.

"We need a lightweight but powerful 2D engine that gives us full control. It must handle at least 1,000 active entities at 60 FPS, support complex collision detection for our platformer mechanics, and have a clean API that our game programmers can easily use. Can you build an engine that matches the performance of established frameworks while being tailored to our specific needs?"

Alex Chen, Technical Director

Technical Requirements

Performance Targets
  • Maintain 60 FPS with 1,000+ active entities
  • Physics step at fixed 50Hz (20ms intervals)
  • Sprite batch rendering (minimize draw calls)
  • Memory budget: < 100MB for 10,000 entities
Core Systems
  • Entity Component System with archetypes
  • AABB and Circle collision detection
  • Sprite sheets and animation playback
  • Input mapping for keyboard/gamepad
Architecture
  • Data-oriented design for cache efficiency
  • Component pools with contiguous memory
  • System execution order management
  • Scene serialization to JSON/binary
Extensibility
  • Plugin system for custom components
  • Scripting hooks (Lua optional bonus)
  • Debug visualization overlays
  • Profiling and performance metrics
Pro Tip: Think like an engine architect! Your engine should be generic enough to support multiple game genres (platformers, shooters, puzzles) while being optimized for the specific use case of 2D action games.
03

Test Data & Benchmarks

Download the test suite and benchmark data to validate your game engine implementation. The data includes performance targets, test scenarios, and expected outputs:

Test Suite Download

Download the game engine test data files containing benchmark scenarios, collision test cases, and performance baselines for validation.

Original Data Source

This project uses benchmark data inspired by the Predict Online Gaming Behavior Dataset from Kaggle - containing player engagement metrics, session performance data, and gaming behavior patterns ideal for understanding game performance optimization and player experience analysis.

Dataset Info: 500 test scenarios × 12 columns | Entity counts: 100 to 10,000 | Frame time targets: 16.67ms (60 FPS) | Physics timestep: 20ms fixed | Collision types: AABB, Circle, Polygon | Rendering: Batched sprites, Tilemap
Test Data Schema

ColumnTypeDescription
test_idStringUnique test identifier (e.g., ECS_001, PHY_015)
categoryStringecs, physics, rendering, audio, input, resource
test_nameStringDescriptive test name
entity_countIntegerNumber of entities in test (100-10000)
component_countIntegerComponents per entity (1-10)
expected_frame_time_msFloatMaximum allowed frame time
memory_budget_mbFloatMaximum memory usage allowed
test_duration_framesIntegerNumber of frames to run test
pass_criteriaStringSuccess condition description
priorityStringcritical, high, medium, low

ColumnTypeDescription
collision_idStringUnique collision test ID
shape_a_typeStringaabb, circle, polygon, point
shape_a_paramsStringJSON parameters for shape A
shape_b_typeStringaabb, circle, polygon, point
shape_b_paramsStringJSON parameters for shape B
expected_collisionBooleantrue if collision expected
expected_normalStringExpected collision normal vector
expected_penetrationFloatExpected penetration depth

ColumnTypeDescription
render_test_idStringUnique rendering test ID
sprite_countIntegerNumber of sprites to render
texture_countIntegerUnique textures used
batch_expectedIntegerExpected draw call count
resolutionStringScreen resolution (1920x1080, etc.)
target_fpsIntegerMinimum FPS requirement
featuresStringalpha_blend, rotation, scaling

ColumnTypeDescription
ecs_test_idStringUnique ECS test ID
operationStringcreate_entity, add_component, query, iterate
entity_countIntegerEntities involved in operation
component_typesIntegerNumber of component types
target_time_usFloatMaximum operation time (microseconds)
memory_per_entityIntegerExpected bytes per entity
cache_friendlyBooleanWhether test measures cache efficiency
Test Suite Stats: 500 test scenarios, 200 collision cases, 150 rendering benchmarks, 100 ECS performance targets
Performance Targets: 60 FPS @ 1000 entities, <16.67ms frame time, <100MB memory for 10K entities
04

Project Requirements

Your game engine must include all of the following systems. Structure your code with clean separation between engine core and game-specific code.

1
Entity Component System

Core ECS Implementation:

  • Entity manager with unique ID generation
  • Component pools with contiguous memory allocation
  • System base class with update() and fixed_update()
  • Component queries with type-safe iteration

Built-in Components:

  • TransformComponent: position, rotation, scale
  • SpriteComponent: texture, source rect, color
  • RigidbodyComponent: velocity, mass, drag
  • ColliderComponent: shape, layer, trigger flag
  • AnimationComponent: current frame, speed, loop
Deliverable: ECS core with at least 5 built-in components and ability to create custom components through templates.
2
Physics System

Collision Detection:

  • AABB vs AABB collision detection
  • Circle vs Circle collision detection
  • AABB vs Circle collision detection
  • Collision layers and masks for filtering
  • Trigger zones (collision without physics response)

Physics Simulation:

  • Fixed timestep physics loop (50Hz recommended)
  • Velocity and acceleration integration
  • Basic collision response (bounce, slide)
  • Gravity and drag forces
  • Spatial partitioning (grid or quadtree) for optimization
Deliverable: Physics system that handles 500+ colliding entities while maintaining 60 FPS performance.
3
Rendering System

Sprite Rendering:

  • Texture loading with caching (PNG, JPG support)
  • Sprite batching by texture to minimize draw calls
  • Sprite sheets with source rectangle support
  • Color tinting and alpha blending
  • Rotation and scaling transformations

Animation System:

  • Frame-based animation playback
  • Animation state machine (idle, walk, jump, etc.)
  • Animation events and callbacks
  • Sprite flip for direction changes

Camera System:

  • 2D camera with position and zoom
  • Camera follow with smoothing/lerp
  • Screen shake effects
Deliverable: Renderer capable of drawing 10,000 sprites at 60 FPS with proper batching and z-ordering.
4
Input & Audio Systems

Input System:

  • Keyboard input (pressed, held, released states)
  • Mouse input with position and button states
  • Gamepad support (optional but recommended)
  • Input action mapping (rebindable controls)
  • Event-based input callbacks

Audio System:

  • Sound effect playback (WAV, OGG)
  • Background music with looping
  • Volume control per channel
  • Audio resource caching
Deliverable: Complete input system with action mapping and audio manager with at least 8 simultaneous sound channels.
05

ECS Architecture

The Entity Component System is the heart of your game engine. Design it for cache efficiency and flexibility. Components should be stored in contiguous memory pools, and systems should iterate over components efficiently.

ECS Core Implementation
// Entity - just an ID
using Entity = uint32_t;
constexpr Entity NULL_ENTITY = 0;

// Component base - uses CRTP for type identification
template<typename T>
class Component {
public:
    static size_t GetTypeId() {
        static size_t type_id = next_type_id++;
        return type_id;
    }
private:
    static inline size_t next_type_id = 0;
};

// Example components
struct TransformComponent : Component<TransformComponent> {
    float x = 0.0f, y = 0.0f;
    float rotation = 0.0f;
    float scale_x = 1.0f, scale_y = 1.0f;
};

struct SpriteComponent : Component<SpriteComponent> {
    std::string texture_id;
    int src_x = 0, src_y = 0, src_w = 0, src_h = 0;
    uint8_t r = 255, g = 255, b = 255, a = 255;
    int z_order = 0;
};

struct RigidbodyComponent : Component<RigidbodyComponent> {
    float velocity_x = 0.0f, velocity_y = 0.0f;
    float mass = 1.0f;
    float drag = 0.0f;
    bool is_kinematic = false;
};

// Component Pool - stores components contiguously
template<typename T>
class ComponentPool {
public:
    T& Add(Entity entity) {
        size_t index = components_.size();
        components_.emplace_back();
        entity_to_index_[entity] = index;
        index_to_entity_.push_back(entity);
        return components_.back();
    }
    
    void Remove(Entity entity) {
        auto it = entity_to_index_.find(entity);
        if (it == entity_to_index_.end()) return;
        
        size_t removed_index = it->second;
        size_t last_index = components_.size() - 1;
        
        if (removed_index != last_index) {
            // Swap with last element
            components_[removed_index] = std::move(components_[last_index]);
            Entity moved_entity = index_to_entity_[last_index];
            entity_to_index_[moved_entity] = removed_index;
            index_to_entity_[removed_index] = moved_entity;
        }
        
        components_.pop_back();
        index_to_entity_.pop_back();
        entity_to_index_.erase(entity);
    }
    
    T* Get(Entity entity) {
        auto it = entity_to_index_.find(entity);
        return it != entity_to_index_.end() ? &components_[it->second] : nullptr;
    }
    
    bool Has(Entity entity) const {
        return entity_to_index_.count(entity) > 0;
    }
    
    // Iteration support
    auto begin() { return components_.begin(); }
    auto end() { return components_.end(); }
    size_t size() const { return components_.size(); }
    
private:
    std::vector<T> components_;
    std::unordered_map<Entity, size_t> entity_to_index_;
    std::vector<Entity> index_to_entity_;
};
World Manager
class World {
public:
    Entity CreateEntity() {
        return next_entity_++;
    }
    
    void DestroyEntity(Entity entity) {
        // Remove from all component pools
        for (auto& [type_id, pool] : pools_) {
            pool->RemoveIfExists(entity);
        }
        destroyed_entities_.push_back(entity);
    }
    
    template<typename T, typename... Args>
    T& AddComponent(Entity entity, Args&&... args) {
        auto& pool = GetOrCreatePool<T>();
        T& component = pool.Add(entity);
        ((component.*args.first = args.second), ...); // Optional initializer
        return component;
    }
    
    template<typename T>
    T* GetComponent(Entity entity) {
        auto pool = GetPool<T>();
        return pool ? pool->Get(entity) : nullptr;
    }
    
    template<typename T>
    bool HasComponent(Entity entity) {
        auto pool = GetPool<T>();
        return pool && pool->Has(entity);
    }
    
    template<typename... Components>
    auto View() {
        // Returns iterator over entities with all specified components
        return EntityView<Components...>(*this);
    }
    
    void Update(float delta_time) {
        for (auto& system : systems_) {
            system->Update(*this, delta_time);
        }
    }
    
    void FixedUpdate(float fixed_delta_time) {
        for (auto& system : systems_) {
            system->FixedUpdate(*this, fixed_delta_time);
        }
    }
    
private:
    Entity next_entity_ = 1;
    std::vector<Entity> destroyed_entities_;
    std::unordered_map<size_t, std::unique_ptr<IComponentPool>> pools_;
    std::vector<std::unique_ptr<System>> systems_;
};
Performance Tip: Use View<Transform, Sprite>() to efficiently iterate only over entities that have both components. This avoids checking every entity individually.
06

Physics System

The physics system handles collision detection and response. Use a fixed timestep for deterministic physics simulation and implement spatial partitioning for efficient broad-phase collision detection.

Collision Detection Algorithms
struct AABB {
    float x, y, width, height;
    
    float Left() const { return x; }
    float Right() const { return x + width; }
    float Top() const { return y; }
    float Bottom() const { return y + height; }
    
    bool Contains(float px, float py) const {
        return px >= Left() && px <= Right() &&
               py >= Top() && py <= Bottom();
    }
};

struct Circle {
    float x, y, radius;
};

// AABB vs AABB
bool CheckCollision(const AABB& a, const AABB& b) {
    return a.Left() < b.Right() && a.Right() > b.Left() &&
           a.Top() < b.Bottom() && a.Bottom() > b.Top();
}

// Circle vs Circle
bool CheckCollision(const Circle& a, const Circle& b) {
    float dx = b.x - a.x;
    float dy = b.y - a.y;
    float distance_sq = dx * dx + dy * dy;
    float radius_sum = a.radius + b.radius;
    return distance_sq < radius_sum * radius_sum;
}

// AABB vs Circle
bool CheckCollision(const AABB& aabb, const Circle& circle) {
    // Find closest point on AABB to circle center
    float closest_x = std::clamp(circle.x, aabb.Left(), aabb.Right());
    float closest_y = std::clamp(circle.y, aabb.Top(), aabb.Bottom());
    
    float dx = circle.x - closest_x;
    float dy = circle.y - closest_y;
    return (dx * dx + dy * dy) < (circle.radius * circle.radius);
}

// Collision manifold for response
struct CollisionManifold {
    bool collided = false;
    float normal_x = 0.0f, normal_y = 0.0f;  // Collision normal
    float penetration = 0.0f;                 // Penetration depth
};

CollisionManifold GetManifold(const AABB& a, const AABB& b) {
    CollisionManifold m;
    
    float overlap_x = std::min(a.Right(), b.Right()) - std::max(a.Left(), b.Left());
    float overlap_y = std::min(a.Bottom(), b.Bottom()) - std::max(a.Top(), b.Top());
    
    if (overlap_x <= 0 || overlap_y <= 0) return m;
    
    m.collided = true;
    
    // Use smallest overlap as separation axis
    if (overlap_x < overlap_y) {
        m.penetration = overlap_x;
        m.normal_x = (a.x < b.x) ? -1.0f : 1.0f;
        m.normal_y = 0.0f;
    } else {
        m.penetration = overlap_y;
        m.normal_x = 0.0f;
        m.normal_y = (a.y < b.y) ? -1.0f : 1.0f;
    }
    
    return m;
}
Collision Response
void ResolveCollision(RigidbodyComponent& rb_a, RigidbodyComponent& rb_b,
                      TransformComponent& tf_a, TransformComponent& tf_b,
                      const CollisionManifold& manifold) {
    if (!manifold.collided) return;
    
    // Skip if both are kinematic
    if (rb_a.is_kinematic && rb_b.is_kinematic) return;
    
    // Calculate relative velocity
    float rel_vel_x = rb_b.velocity_x - rb_a.velocity_x;
    float rel_vel_y = rb_b.velocity_y - rb_a.velocity_y;
    
    // Relative velocity along collision normal
    float vel_along_normal = rel_vel_x * manifold.normal_x + 
                             rel_vel_y * manifold.normal_y;
    
    // Don't resolve if velocities are separating
    if (vel_along_normal > 0) return;
    
    // Restitution (bounciness) - use minimum of both
    float restitution = 0.3f;  // Could be stored in RigidbodyComponent
    
    // Calculate impulse scalar
    float inv_mass_a = rb_a.is_kinematic ? 0.0f : 1.0f / rb_a.mass;
    float inv_mass_b = rb_b.is_kinematic ? 0.0f : 1.0f / rb_b.mass;
    
    float impulse_scalar = -(1.0f + restitution) * vel_along_normal;
    impulse_scalar /= inv_mass_a + inv_mass_b;
    
    // Apply impulse
    float impulse_x = impulse_scalar * manifold.normal_x;
    float impulse_y = impulse_scalar * manifold.normal_y;
    
    if (!rb_a.is_kinematic) {
        rb_a.velocity_x -= inv_mass_a * impulse_x;
        rb_a.velocity_y -= inv_mass_a * impulse_y;
    }
    if (!rb_b.is_kinematic) {
        rb_b.velocity_x += inv_mass_b * impulse_x;
        rb_b.velocity_y += inv_mass_b * impulse_y;
    }
    
    // Positional correction (prevent sinking)
    const float percent = 0.8f;   // Penetration percentage to correct
    const float slop = 0.01f;     // Penetration allowance
    
    float correction = std::max(manifold.penetration - slop, 0.0f) /
                       (inv_mass_a + inv_mass_b) * percent;
    
    float correction_x = correction * manifold.normal_x;
    float correction_y = correction * manifold.normal_y;
    
    if (!rb_a.is_kinematic) {
        tf_a.x -= inv_mass_a * correction_x;
        tf_a.y -= inv_mass_a * correction_y;
    }
    if (!rb_b.is_kinematic) {
        tf_b.x += inv_mass_b * correction_x;
        tf_b.y += inv_mass_b * correction_y;
    }
}
Spatial Hash Grid
class SpatialHashGrid {
public:
    SpatialHashGrid(float cell_size = 64.0f) : cell_size_(cell_size) {}
    
    void Clear() {
        cells_.clear();
    }
    
    void Insert(Entity entity, const AABB& bounds) {
        int min_x = static_cast<int>(std::floor(bounds.Left() / cell_size_));
        int max_x = static_cast<int>(std::floor(bounds.Right() / cell_size_));
        int min_y = static_cast<int>(std::floor(bounds.Top() / cell_size_));
        int max_y = static_cast<int>(std::floor(bounds.Bottom() / cell_size_));
        
        for (int y = min_y; y <= max_y; ++y) {
            for (int x = min_x; x <= max_x; ++x) {
                cells_[HashCell(x, y)].push_back(entity);
            }
        }
    }
    
    std::vector<Entity> Query(const AABB& bounds) const {
        std::unordered_set<Entity> found;
        
        int min_x = static_cast<int>(std::floor(bounds.Left() / cell_size_));
        int max_x = static_cast<int>(std::floor(bounds.Right() / cell_size_));
        int min_y = static_cast<int>(std::floor(bounds.Top() / cell_size_));
        int max_y = static_cast<int>(std::floor(bounds.Bottom() / cell_size_));
        
        for (int y = min_y; y <= max_y; ++y) {
            for (int x = min_x; x <= max_x; ++x) {
                auto it = cells_.find(HashCell(x, y));
                if (it != cells_.end()) {
                    for (Entity e : it->second) {
                        found.insert(e);
                    }
                }
            }
        }
        
        return std::vector<Entity>(found.begin(), found.end());
    }
    
    // Get potential collision pairs (broad phase)
    std::vector<std::pair<Entity, Entity>> GetPotentialPairs() const {
        std::set<std::pair<Entity, Entity>> pairs;
        
        for (const auto& [hash, entities] : cells_) {
            for (size_t i = 0; i < entities.size(); ++i) {
                for (size_t j = i + 1; j < entities.size(); ++j) {
                    Entity a = std::min(entities[i], entities[j]);
                    Entity b = std::max(entities[i], entities[j]);
                    pairs.insert({a, b});
                }
            }
        }
        
        return std::vector<std::pair<Entity, Entity>>(pairs.begin(), pairs.end());
    }
    
private:
    size_t HashCell(int x, int y) const {
        // Simple hash combining x and y
        return std::hash<int>{}(x) ^ (std::hash<int>{}(y) << 16);
    }
    
    float cell_size_;
    std::unordered_map<size_t, std::vector<Entity>> cells_;
};
Fixed Timestep Game Loop
class Engine {
public:
    void Run() {
        const float FIXED_TIMESTEP = 1.0f / 50.0f;  // 50 Hz physics
        const float MAX_FRAME_TIME = 0.25f;          // Cap to prevent spiral
        
        auto previous_time = std::chrono::high_resolution_clock::now();
        float accumulator = 0.0f;
        
        while (is_running_) {
            auto current_time = std::chrono::high_resolution_clock::now();
            float frame_time = std::chrono::duration<float>(
                current_time - previous_time).count();
            previous_time = current_time;
            
            // Cap frame time to avoid spiral of death
            if (frame_time > MAX_FRAME_TIME) {
                frame_time = MAX_FRAME_TIME;
            }
            
            accumulator += frame_time;
            
            // Process input (once per frame)
            ProcessInput();
            
            // Fixed timestep updates (physics)
            while (accumulator >= FIXED_TIMESTEP) {
                world_.FixedUpdate(FIXED_TIMESTEP);
                accumulator -= FIXED_TIMESTEP;
            }
            
            // Variable timestep updates (rendering, animations)
            float alpha = accumulator / FIXED_TIMESTEP;  // For interpolation
            world_.Update(frame_time);
            
            // Render with interpolation
            Render(alpha);
            
            // Frame limiting (optional, depends on vsync)
            LimitFrameRate(60);
        }
    }
    
private:
    void ProcessInput() {
        SDL_Event event;
        while (SDL_PollEvent(&event)) {
            if (event.type == SDL_QUIT) {
                is_running_ = false;
            }
            input_system_->ProcessEvent(event);
        }
    }
    
    void Render(float interpolation_alpha) {
        renderer_->Clear();
        render_system_->Render(world_, interpolation_alpha);
        renderer_->Present();
    }
    
    void LimitFrameRate(int target_fps) {
        static auto frame_start = std::chrono::high_resolution_clock::now();
        auto frame_end = std::chrono::high_resolution_clock::now();
        
        float target_frame_time = 1.0f / target_fps;
        float elapsed = std::chrono::duration<float>(
            frame_end - frame_start).count();
        
        if (elapsed < target_frame_time) {
            int sleep_ms = static_cast<int>(
                (target_frame_time - elapsed) * 1000);
            SDL_Delay(sleep_ms);
        }
        
        frame_start = std::chrono::high_resolution_clock::now();
    }
    
    bool is_running_ = true;
    World world_;
    std::unique_ptr<InputSystem> input_system_;
    std::unique_ptr<RenderSystem> render_system_;
    std::unique_ptr<Renderer> renderer_;
};
07

Rendering Pipeline

Efficient rendering is crucial for game performance. Implement sprite batching to minimize draw calls and texture atlases to reduce texture switches.

Sprite Batch Renderer
struct Vertex {
    float x, y;           // Position
    float u, v;           // Texture coordinates
    uint8_t r, g, b, a;   // Color
};

class SpriteBatch {
public:
    static constexpr size_t MAX_SPRITES = 10000;
    static constexpr size_t VERTICES_PER_SPRITE = 4;
    static constexpr size_t INDICES_PER_SPRITE = 6;
    
    SpriteBatch() {
        vertices_.reserve(MAX_SPRITES * VERTICES_PER_SPRITE);
        indices_.reserve(MAX_SPRITES * INDICES_PER_SPRITE);
    }
    
    void Begin() {
        vertices_.clear();
        indices_.clear();
        current_texture_ = nullptr;
        draw_calls_ = 0;
    }
    
    void Draw(Texture* texture, float x, float y, float w, float h,
              float src_x, float src_y, float src_w, float src_h,
              uint8_t r = 255, uint8_t g = 255, uint8_t b = 255, uint8_t a = 255,
              float rotation = 0.0f, float origin_x = 0.0f, float origin_y = 0.0f) {
        
        // Flush if texture changes or batch is full
        if (texture != current_texture_ || 
            vertices_.size() >= MAX_SPRITES * VERTICES_PER_SPRITE) {
            Flush();
            current_texture_ = texture;
        }
        
        // Calculate UV coordinates
        float tex_w = static_cast<float>(texture->GetWidth());
        float tex_h = static_cast<float>(texture->GetHeight());
        float u0 = src_x / tex_w, v0 = src_y / tex_h;
        float u1 = (src_x + src_w) / tex_w, v1 = (src_y + src_h) / tex_h;
        
        // Apply rotation if needed
        std::array<float, 8> positions;
        if (std::abs(rotation) > 0.001f) {
            float cos_r = std::cos(rotation);
            float sin_r = std::sin(rotation);
            
            // Rotate around origin
            auto rotate = [&](float px, float py) -> std::pair<float, float> {
                float rx = px - origin_x;
                float ry = py - origin_y;
                return {
                    x + origin_x + rx * cos_r - ry * sin_r,
                    y + origin_y + rx * sin_r + ry * cos_r
                };
            };
            
            auto [x0, y0] = rotate(0, 0);
            auto [x1, y1] = rotate(w, 0);
            auto [x2, y2] = rotate(w, h);
            auto [x3, y3] = rotate(0, h);
            
            positions = {x0, y0, x1, y1, x2, y2, x3, y3};
        } else {
            positions = {x, y, x + w, y, x + w, y + h, x, y + h};
        }
        
        // Add vertices
        uint32_t base_index = static_cast<uint32_t>(vertices_.size());
        
        vertices_.push_back({positions[0], positions[1], u0, v0, r, g, b, a});
        vertices_.push_back({positions[2], positions[3], u1, v0, r, g, b, a});
        vertices_.push_back({positions[4], positions[5], u1, v1, r, g, b, a});
        vertices_.push_back({positions[6], positions[7], u0, v1, r, g, b, a});
        
        // Add indices (two triangles per quad)
        indices_.push_back(base_index);
        indices_.push_back(base_index + 1);
        indices_.push_back(base_index + 2);
        indices_.push_back(base_index);
        indices_.push_back(base_index + 2);
        indices_.push_back(base_index + 3);
    }
    
    void End() {
        Flush();
    }
    
    int GetDrawCallCount() const { return draw_calls_; }
    
private:
    void Flush() {
        if (vertices_.empty() || !current_texture_) return;
        
        // Bind texture and upload vertex data
        current_texture_->Bind();
        
        // Upload to GPU and draw (implementation depends on graphics API)
        // For SDL2: SDL_RenderGeometry()
        // For OpenGL: glBufferData() + glDrawElements()
        
        draw_calls_++;
        vertices_.clear();
        indices_.clear();
    }
    
    std::vector<Vertex> vertices_;
    std::vector<uint32_t> indices_;
    Texture* current_texture_ = nullptr;
    int draw_calls_ = 0;
};
Animation System
struct AnimationFrame {
    int src_x, src_y, src_w, src_h;  // Source rectangle in sprite sheet
    float duration;                   // Frame duration in seconds
};

struct Animation {
    std::string name;
    std::vector<AnimationFrame> frames;
    bool loop = true;
};

class AnimationController {
public:
    void AddAnimation(const std::string& name, Animation animation) {
        animations_[name] = std::move(animation);
    }
    
    void Play(const std::string& name, bool force_restart = false) {
        if (current_animation_ != name || force_restart) {
            current_animation_ = name;
            current_frame_ = 0;
            frame_time_ = 0.0f;
        }
    }
    
    void Update(float delta_time) {
        auto it = animations_.find(current_animation_);
        if (it == animations_.end()) return;
        
        const Animation& anim = it->second;
        if (anim.frames.empty()) return;
        
        frame_time_ += delta_time;
        
        while (frame_time_ >= anim.frames[current_frame_].duration) {
            frame_time_ -= anim.frames[current_frame_].duration;
            current_frame_++;
            
            if (current_frame_ >= anim.frames.size()) {
                if (anim.loop) {
                    current_frame_ = 0;
                } else {
                    current_frame_ = anim.frames.size() - 1;
                    is_finished_ = true;
                    break;
                }
            }
        }
    }
    
    const AnimationFrame& GetCurrentFrame() const {
        return animations_.at(current_animation_).frames[current_frame_];
    }
    
    bool IsFinished() const { return is_finished_; }
    
private:
    std::unordered_map<std::string, Animation> animations_;
    std::string current_animation_;
    size_t current_frame_ = 0;
    float frame_time_ = 0.0f;
    bool is_finished_ = false;
};
Optimization Tip: Sort sprites by texture before rendering to maximize batching efficiency. Use a texture atlas to pack multiple sprites into a single texture.
08

Submission Requirements

Create a public GitHub repository with the exact name shown below:

Required Repository Name
cpp-simple-game-engine
github.com/<your-username>/cpp-simple-game-engine
Required Project Structure
cpp-simple-game-engine/
├── include/
│   └── engine/
│       ├── core/
│       │   ├── engine.hpp
│       │   ├── world.hpp
│       │   └── entity.hpp
│       ├── ecs/
│       │   ├── component.hpp
│       │   ├── component_pool.hpp
│       │   └── system.hpp
│       ├── components/
│       │   ├── transform.hpp
│       │   ├── sprite.hpp
│       │   ├── rigidbody.hpp
│       │   ├── collider.hpp
│       │   └── animation.hpp
│       ├── systems/
│       │   ├── physics_system.hpp
│       │   ├── render_system.hpp
│       │   ├── animation_system.hpp
│       │   └── input_system.hpp
│       ├── physics/
│       │   ├── collision.hpp
│       │   └── spatial_hash.hpp
│       ├── rendering/
│       │   ├── renderer.hpp
│       │   ├── sprite_batch.hpp
│       │   └── texture.hpp
│       ├── audio/
│       │   └── audio_manager.hpp
│       └── input/
│           └── input_manager.hpp
├── src/
│   └── ... (implementation files)
├── examples/
│   └── platformer/
│       ├── main.cpp
│       └── assets/
├── tests/
│   ├── test_ecs.cpp
│   ├── test_collision.cpp
│   └── test_rendering.cpp
├── benchmarks/
│   └── benchmark_ecs.cpp
├── docs/
│   └── API.md
├── CMakeLists.txt
└── README.md
Do Include
  • Complete engine source code with headers
  • Working example game (simple platformer)
  • Unit tests for ECS and collision
  • Performance benchmarks
  • API documentation (Doxygen or markdown)
  • CMakeLists.txt for cross-platform build
Do Not Include
  • Pre-built binaries or object files
  • IDE-specific project files (.vs, .idea)
  • Large asset files (>10MB images/audio)
  • Third-party library source (use submodules)
  • Build directories (build/, bin/, obj/)
Submit Your Project

Enter your GitHub username - we will verify your repository automatically

09

Grading Rubric

Your project will be graded on the following criteria. Total: 750 points.

Criteria Points Description
ECS Architecture 150 Entity/Component/System design, component pools, queries, iteration efficiency
Physics System 125 Collision detection (AABB, Circle), response, spatial partitioning, fixed timestep
Rendering System 125 Sprite batching, texture management, animations, camera system
Input & Audio 75 Keyboard/mouse input, action mapping, sound effects, music playback
Performance 100 60 FPS with 1000+ entities, efficient memory usage, minimal draw calls
Example Game 75 Working demo showcasing engine features (movement, collision, sprites)
Code Quality 50 Modern C++17/20, RAII, const-correctness, naming conventions
Documentation 50 README, API docs, code comments, architecture overview
Total 750
Grading Levels
Excellent
675-750

Exceeds all requirements with exceptional quality

Good
563-674

Meets all requirements with good quality

Satisfactory
450-562

Meets minimum requirements

Needs Work
< 450

Missing key requirements

Ready to Submit?

Make sure you have completed all requirements and reviewed the grading rubric above.

Submit Your Project