KnowSys
◆Build it yourself

Build a game engine

A small 2D engine with a fixed-timestep loop, an entity-component system, a batched sprite renderer, input, collisions and sound, built so it can grow a third dimension later.

Ambitious⏱ 8–14 weekendsC++ · Rust · C · Zig

A game is a program with a deadline it can never miss. Every 16.7 milliseconds it has to read the controller, move everything in the world, work out what touched what, draw a fresh picture and keep the speakers fed, and then do it all again. An engine is the code that makes that deadline routine, so the game on top can think about jumping and not about vertex buffers.

Don't try to build Unreal. Build a 2D engine you can finish: a window, a loop that keeps honest time, sprites on the GPU, entities, collisions and sound, with a small game on top to prove it works. Once that's done you can grow it toward 3D. Your loop, entity system, input and audio carry over as they are; only the renderer and the physics have to get bigger.

01Why build this

Game engines are where performance stops being a nice-to-have and becomes a hard real-time budget. That changes how you think about code you write anywhere:

  • Memory layout becomes visible. Iterating 50,000 entities stored as a pointer-chasing object graph versus a packed array is the difference between missing the frame and not. Chapter 02 stops being theory.
  • Time gets harder than it looks. Fixed timesteps, interpolation and determinism are the same problems you meet in simulations, replay systems and anything that has to agree on "what happened when".
  • The GPU stops being a black box. You'll see why draw calls cost what they cost, why batching matters, and what a shader actually receives.
  • Real-time audio teaches the strictest threading rule there is. The audio callback can't block, allocate or wait on a lock, or the user hears it.
  • Data-oriented design makes sense. An entity-component system is, in practice, a small in-memory column store with a query language. Once you've built one, a lot of database and analytics code looks familiar.

It's also a project with a built-in reviewer. Either the game feels right when you play it or it doesn't, and you can tell within about ten seconds.

02What you're building

Every frame, the finished engine runs these stages in order:

One frame of your engine
●
⌁
Events
OS pump
⌨
Input
actions
⟳
Simulate
fixed steps
◫
Physics
collide
▦
Render
batch
▶
Present
swap
Step 1. Drain the operating system's event queue: key presses, mouse motion, gamepad connects, window resizes and the close button.
1 / 6

?Why does the simulation run on a different clock from the renderer?

Physics code is only stable and repeatable when it advances by the same dt every time. Displays refresh at 60, 120 or 144 Hz, and frames sometimes take longer than planned. So the simulation ticks at a fixed rate, the renderer draws as often as the display allows, and it blends the last two simulation states to hide the difference. Glenn Fiedler's "Fix Your Timestep!" is the classic explanation, and it's probably the single most useful page for this project.

03Before you start

You needWhyWhere to get it
A language that lets you control memory layoutEntity storage and batching depend on packed arraysC, C++, Rust or Zig
A platform layerWindows, input and a GPU context on every OSSDL3, GLFW or sokol_app; raylib if you want to postpone the GPU
A graphics API you can learn fromDrawing sprites and, later, meshesOpenGL 3.3 core with LearnOpenGL, or wgpu / sokol_gfx for a modern API
An audio output libraryTalking to the OS sound deviceSDL's audio API or miniaudio
Vectors and matricesTransforms, cameras and collisionAny game maths primer; you need 2D first, 4x4 matrices for 3D
A tiny game to buildA target keeps the scope honestPong, Breakout or a one-screen platformer

04The roadmap

Eight milestones. After each one you have something that runs, and the order is chosen so that nothing has to be torn out later.

1

A window and an honest frame timer

1 evening

Open a window, create a GPU context, and write the loop: pump events, clear the screen, swap buffers. Measure each frame with a monotonic, high-resolution clock, never wall-clock time, which can jump when the system adjusts it.

Log the frame times and look at them. You'll see that vsync on and vsync off behave very differently, and that even an empty frame isn't perfectly regular. Keep that log; you'll use it in every later milestone.

