SimpleECS

Drawn by Stephanie

Background

During my first year at The Game Assembly, some senior students held a presentation that briefly introduced the Entity Component System (ECS) architecture. At that point I had only just begun my journey as a programmer, and I knew very little about how game engines actually worked.

Growing up, I had always been on the poorer side when it came to hardware. Most of the games I played ran with low frame rates, frequent freezes, or occasional crashes. Competitive games were especially frustrating under those conditions. Because of that, I developed a bit of an obsession with performance. I spent a lot of time trying to squeeze more frames out of my computer—lowering graphics settings, experimenting with CPU overclocking, and tweaking anything that might improve performance even slightly.

When I first started learning programming and game development, most discussions revolved around game objects and inheritance. I often heard how convenient those systems were, but also how complex the hierarchies could become and how performance might suffer in certain situations.

Then I heard someone mention that ECS could be faster. That single idea immediately sparked my curiosity. The more I learned about ECS, the more interested I became in understanding how it actually worked. Instead of simply using an existing library, I wanted to explore the architecture myself and see how it behaved in practice.

There are many excellent ECS libraries available today, such as EnTT and Flecs, and they solve most practical problems extremely well. However, my goal was not just to use ECS, but to understand the core ideas behind it. Building my own implementation seemed like the best way to learn how the system truly works under the hood.

Goals

When starting this mini project, I defined a few goals to guide the implementation:

  • Understand ECS architecture at a fundamental level rather than relying on an existing framework.
  • Separate data from behavior by keeping components as pure data and systems responsible for logic.
  • Improve memory locality by storing components in contiguous memory to make iteration more cache-friendly.
  • Keep the implementation simple so the system remains easy to reason about and suitable for learning.
  •  Simply to have fun and enjoy the process of learning.💀

Challenges

How to create and manage objects without leaving gaps?

During the learning process, memory management quickly became one of the biggest challenges. Before even thinking about systems or gameplay logic, I had to decide how components should exist in memory: how they are allocated, how they are aligned, and how different component types could be stored in a generic way.

One of the goals of the project was to store components in contiguous memory to improve iteration performance. That meant thinking carefully about object size, alignment requirements, and how to store different component types without knowing their concrete type ahead of time. To solve this, I experimented with type erasure so that abstract component storage could handle many different component types while still managing their memory correctly.

Creating objects initially felt straightforward. Allocating memory and inserting components into arrays was manageable. The real complexity appeared when components needed to be removed.

Because components are stored in dense arrays for performance reasons, removing a component from the middle of the array creates a gap. Leaving gaps would defeat the purpose of contiguous storage, so the common solution is to swap the removed element with the last element in the array and then shrink the container.

In theory this sounds simple. In practice it means updating several pieces of bookkeeping at the same time: the component index mapping, the entity-to-component mapping, and any internal lookup tables. If any of those mappings become inconsistent, the entire ECS state can silently break.

Another challenge appeared when exposing component access. Returning direct references to components seemed natural at first, but this quickly revealed another problem: reallocation. If the underlying storage grows and the container reallocates its memory, any previously returned references immediately become invalid. That means a system could accidentally hold a reference to memory that no longer belongs to that component.

Handling these edge cases forced me to think much more carefully about ownership, access patterns, and how systems interact with component storage.

How does the engine even knows a component type exist?

Another challenge I encountered was deciding how component types should be registered within the ECS.

One simple approach would be to maintain a central header file where every component type is included and registered. While this works, it can quickly become difficult to maintain as the number of components grows. Over time the file becomes a long list of includes and registrations, which felt both fragile and aesthetically unpleasant.

Instead, I chose to register components closer to where they are defined. To achieve this, I used type erasure together with a small macro that behaves similarly to a lightweight reflection mechanism. Since C++ 20 does not provide native runtime reflection, this approach allows component types to register themselves at declaration and expose the information the ECS needs to manage them. I also wanted the system to enforce some constraints. Not every class or struct should automatically be treated as a component. To keep the design explicit and easier to reason about, only types that are intentionally declared as components are allowed to be registered and used within the ECS.

For example, a component such as EmilTest can register itself inside its own header file. This removes the need for a central registry file and keeps the component definition and its registration logic in the same place. This approach also allowed me to be more flexible during development. When creating a new component, I could simply declare it and register it immediately without needing to modify a separate file elsewhere in the project. While this solution is fairly lightweight, it helped keep the ECS implementation modular and avoided the growing maintenance cost of a centralized registration system.

GitHub

Feel free to take a peek at the code here https://github.com/NatdanaiPuthom/SimpleECS