Devlog 1: 50,000 entities
Happy new year! 🎉
I wrapped up devlog 0 just before Christmas two weeks ago with an empty GitHub repository and a plan.
And now, I’ve got this:

It’s more impressive than it looks (I think)
I’m building the game with MonoGame, which is a framework rather than an engine – so I suppose it’s more accurate to say: I’m building the game with a custom engine that I’m building with MonoGame.
So, before I worry too much about the game itself, I’m working on the foundations of the engine. The two big architectural decisions I’ve made so far are:
- The Entity Component System (ECS) pattern
- Custom content management
Entity Component System
What’s the Entity Component System pattern?
There are uncountably infinite ways of building games and game engines. If you’ve ever started learning game development and felt overwhelmed by the options, I hear ya and I get ya and for what it’s worth, this diversity is a strength rather than a curse. Different approaches suit different games.
Among that infinity of approaches is the Entity Component System pattern. This boils down to:
- Entities, which are unique identifiers for the things in the game. The player character might be ID
108, an enemy might be7and a sewer grate might be19208. There’s no significance to the identifier; it’s just unique. - Components, which are sets of attributes that can be associated with entities. For example, you might have a “position” component that describes an
x, ycoordinate. You could then give entity108a “position” component with value0, 0to put the player character in the centre of the world. - Systems, which are functions that rapidly iterate over components to change the game’s state. For example, a “movement system” might find every entity that has both a “position” and “velocity” component, then update the position in respect of the velocity.
The key concepts are:
- Entities have no inherent meaning. There’s nothing special about the “player” entity or the “sewer grate” entity. They’re just identifiers to associate with components.
- Components are independent, reusable and blind to the entity they’re associated with. If something needs velocity, just give it a “velocity” component. If something needs to hurt the player, just give it a “hurt box” component. They’re just descriptions of behaviour.
- Systems have singular responsibilities and care only about the minimum data they need to care about. If a “movement system” updates positions based on velocity, it doesn’t care–and doesn’t need to care–if the velocity was set by a game pad, scripted narrative scene or AI. It’s just velocity without emotional baggage.
Now, I adore this approach for two reasons. First, I get overwhelmed by large functions that do many things and classes with many responsibilities. Anyone who’s ever worked with me knows I’m savage for refactoring long functions into dozens of two-or-three liners. I just feel–and know from experience–that far fewer things go wrong when a function does exactly what it says on the tin, no more and no less. Building a set of minimal, strongly-typed components with single-responsibility systems sounds lush to me.
And second: if you get it right, the performance is absolutely killer. If your entity and component lists are packed into dense arrays, and if your systems iterate over those arrays in tight loops without pulling in additional stuff, there’s a good chance of that critical data moving into a fast CPU cache. It’s far quicker for a CPU to perform a million operations on two arrays in its cache than a million operations on structures than need to be dragged in from RAM.
Now, look, I’m not saying this game is going to be particularly taxing on modern systems. It’s a low-resolution story-driven platformer, my friends. But I’m not ashamed to admit I dream about a publisher wanting this on Nintendo Switch 2, or having a million folks with older laptops buy it because they know it’ll run well on the kit they already have. It’s important to me to write the best code I can, because I think players will have a better experience because of it.
And also, computer science is fun!
I don’t want to get too technical on this blog, but I annoyed myself and I want to share it
Without going into too much detail, my engine associates entities with components in a “component set” class, in which I have:
- A dense
entitiesarray which contains every entity with this component. “dense” means we ensure there are never any gaps in the array, to minimise the memory footprint and increase our chances of getting as much of the array into the CPU’s fast cache as possible. - A dense
valuesarray which contains the value assigned to each entity. Theentitiesandvaluesarrays are aligned, so the entity atentities[i]has the value described atvalues[i]. - A sparse
indexarray, where the value at indexedescribes the index of entityein theentitiesandvaluesarrays. This array is sparse, meaning it contains gaps. If only entities3and7have a given component then the set will allocate eight slots toindexbut keep six empty, compared to the dense arrays which allocate just the two slots they need. This is slightly wasteful of memory, but lookups absolutely scream; to find entity3’s position, we can just hitvalues[index[3]]– job done.
Anyway! This plan dictates that entities should be integers, which isn’t a great surprise.
But do they need to be int? That would give me entities from -2,147,483,648 to 2,147,483,647 in theory, but if I’m using it as array indices then I can’t use the negative numbers. So, if my entities are int then I’m using 32 bits per entity to support a maximum of 2,147,483,648 entities.
But if I can’t use negative numbers, why not use uint instead? With those same 32 bits, I get a maximum of 4,294,967,296 entities! That’s double what int gives me!
But do I really need billions of entities? My finger-in-the-air estimate for this game is only in the thousands. So, let’s drop that 32-bit uint in favour of a 16-bit ushort! The maximum number of entities drops considerably down to 65,536, but it halves the memory allocation and doubles the components we can fit in that fast CPU cache. Brilliant!
Except… after many hours of dithering and refactoring, my optimised memory leaned on some pretty ugly code.
Whether I like it or not, C# really wants array indices to be int. And array lengths are all int. And I needed overflow guards everywhere. And I suspect the compiler was doing a ton more int casts behind the scenes than I could spot.
So, you know what? I took a deep breath, went out for a long walk then changed my entities to int. It’s not the most memory-efficient, but it results in far less code, far less branching and far more understandable functions. Maybe one day this’ll be a problem I have to solve, but right now? Nah.
So, that’s the core of the ECS done.
Content management
The Content Pipeline’s great, but…
MonoGame has a neato content pipeline ready to use out-of-the-box. You just point it at your assets, like PNGs, and it bakes them into intermediate formats to be loaded into RAM and the GPU when the game starts.
It’s very easy to use and requires very little code, but there are trade-offs. The key weakness for me, and the reason why I rolled my own content manager, is the blocking of the main thread.
See, when you use MonoGame’s content manager to load a texture from the file system, the main thread blocks while the file is read into RAM, and continues to block while it’s uploaded to the GPU.
That’s no great problem if you’re preloading assets behind a progress bar while the game starts up, but I’m planning to build a seamless open world. No transition between rooms, no fade to black between biomes; just a single world that swaps textures in and out of the GPU without dropping any frames.
The solution
And so, I’ve built my own content pipeline to support streaming that won’t interrupt the game. I haven’t battle-tested it yet, but I reckon it’s a good start if nothing else!
It boils down to:
- A contiguous, uncompressed file format.
- Loading from the file system to RAM off the main thread.
- Trickle-uploading from RAM to the GPU over several frames.
The file format is embarrassingly on-the-nose and favours speed of reading over storage size. It’s a binary stream of 8-bit red, green, blue and alpha values for each pixel, top-to-bottom and left-to-right, with a short header for the dimensions. No compression, so no CPU interruptions.
It’s no great trouble to load the file into RAM in a new thread, but the upload to the GPU must happen in the thread that owns the graphics device – and that’s the main thread. So, in the game’s event loop, I check for any queued textures in RAM and upload some small number of bytes to the GPU.
I haven’t particularly tuned that number of bytes, but it’s low enough to not interrupt the frame and high enough that any reasonable texture should load before it’s needed. It could be tuned per-platform even, so small-GPU machines have a smaller frame budget than larger ones. But there’s plenty of time for that level of tuning in a couple of years!
Architecture demo
To test these new ECS and content frameworks, I figured I’d see how many entities I could move around the screen per-frame.
So, I created four components:
- Position, to place textures on the screen.
- Velocity, to describe the distance to move each texture per-frame.
- Texture ID, to map many entity references to a single texture in the content manager.
- Spawn area, to describe an area on the screen where entities can be spawned.
Then I created three systems:
- A movement system, which updates each entity’s position given its velocity.
- A destruction system, which enqueues the destruction of entities that leave the screen.
- A spawning system, which enqueues the creation of new entities to replace the destroyed ones.
The result is 50,000 entities falling at 60 frames per second!
And that ain’t bad for a first pass!
I see the bottlenecks, and I sleep well enough
By far, the greatest bottleneck in this demo is the texture rendering. 50,000 sprite stamps per frame is unreasonably high, and just isn’t ever going to happen in this game.
The second bottleneck is entity deletion and recycling. The dense arrays do a bit of shuffling to manage gaps when entities are removed, and hundreds-to-thousands of those happening per-frame takes a toll. Practically, though, I don’t think this is going to be a problem for a game of the scale I’m planning, so I’ll tackle it only if and when I need to.
What’s next?
Honestly, I’m not ashamed to admit I think this is a pretty fun little start! I’m sure it’ll need more work and optimisation as the game grows, but–for now–the ECS and content frameworks are done.
I think the next thing I’ll look at is the map.
I’ve been using Tiled for years now, but I’m going to give LDtk a shot first. I’ve heard some neat things!
At the very least, the first pass will need a binary format for streaming chunks in via the custom content manager, and an atlas to tile sets in textures. I’ll probably save collision boundaries until I’m confident I can stream and swap chunks of the world. Maybe I’ll need a camera to move around to test it?
Anyway: I hope you all have a great 2026! ❤️