You’ll learnplatform layerevent pumpvsyncmonotonic clocks
Done when: A window opens, clears to a colour, closes cleanly from the close button, and logs a frame time that sits near 16.7 ms with vsync on.
2

Fixed timestep with interpolation

1 evening

Split the loop in two: update(dt) with a constant dt, and render(alpha) where alpha is how far you are between the last two steps. Its core is a few lines:

C
acc += min(frame_time, 0.25);
while (acc >= DT) { prev = curr; update(&curr, DT); acc -= DT; }
render(lerp(prev, curr, acc / DT));

Don't drop the min. Without it, one slow frame makes the next frame run extra steps, which makes it slower, which asks for more steps. That's the "spiral of death", and clamping the frame time is the standard way out.

You’ll learnaccumulatorfixed dtinterpolationspiral of death
Done when: A square moves at the same speed with vsync at 60 Hz, vsync off, and an artificial 30 fps cap, and a replay from the same inputs lands on the same position every run.
3

Sprites on the GPU

1–2 weekends

Write one vertex shader and one fragment shader, load a PNG into a texture, and draw a textured quad through an orthographic projection so one unit is one pixel. Then do the naive thing, one draw call per sprite, and measure it.

Now fix it. Put your sprites on a texture atlas, write every quad for the frame into one big vertex buffer, and draw it in a single call. Sort by texture and layer first. That jump in throughput is probably the most useful lesson about GPUs in the project: they're fast at big batches and slow at many small requests.

You’ll learnvertex buffersshaderstexture atlasbatchingorthographic projection
Done when: Ten thousand moving sprites draw in a handful of draw calls, and your frame log shows the cost barely changes from one hundred sprites to ten thousand.
4

Input as actions

1 evening

Keep two snapshots of every key and button: this frame and last frame. From those you get held, pressed and released. Games care about the edges far more than the state; a jump that fires while the key is held feels broken immediately.

Then add a layer of actions. Game code asks for jump or a move_x axis, and a small table maps keys, mouse buttons and gamepad inputs onto them. Be careful with fixed steps here: a press that happens between two updates has to be seen by exactly one of them.

You’ll learnkey stateedge detectionaction mappinggamepads
Done when: jump fires exactly once per press at any frame rate, and rebinding it from Space to a gamepad button changes no game code.
5

Entities and components

1–2 weekends

An entity is just an ID. Components are plain data, like Position, Velocity or Sprite, stored in packed arrays. Systems are functions that iterate every entity with a given set of components. Choose a storage model: sparse sets are simpler and make adding components cheap, while archetypes group entities by their exact component set and iterate faster.

Make IDs generational: an index plus a counter that bumps when the slot is reused. Otherwise a system holding the ID of a dead enemy will quietly start steering whatever spawned into its slot.

You’ll learnECSstruct of arraysarchetypes vs sparse setsgenerational IDs
Done when: Spawning and destroying 100,000 entities leaks nothing, and a stale entity ID returns 'not found' instead of a recycled entity.
6

Collision and a little physics

2 weekends

Start with axis-aligned boxes and move one axis at a time, resolving overlaps after each. That alone probably handles most platformers. Then add a broad phase, such as a uniform grid or spatial hash, so you only test pairs that share a cell. Checking every pair against every other is quadratic and falls over surprisingly early.

Fast objects will tunnel: in one step they're in front of a wall, in the next they're behind it, and they never overlapped. Fix it with swept tests or by sub-stepping fast bodies. Keep this separate from game logic, which should only hear about collisions as events.

You’ll learnAABB testsbroad phasespatial hashingtunnellingcollision response
Done when: A player lands on platforms without sinking or jittering, and a fast projectile can't pass through a one-tile wall.
7

Sound that never clicks

1 weekend

