C++ Game Engine (Basic 2D) Project
A small 2D engine: sprites, input, physics, collision — then a game on top.
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
- Fixed-timestep game loops
- Entity-component design
- AABB collision detection and response
- Sprite rendering and input handling with SDL2/SFML
- Delta-time-based movement
Feature checklist
- Window + renderer with a camera
- Entities with position/velocity/sprite components
- Keyboard input system
- Gravity, velocity, and AABB collision
- Tile-map level loading from a text file
- A demo game: collect coins, avoid hazards
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
- Moving entities by fixed pixels per frame, so game speed doubles on a 144 Hz screen — always scale by dt.
- Deleting entities from the vector WHILE iterating it, invalidating the loop; flag !alive and sweep afterwards.
- Resolving collisions along the largest penetration axis (or not at all), producing sticky walls and sinking floors.
Compile and run
g++ -std=c++17 -o engine engine.cpp -lSDL2You 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 leakedTry it in your browser
No browser demo is included for this project. Build the C++ version below — that is the real program.