Started with what seemed like a straightforward assignment: "Build a 2D game engine with component architecture. We'll guide you through it: text adventures, then 2D rendering, physics, Lua scripting. You have 10 weeks."
Most people in EECS 498.007 followed the progression exactly. Built what was asked, got their A+, moved on. Smart approach honestly.
But by week 10, with a functional engine and two weeks for a "custom feature," I started noticing things. Why does adding components feel so clunky? Why does the editor feel like vim from 1987? Why do frame times spike when more than three sprites exist?
These aren't rhetorical questions when you built the thing from scratch. They're bugs staring at you, daring you to fix them.
What emerged: Pulsar, a cross-platform engine with multi-threaded physics, parallel ECS, hot-reloading editor, and enough architectural complexity to make me question my life choices at 3am. Built on C++20, Box2D, SDL2, ImGui, with custom memory allocators because apparently I thought malloc() was too mainstream.
(Demo • Source • Docs • Lua API)
Finished this beast in April 2024, graduated, moved to the real world, and somehow never got around to writing about it. Better late than never—the technical lessons still hold up.
Here's when things got interesting. Turns out game engines are just distributed systems with anger issues:
┌─────────────────────────────────────────────────────────────┐
│ PULSAR ENGINE ARCHITECTURE │
└─────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ EDITOR │ │ GAME LOOP │ │ PHYSICS │
│ THREAD │ │ THREAD │ │ THREAD │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ Hot Reload │ ECS Update │ Step World
│ │ │
┌──────▼───────────────────▼───────────────────▼──────┐
│ RESOURCE MANAGER │
│ (File Watcher + Asset Cache) │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────┐
│ ECS CORE │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ ACTORS │ │ COMPONENTS │ │ SYSTEMS │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────┬───────────────────────────────┘
│
┌─────────────────────▼───────────────────────────────┐
│ LUA BRIDGE │
│ C++ Components ←→ Lua Components │
└─────────────────────────────────────────────────────┘
Multiple subsystems fighting for CPU time, all needing perfect coordination, all expected to hit 16.67ms frame budgets. No pressure.
The first time my physics thread deadlocked with the render thread, I finally understood why senior engineers have that thousand-yard stare when you mention "thread safety."
Entity-Component-System sounds elegant in blog posts. Then you try implementing it and discover edge cases that would make Dijkstra weep.
The nightmare scenario: Component A's OnUpdate()
adds Component B which triggers Component C's removal while iterating. Most engines handle this by crashing. Unity's own documentation basically says "good luck lmao."
My solution involved more defensive programming than a paranoid sysadmin:
if (has_JIT_added_components) {
just_added_component_for_key =
actor->entity_JIT_added_components.count(key) > 0;
}
if (component_enabled && !just_added_component_for_key &&
on_update.isFunction()) {
on_update(component); // pray this doesn't add more components
}
This seemingly simple check prevents the entire system from eating itself when components get creative with lifecycle management. Learned this after spending 3 hours debugging why my particle system kept corrupting memory—turns out each particle was spawning more particles in its death callback. Exponential particle growth is bad for frame times, who knew?
Most student engines require a full restart to change a texture. Mine doesn't, because I have the patience of a caffeinated squirrel.
Built a file watcher that actually works:
std::unordered_map<std::string,
std::pair<std::string, std::filesystem::file_time_type>>
editor_file_cache;
Background thread watches files, notifies on changes, assets reload without breaking references. Change a Lua script? Instant. Swap a texture? Already rendering. Zero compile cycles.
The mutex dance required to make this thread-safe while maintaining performance taught me why lock-free programming papers always start with "Don't."
Game engines need robust messaging between systems that absolutely should not know about each other. My event bus handles the classic "oops I modified the subscriber list while iterating" problem:
static std::vector<std::tuple<std::string, luabridge::LuaRef, luabridge::LuaRef>>
pending_subscriptions;
static std::vector<std::tuple<std::string, luabridge::LuaRef, luabridge::LuaRef>>
pending_unsubscriptions;
Defer mutations until frame boundaries. Simple pattern, prevents mysterious crashes. The number of game engines that get this wrong is... most of them.
C++ for speed, Lua for sanity. Making them play nice is like negotiating a peace treaty between vim and emacs users.
Using LuaBridge for bindings, but the real work was making both languages feel native:
// Both C++ and Lua components look identical to the engine
component["OnStart"](component);
component["OnUpdate"](component);
component["OnDestroy"](component);
Keep hot paths in C++ (physics, rendering), use Lua for game logic. Cache everything aggressively because crossing the language boundary is expensive.
Result: ~12ms frame times with complex scenes, physics, and live Lua execution. Not bad for something held together with mutex prayers and undefined behavior.
Game engines live or die by memory patterns. One poorly-timed allocation during gameplay and your silky 60fps becomes choppy garbage.
Object pooling everything became religion:
// Pre-allocated everything because malloc() is the enemy
std::vector<Actor> scene_actors; // actors pool
std::deque<IMGRenderRequest> img_render_requests; // render queue
std::unordered_map<std::string, Mix_Chunk*> audio_clips; // audio cache
This forced me into data-oriented thinking. Instead of cozy objects with virtual methods, everything became tightly-packed arrays and cache-friendly access patterns.
(If you haven't watched Mike Acton's "Data-Oriented Design" talk, do it. It'll ruin how you think about code in the best way)
Could've stopped at "it renders sprites correctly." Instead built a full development environment because apparently I hate free time:
Multi-panel layouts, live Lua editing with syntax highlighting, component property inspection, asset browser, scene hierarchy. Basically Unity but held together with hope and late-night determination.
The constraint of making it stable enough for non-programmers forced me to fix every edge case. Nothing teaches defensive programming like watching someone drag-and-drop assets in ways you never imagined.
While classmates debugged rendering code, I was deep in the build system, tooling trenches. Platform-specific manifests, automated asset bundling, dependency management that actually works.
Result: One codebase, three platforms (Windows/macOS/Linux), zero configuration. Also a deep understanding of why build engineers have trust issues.
Built instrumentation from day one:
APP_PROFILE_SCOPE("ECS_Update");
// Automatic timing, outputs Chrome trace format
This paid off immediately. Discovered my sprite batching was actually slower than individual draw calls because I was rebuilding the vertex buffer every frame like an idiot.
Performance problems should be obvious, not mysterious. Measure everything, optimize based on data.
Building a game engine teaches you about performance-critical systems in ways that CRUD apps never will:
Hard real-time constraints - 16.67ms isn't a suggestion
Cache-conscious programming - L1 misses hurt more than bad algorithms
Lock-free patterns - Because mutexes are too slow for render loops
API design under pressure - Every interface decision has performance implications
Resource management - When you have 6ms for physics, every allocation counts
These skills transfer everywhere. Database internals, networking stacks, embedded systems—same constraints, same solutions.
Should you build your own engine? Probably not. Godot exists and doesn't require therapy.
But if you want to understand—really understand—how high-performance software works, there's no substitute. You'll learn why some code is fast, why other code pretends to be fast, and why senior engineers twitch when you mention "premature optimization."
Plus you end up with something genuinely yours. And opinions about engine architecture based on scars, not blog posts.
Final stats: 12.3ms average frame time on 2021 MacBook Pro. Complex physics, live scripting, zero compromises. Not bad for 10 weeks of "just following the assignment" until I didn't.
Pulsar is open source on GitHub. Built with C++20, Box2D, SDL2, and enough caffeine to kill a horse. Pull requests welcome, but I can't guarantee the codebase won't give you second thoughts about object lifecycles.