Kelick Tech Hub

Build, learn & keep up — tech, Linux & sports news included.

C++ Game Engine (Basic 2D) Project

Advanced•2–3 weeks•game loop, entities, rendering

A small 2D engine: sprites, input, physics, collision — then a game on top.

Share on X

What you will build

Build the engine, not just a game: a fixed-timestep loop, an entity system with components (position, sprite, velocity), AABB collision, and a renderer (SDL2 or SFML) — then prove it with a small playable game. This is how real engines are structured, at 1% of the size.

What you will learn

Feature checklist

Starter code

// Engine core sketch (renderer-agnostic)
#include <vector>
#include <algorithm>

struct Entity {
    float x, y, w, h;       // position + size
    float vx = 0, vy = 0;   // velocity
    bool solid = true;
    bool alive = true;
    int sprite = 0;
};

bool overlaps(const Entity& a, const Entity& b) {
    return a.x < b.x + b.w && a.x + a.w > b.x &&
           a.y < b.y + b.h && a.y + a.h > b.y;
}

class World {
    std::vector<Entity> ents_;
public:
    Entity& spawn(Entity e) { ents_.push_back(e); return ents_.back(); }
    void update(float dt) {
        for (auto& e : ents_) {
            if (!e.alive) continue;
            e.x += e.vx * dt;
            e.y += e.vy * dt;
        }
        // Naive pairwise collision: resolve by stopping overlap
        for (size_t i = 0; i < ents_.size(); ++i)
            for (size_t j = i + 1; j < ents_.size(); ++j)
                if (ents_[i].solid && ents_[j].solid &&
                    overlaps(ents_[i], ents_[j])) {
                    ents_[i].vx = ents_[j].vx = 0;  // simple response
                }
        std::remove_if(ents_.begin(), ents_.end(),
                       [](const Entity& e) { return !e.alive; });
    }
};

// Loop: poll input -> world.update(1/60) -> draw every entity.

Full walkthrough

Step 1: Separate the engine core from the renderer

The starter's core — Entity, World, overlaps() — includes NO graphics headers. Entity { x, y, w, h, vx, vy, solid, alive, sprite } is pure data; World owns the entity vector. An SDL window/renderer is plugged in at the edges (input in, sprites out). This separation is what lets you unit-test physics without opening a window, and swap SDL for SFML without touching the game logic.

Step 2: The game loop: input, update, render, pace

Every engine is the Snake loop grown up: poll events into an input state (which keys are DOWN, not just pressed), call world.update(dt), render, and pace to ~60 fps. dt — the real elapsed seconds since the last frame, from a chrono clock — scales all movement (x += vx * dt) so the game runs at the same speed on a 60 Hz laptop and a 144 Hz monitor. Fixed-pixel-per-frame movement is the beginner tell this project exists to cure.

Step 3: Spawn entities and update the world

World::spawn() creates an entity (player, coin, hazard, tile block) and returns a reference/id. update(dt) applies gravity to vy for affected entities, integrates positions, resolves collisions (next step), and sweeps out entities flagged !alive using the erase-remove idiom — with std::remove_if over the vector exactly as the starter sketches. Order matters: integrate, collide, then sweep.

void World::update(float dt) {
    for (auto& e : entities) {
        e.vy += GRAVITY * dt;
        e.x += e.vx * dt;
        e.y += e.vy * dt;
        // ... collision resolution ...
    }
    entities.erase(std::remove_if(entities.begin(), entities.end(),
        [](const Entity& e) { return !e.alive; }), entities.end());
}

Step 4: AABB collision: overlaps() plus resolution

overlaps(a, b) is the classic axis-aligned box test: a.x < b.x + b.w && a.x + a.w > b.x && (same for y). Detection alone leaves entities interpenetrating — resolution pushes the mover back along the SMALLEST penetration axis and zeroes that velocity component (landing on a floor kills vy, which is what makes standing possible). Gravity + AABB resolution is the complete physics of a platformer.

Step 5: Load levels from a text tile map

Read rows of characters from a file: '#' spawns a solid block at (col × TILE, row × TILE), 'C' a coin, 'H' a hazard, 'P' the player start. Data-driven levels mean new levels are text files, not recompiles — the moment a game becomes a platform for content. The camera follows the player: render coordinates are world coordinates minus the camera offset, clamped to the level bounds.

Step 6: Build the demo game on top: collect coins, avoid hazards

Coins: overlap with the player → alive = false, score++. Hazards: overlap → player respawns at 'P', lives--. Win when every coin is collected. Notice the demo game contains almost no code of its own — it is engine features composed. That ratio (95% engine, 5% game) is the honest shape of real engine work.

Step 7: Test the physics, not the pixels

Verify with numbers: drop the player from a known height and check landing stops exactly on the floor's top edge; run into a wall mid-jump and confirm no tunnelling at your max speed (if there is — cap dt and substep); collect every coin and confirm the win fires once. Engines fail in milliseconds and pixels — tests you can state numerically are the only ones that catch it.

Common mistakes

Compile and run

g++ -std=c++17 -o engine engine.cpp -lSDL2

You need a multimedia library: install SDL2 (Linux: sudo apt install libsdl2-dev; Windows: SDL2 dev package via vcpkg or the SDL site; macOS: brew install sdl2) and link -lSDL2 as shown — or use SFML (-lsfml-graphics -lsfml-window -lsfml-system) with the same architecture. The starter code is renderer-agnostic, so the engine design below is identical either way.

Sample run

$ ./engine
Engine boot: SDL2 window 960x540 · renderer ready
Loaded level1.txt — 40 x 12 tiles · 6 coins · 3 hazards · player start found
Running at 60 fps (dt ~16.7 ms) — the demo game: collect all coins, avoid hazards
[game] player landed on the floor (gravity + AABB resolution OK)
[game] coin collected (1/6)
[game] coin collected (2/6)
[game] touched a hazard — respawning at start (lives: 2)
[game] coin collected (3/6)
[game] coin collected (4/6)
[game] coin collected (5/6)
[game] coin collected (6/6)
[game] ALL COINS COLLECTED — you win! Time: 1:23.4
Engine shutdown: window closed, renderer destroyed, 0 entities leaked

Try it in your browser

No browser demo is included for this project. Build the C++ version below — that is the real program.

Related Advanced projects

← All 60 projects · Classic site (Learn & Sandbox)

Download Fake GPS