← All selected work

Project 03 / Engine architecture

2D Game Engine.

A small engine with visible internals: fixed-step physics, spatial-hash collision detection, explicit ownership, and interpolated rendering.

C++20 SDL2 SDL2_ttf CMake
Explore the repository

Own the path from input to pixels

I built this engine to understand the layers between a keypress and a rendered frame. It owns input handling, scene objects, physics, collision callbacks, rendering, and the lifetime of the resources those systems use.

The result is a C++20 / SDL2 engine with two complete examples: Pong and Breakout. They exercise the same component model and engine loop, rather than embedding all behavior in separate game-specific loops.

A fixed step, an interpolated frame

Frame boundary flush queued
spawns / destroys
Input + physics capture input
N fixed steps
Game logic update once
per frame
Render interpolate
previous → current

The engine accumulates elapsed time and advances physics in fixed increments, configured to 120 Hz by default. A rendered frame may contain multiple physics steps. Unconsumed time stays in the accumulator.

// Core sequence in Engine::Run
accumulator += frameTime;
while (accumulator >= fixedDt) {
  physics->Update(fixedDt, gameObjects);
  accumulator -= fixedDt;
}
Update(frameTime);
Render(accumulator / fixedDt);

The final ratio is the interpolation factor between previous and current physics positions. This separates the simulation step from the render rate. Game logic still runs once per rendered frame, so this is not a claim that the entire game is render-rate independent.

Reduce the collision search space

FIG. 04 / Find the nearby pairs 2D engine
A B C D E F G H
3 candidate pairs instead of 28.
Only check objects that share a cell. A small illustrative scene, using the engine’s broadphase rule. A shared cell is a candidate, not a confirmed collision.

The broadphase inserts each collider into every uniform-grid cell overlapped by its bounds. Only objects sharing a cell become candidates for an AABB test. A pair key prevents two large colliders from being checked repeatedly when they share several cells.

The diagram uses eight illustrative colliders and computes the shared-cell pairs from their bounds. It demonstrates candidate reduction, not a measured speedup. Dense scenes can still produce many pairs.

Detection is only half the system

Static, dynamic, and trigger colliders have different behavior. Triggers report events without positional resolution. Dynamic collisions use reflection with restitution, while active-pair tracking emits enter, stay, and exit callbacks.

Make ownership and lifetime explicit

The engine owns scene objects through unique_ptr. Each GameObject contains reusable components, including a transform. Components receive the systems they need through UpdateContext and RenderContext; there are no global system singletons.

Spawns enter a pending queue. Destruction marks an object for removal. At the next frame boundary, the engine removes its physics pairs, sends exit callbacks to surviving partners, sweeps destroyed objects, and incorporates pending additions. This keeps callbacks from leaving collision pairs pointing at deleted objects.

Reuse the expensive resources

Texture and font managers own rendering resources. Text uses dirty-state tracking so it can reuse its texture until the content changes, and fonts are cached by size.

Component objects are a choice

This engine uses component objects, not a data-oriented ECS. A game object owns heap-allocated components, and behavior is reached through virtual calls. That is convenient for small games with entity-centric logic.

Memory access / conceptual layouts
Objects
→ object → object → object
SoA
x x x x y y y y v v v v
The experiment contrasts scattered component objects with contiguous arrays of the data a movement system needs.

The separate ecs_benchmark compares three layouts for the same movement workload: heap-scattered objects, an array of structs, and parallel arrays of hot data. It explores cache locality without claiming that the engine itself has been converted to ECS.

For Pong and Breakout, component objects keep gameplay code straightforward. Moving a hot system to contiguous storage is a profiling decision, not a prerequisite for calling something an engine.

Image-file loading, audio, animation, tilemaps, and an integrated frame-time profiler remain on the repository roadmap.

Run it and inspect the source

With a C++20 compiler, CMake, SDL2, and SDL2_ttf installed:

cmake -S . -B build
cmake --build build
./build/Pong
# Or: ./build/Breakout

Next project

Shadow Index