Part 5 of 5
Four Engines, Fifteen Years
What the Numbers Show
This series has worked through four custom C++ engines in the order I built them. The point of keeping the source was never nostalgia. It's that a claim about how one's judgment improved is worth very little, and four repositories that can be counted are worth something.
Here is what counting them showed.
The One Table
| Engine | Era | Lines | Files | Average file |
|---|---|---|---|---|
| Gumball | 2010 | 20,421 | 43 | 475 |
| Manic | 2012 | 29,030 | 397 | 73 |
| Critical Mass 2, on Carbon Monoxide | 2014 | 20,617 | 112 | 184 |
| Carbon Monoxide, now | current | 33,591 | 266 | 126 |
Volume moves far less than structure does. All four sit in the same tens of thousands of lines. What changes by a factor of nine is how many pieces that code is divided into.
Fifteen years of improving judgment did not produce less code. It produced a similar amount of code with far more seams in it, and nearly everything else in these posts is a consequence of that.
Every count here is lines of code in .c, .cpp, .h, .hpp, .inl, .m and .mm files, excluding blank lines, comments and third-party code sitting in the source directories. That last exclusion matters more than it sounds: glext.h alone accounts for 9,480 lines in the first engine and 13,146 in Critical Mass 2, and Manic carries the whole SQLite amalgamation. The current row is the engine plus the game code built on it, Dungeoneer's CarbonMonoxideBase. The 2014 row is the iOS game rather than the engine underneath it, so it belongs beside the others rather than on the same trend line.
One more number. The first engine's globals.h declares 603 globals. The current game's headers declare none.
Every Improvement Took a Rule Out of Convention
Reading the four in sequence, the changes look varied. Filename prefixes became static libraries. Libraries became a submodule with a pinned version. Manager singletons became compile-time concepts. Hand-written content classes became code generated from JSON schemas. Hand-written behavior became scripts.
Each one takes a rule that lived in convention and hands it to something that enforces it.
And almost nothing was thrown away. The atlas allocator written for the first engine is still recognizable in the second. The streaming state machine that lived in twenty globals became a class with one method per state. The ideas survived every rewrite. What kept having to be rebuilt was the structure holding them, which is the same observation from the other direction.
engine_graphics.cpp and engine_sound.cpp said "these are separate concerns" and nothing stopped either from touching the other's globals. Making them libraries meant the linker said it. A manager said "there is one of these" and nothing checked. A singleton concept means the compiler checks. A struct and its loader and its editor's model of it said "these agree" and nothing verified it. Generating all three from one schema means they cannot disagree.
Once I saw that, the useful question stopped being "what's the right architecture" and became "which of my current rules are only conventions." That question has an answer on any codebase, including ones with no engine in them.
What Each One Actually Contributed
Gumball proved that the idea and the structure under it are separate skills. It went on Steam with 603 globals, and also with a self-tuning texture streamer, a call-tree profiler that understood exclusive time, a redundant GL state filter, and best-fit atlas packing. Every one of those is something a good engine programmer would think of. The profiler allocated a string per sample and stored pointers into a growing vector. The state filter did a linear scan to avoid a flag set. The instincts were years ahead of the data structures, and knowing that a technique exists turns out to be much easier than knowing what makes it pay.
Manic proved that boundaries need enforcement, and it's where the instincts that survive today arrived. Its particle system precomputes a shared interpolation curve into a table, pools particles in an allocation-free ring buffer, and invalidates its own cache so the editor felt immediate. That combination is why it was the code I sent to EA. It also introduced sixteen manager singletons, which reorganized global state without changing who could reach it. Both are true of the same engine, which keeps happening.
Carbon Monoxide on mobile proved that a constrained platform teaches faster than an unconstrained one lets a person avoid. Explicit states, relative layout, and batched drawing all arrived because iOS made the alternatives impossible. It also put the renderer behind an interface with three backends, one of which draws nothing, and that decision is why the engine outlived the game it was written for.
Carbon Monoxide now proved that the ceiling on a codebase is how much of it has to be written by hand. Moving content into schemas and behavior into scripts didn't make the C++ better. It made there be less of it in the places that change most often.
What I'd Do Differently
Ordered by how much they'd have mattered.
I'd have found the ownership problem years earlier. Globals became managers became services, and all three answer "where does this live" while leaving "who may change it and when" open. The step from the second engine to the fourth is the step I'd compress hardest, and nothing about it required fifteen years of experience. It required someone to ask why a subsystem couldn't be constructed on its own.
I'd have written tests, and I'd have started when the answer was still cheap. None of these four have a test project. The reason is the same in each case: the state a subsystem needed couldn't be stood up in isolation. That's a design symptom, not a testing decision, and it went unexamined precisely because the absence of tests never blocked anything.
And I'd have revisited defaults on a schedule. The second engine is 32-bit only because that was reasonable in 2012 and nobody revisited it. It has a global using namespace std; because that saved typing in week one, and a decade later the build documentation carries a note about a Windows header collision caused by it. Defaults don't decay loudly. They just quietly stop being right.
Whether to Write One at All
For almost everyone the answer is no.
Unreal and Unity and Godot exist, they solve problems these engines never got to, and a person choosing between writing an engine and shipping a game should ship the game. I've spent most of my professional life working in other people's engines, and I'd have been worse at it without having built these, which is a different claim than saying the engines were the efficient path.
What building them actually bought was that nothing in a game engine is opaque to me now. When a commercial engine behaves strangely at the asset pipeline, or the frame loop, or the platform layer, I have a model of what it's probably doing, because I've written a bad version of it. That's worth a great deal and it is not worth fifteen years on its own. It came as a side effect of making games I wanted to make with the tools I had.
So the recommendation I'd give is narrow. Write an engine if the writing is the part being enjoyed, or if a specific need genuinely isn't served. Don't write one to learn architecture, because the lesson is available in any codebase and the engine is a slow way to receive it. And if one gets written, expect the valuable output to be the judgment rather than the engine, because four of mine are in an archive and the judgment is what I use every day.
The Thing I'd Tell the Person Starting
Ship something first, however badly built. The first engine here went on Steam carrying 603 globals, and the fact that it shipped is why there were three more. An architecture that never ships teaches nothing about what a codebase has to survive.
Then, having shipped, ask which of the rules are only conventions. That question is what the next fifteen years turned out to be about.