Familiar Castaways

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:

A screenshot of a window named "Model.WindowsGL" with the MonoGame logo. The window is full of overlapping yellow circles, red squares, blue triangles and green crosses.
50,000 entities

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:

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:

The key concepts are:

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:

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:

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:

Then I created three systems:

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! ❤️