Memory Management - A Mental Model
Ever wondered why languages like C and C++ are labeled ‘dangerous’? Let me show you how game developers control memory manually with a simple mental model. This mental unlock was a game-changer for me.
There are all kinds of bugs possible in these languages. Use-after-free, memory leaks, and data overwriting to name a few.
Instead of asking “When should I free this?”, ask yourself “What other things live and die with this?”
- “Will this data be needed for the entire game?” -> Never Free
- “Will this data be used for just this level?” -> Free (or recycle) on Level Change
- “Will this data be used in the next frame? No?” -> Free Every Frame
Putting your data into buckets likes this is one step, the next is figuring out limits.
Ask yourself “How many, in the maximum case, will I need of this?”.
Let’s say we are working on an online ARPG like Path of Exile 2.
Lifetime: Level
Enemies, Props, NPCs, Terrain, Navigation Mesh/Collision Data, Skill Effects, Particles, Damage Numbers.
The allocation strategies may vary between these types, but their lifetimes are all level-bound. If the player isn’t in the level anymore, these can all be unloaded (or recycled into the next level, but let’s keep it simple).
The code may look something like this:
arena_free(&level_arena);
That’s it. Really.
An Arena is a type of Memory Allocator that takes one block of memory and pushes data into it.
When creating all these instances, the code can be almost what you’d be used to.
Instead of:
T my_instance = new T();
You’d have (something like):
T my_instance = arena_push(&level_arena, T);
Actually, I’d probably use Pools for same-typed data.
A Pool is like an Array of
T
, but we can mark instances as unused when we are done with them and then use any unused instance when asking for a new one. It’s a way of recycling memory - useful for Garbage Collected languages, too.
So, we’d figure out the maximum number of T
that is ever used in any level and then do something like this:
size_t t_max = 100;
Pool t_pool = pool_create(T, t_max, level_arena);
We create a Pool
that lives inside the Arena
.
That means, when we free the Arena
, the Pool
is also freed.
Lifetime: Permanent
The model here is simple. Allocate everything once and never free it.
Examples: Player Data, UI, Some Sound Effects, Some Music, Social Data (friends lists, etc), Web Socket Connection, etc.
You can use malloc
or your own allocator, since we never have to think about the lifetime.
We may have to think about the data access patterns. We do have to think about them if they are in a Hot Loop (accessed every frame, or in some kind of high-speed loop).
Now you can see that memory management should not be about juggling individual object lifetimes. That path leads to bugs, performance issues, and many late nights pulling out your hair. Trust me, I used to have hair, and now I don’t.
It’s all about organising your data by lifetimes. Think about your favourite games. Everything from UI, to Assets, and Particles all fit into different lifetime buckets.
As mentioned above, even Garbage Collected languages benefit from thinking this way. In fact, I was trying to figure out why my JavaScript game was running so slowly and that’s when I discovered Pools. Turns out if you recycle memory, the Garbage Collector doesn’t need to run and things get way faster.
Now you know the mental model, but learning requires doing!
If you want to see this in action first, I’m live-streaming building an engine and game from scratch in C on Tuesdays, Thursdays, and Saturdays (usually at 8AM AEDT, 4PM EST, 9PM GMT). Check it out here.
Join us in the Program Video Games Community. We have talented people sharing their games and engines. I’m sharing all my source code from the streams in there, too.