Your OS calls the audio callback on its own thread, asking for the next few milliseconds of samples. Mix every playing voice into that buffer, apply volume and panning, and clamp the result so it can't clip past full scale.

Inside it there's a hard rule: no locks, no allocation, no file I/O. Your game thread sends commands like "play this sound" through a single-producer, single-consumer ring buffer, and all the sound data is decoded before it's needed. If you've read Chapter 15, this is a real use for everything in it.

You’ll learnaudio callbackmixingsample rateslock-free queues
Done when: Twenty overlapping effects and a music track play without pops, and a deliberate 200 ms stall in your update loop doesn't glitch the audio.
8

Ship a game, then add a dimension

2–4 weekends

Finish the game. Add a title screen, a restart, and a way to load levels from a file. This is where you'll find which parts of the engine are awkward, because you'll be the one fighting them.

Then take the first step into 3D: a perspective projection, a camera, a depth buffer, and one mesh loaded from glTF. Your loop, ECS, input and audio shouldn't change at all. If they do, that's a design problem worth fixing now, before lighting and shadows pile on.

You’ll learnasset loadingscenesperspective projectiondepth bufferglTF
Done when: Someone else plays your one-screen game from title screen to game over, and a textured glTF model spins under a perspective camera with correct depth.

05Traps that catch everyone

SymptomCauseFix
Your game runs faster on a 144 Hz monitorMovement is counted per frame, not per unit of timeFixed timestep from milestone 2
Motion stutters even at a high frame rateYou render fixed-step state without interpolation, or you time frames with a coarse clockInterpolate between the last two states; use a monotonic high-resolution timer
One hitch freezes the game for secondsThe accumulator runs ever more steps to catch upClamp frame time before adding it to the accumulator
Thin lines appear between tilesTexture filtering samples neighbouring atlas cells, or sprites sit on fractional pixelsPad atlas cells, use nearest filtering for pixel art, snap to whole pixels
Audio crackles or popsThe callback blocks on a lock, allocates, or underrunsPreallocate, pass commands through a lock-free queue, raise the buffer size
Bullets pass through wallsDiscrete collision at high speedSwept tests or sub-steps for fast bodies
Crash when an enemy dies mid-updateDestroying entities while iterating their arraysQueue destroys and apply them at the end of the frame

06Stretch goals

  • Hot-reload game code. Put the game in a shared library and reload it while the engine keeps running, the way Handmade Hero does.
  • Deterministic replay. Record only inputs, play them back, and get the same game. It's also the foundation for rollback netcode.
  • A job system. Spread systems across cores with a work-stealing scheduler, and find out which of your systems actually share data.
  • Real 3D rendering. Lighting, shadow maps, a material system, and frustum culling.
  • Scripting. Embed Lua and expose a few components to it. You'll learn a lot about where the engine/game boundary should be.

07References worth your time

Robert Nystrom, Game Programming Patterns

Free online. Its chapters on the game loop, update method, component and data locality map directly onto milestones 2 and 5.

Jason Gregory, Game Engine Architecture

A large, detailed book on how commercial engines are put together, written by a lead programmer at Naughty Dog. Use it as a reference, not a cover-to-cover read.

Handmade Hero

Casey Muratori builds a complete game from scratch in C on stream, with no libraries. Hundreds of episodes; the early ones on the platform layer and audio are the best.

Glenn Fiedler, Fix Your Timestep!

The short Gaffer On Games article behind milestone 2. Read it before you write your loop.

LearnOpenGL

Joey de Vries' tutorial site. The "Getting started" section covers everything milestone 3 needs, and later sections cover the 3D stretch goals.

Christer Ericson, Real-Time Collision Detection

The standard book on collision: bounding volumes, broad phases and the geometric tests themselves.

EnTT and flecs source

Two open-source ECS libraries: EnTT uses sparse sets, flecs uses archetypes. Read them after milestone 5 to compare against your own.

Chapters that back this project

Next project◎ a web browser engine→