edit

Liam Laverty

Software Engineer & Technical Manager | Edinburgh, UK

Trying to build a little game with Entity/Component/System (ECS) Architecture

On a few occasions, I’ve attempted to make small videogames. I’ve found that progrmaming a game is a great way for me to learn a new paradigm, architecture, or language. In games, code is always running against the clock, since the next frame is usually a maximum of 16.6ms away. This forces me into understanding the data structures I’m using, and the time-complexity of the code I’m writing.

In my 9-5, I’ve mostly worked in Object Oriented Programming (OOP) langauges & frameworks. Because of this, I’m comfortable in OOP, and whenever I’ve tried to build a game, I’ve slipped into those farmilar patterns. OOP in gamedev brings some unique challenges in the way inheritence trees work (discussed later), which descends quickly into bug-filled madness. While I’m pretty sure it’s possible for someone to build very high quality & performant games in an OOP paradigm using lots of composable classes, I’m certain I’m not that person.

An alternative architecture is Entity Component System (ECS). It’s an approach to building composable & performant software, most commonly used in the games industry & in simulation code. In 2023, I attempted to use something close to ECS in my dissertation’s economy simulator, though I didn’t know it at the time.

If ECS is done well, it creates a tidy, composable state management experience for the developer. It has the added benefit of being able to take advantage of low-level SIMD optomisations in the CPU, which are easier to miss in other architectures. It’s a big departure from the comfort of OOP though. I’ve been implementing it in a small game over the last few weeks, and I’ve found it fairly easy to make trivial mistakes with big consequences.

This article isn’t a guide to implementing ECS. It’s me publishing where I went wrong while implementing.

Problems with OOP in game development

When writing games in an OOP approach, I consistently fall into an inheritence hierarchy problem, where I want to manage the capabilities an object has in a common way through its ancestors. The aim is that, everything that’s impacted by collision detection is modified by the same code, reducing code reuse. For exmaple, I’ll have:

Base
--> Renderable : Base
    --> Movable : Renderable
        --> Collidable : Movable
            --> Player : Collidable
            --> Bridge : Collidable
            --> Baddy : Collidable

That seems fine at first glance, especially if I always want all the game objects to have the Render, Move and Collide capabilities… But then what happens if I’m making an Indiana Jones inspired game, and I want my Bridge to be Collidable but not Renderable? I could re-arrange the hierarchy to acheive this fairly easily:

Base
    --> Collidable : Base
        --> Renderable : Collidable
            --> Movable : Renderable
                --> Player : Movable
                --> Baddy : Movable
        --> Bridge : Collidable

Fine for this example game with three objects. But it’s not scalable very far beyond that. To add the “invisible bridge” feature, I’ve adjusted the inheritence in 5 different places, changing the implementation of every object in the game. A maintenance nightmare in the making.

Another issue is that sometimes I want to change the capabilities an object has at runtime, rather than baking them in at compile time. For example, once the player has started crossing the Bridge, I want it to be Renderable.

To work around this probelm, I’d often end up with toggle properties like Renderable->IsRenderable on the ancestor classes. Then descendant objects would manage their own Bridge.IsRenderable state - if it’s set to false, the bridge doesn’t appear on screen. Any feature which wanted to interact with any Renderable objects would need to remember to check if (bridge.IsRenderable) before applying any state changes, or drawing to screen.

That’s a lot of state management, and a lot of if checks. Even in very small games, this quickly descends into an unsustainable mess of conditional logic, and state management bugs. It also means I’m constantly writing foundational code, which is tightly coupled to the game I’m building. If I go on to make a mini tennis simulator, I probably won’t need the object can collide, but not draw code. This undermines the one of the main benefits of using OOP in the first place, as the core code isn’t reusable without a huge refactor.

I’ve tried out a few different approaches over the years, including a 2019 attempt at building an OOP/Service Oriented Architecture (SOA) engine in Typescript: March. When I was strict about my implementation, my objects were composable & managed their own data, then services acted on those data to change the game state. I learned a lot about Typescript, SOA, & HTML Canvas, but the closest thing to a game I made was this WASD spaceship thing.

How about ECS instead?

This year I’ve been building a little game on top of the MonoGame frameowrk, and I’ve been messing around with different ECS implementations.

ECS isn’t a prescriptively defined architecture. As far as I can tell, everyone who’s written about it has a different implementation, but there are some common themes. It has Entities, representing “things” in the game. Each Entity can have many Components, which hold of the data about an Entity. Then there are Systems, which change the state of Components.

Beyond that, implementation details are mostly an exercise for the reader. The description below is how I’ve implemented ECS, but it’s probably not how you should implement ECS.

Entity

My Entities are very abstract. They’re a struct which holds an int ID, and a string which is its debug name.

public struct EcsEntity 
{
    public int Id;
    public string DebugName { get; }

    public EcsEntity(int id, string debugName)
    {
        Id = id;
        DebugName = debugName;
    }
}

Ideally I’d get rid of the debug name, and just have the int (maybe not even the wrapping struct). Entities are just a primary key, which links a group of Components together to create a fleshed out “thing”.

Component

My Components are less abstract, they hold some data. Importantly, they contain no behaviour or logic, they’re just data. The following is a lifespan component, which is storing data about the amount of time an Entity will persist in the game world.

public struct LifespanComponent : IEcsComponent
{
    public int LifetimeMs = 2000;
    public int AgeMs = 0;

    public LifespanComponent(int lifetimeMs)
    {
        LifetimeMs = lifetimeMs;
        AgeMs = 0;
    }
}

In my current implementation, Components all inherit from an empty public interface IEcsComponent { }. For reasons I’ll get into later, I regret this decision, but appear to be stuck with it.

System

The Systems contain logic and behaviour code. They have an Update method, which is called every tick of the game. All relevant Components are gathered; logic is run to adjust the Component data state; and the Component is saved back to the data store. The IEcsSystem interface just contains the empty Update method.

public class LifespanSystem : IEcsSystem
{
    private EcsCoordinatorService _ecs;

    public LifespanSystem(EcsCoordinatorService ecsCoordinatorService)
    {
        _ecs = ecsCoordinatorService;
    }

    public void Update(float deltaTime, GameTime gameTime)
    {
        var entitiesWithLifespan =
            _ecs.GetEntitiesWith<LifespanComponent>();

        foreach (var (entityId, lifespanComp) in entitiesWithLifespan)
        {
            var nextLifespanComp = lifespanComp;
            nextLifespanComp.AgeMs += (int)deltaTime;

            if (nextLifespanComp.AgeMs >= nextLifespanComp.LifetimeMs)
                _ecs.QueueEntityForDestruction(entityId);
            else
                _ecs.UpdateComponent<LifespanComponent>(entityId, nextLifespanComp);
        }
    }
}

This example System is straightforward - it just counts down to zero, and then queues the Entity for destruction. _ecs.GetEntitiesWith<LifespanComponent>(); queries the data store to find any entity which has a LifespanComponent registered, and returns the Entity, Component in a tuple.

ECS Coordinator & Managers

I’ve got three service classes EntityManager, ComponentManager and SystemManager, which are each responsible for CRUD operations on their respective domain. I didn’t want to expose these three Manager classes to the rest of the application, so they’re wrapped in a CRUD type API (the EcsCoordinator class). This is a kind of orchestrator for all things ECS.

Entity Destruction

I decided to do this because I’d prefer the implementer (me in the future, probably) to not have to remember all of the steps involved in deleting an Entity. At the moment, when an Entity’s Component gets into a state where the Entity needs to be deleted, the app calls the _ecs.QueueEntityForDestruction(entityId); method. That method adds the Entity.Id to a queue for deltetion.

public void QueueEntityForDestruction(int entityId)
{
    _entitiesToRemove.Add(entityId);
}

At the end of each tick, the method DestroyEntitiesInQueue is called. That method loops through each of the three domain Managers, and tells it that “the entity with this Id no longer exists”.

public void DestroyEntitiesInQueue()
{
    foreach (var entityId in _entitiesToRemove)
    {
        _componentManager.EntityDestroyed(entityId);
        _systemManager.EntityDestroyed(entityId);
        _entityManager.DestroyEntity(entityId);
    }
    _entitiesToRemove.Clear();
}

Then each of those managers deals with that destruction in whatever way is relevant to it. For example, the ComponentManager looks for any associated Components and removes them.

    internal void EntityDestroyed(int entityId, string reason = "")
    {
        foreach (var componentDict in _components.Values)
        {
            if (componentDict.ContainsKey(entityId))
            {
                componentDict.Remove(entityId);
            }
        }
    }

Before I implemented this queue approach, I was hitting a bunch of bugs where entities would expect each other to be in the world during processing, even after they’d been deleted. For example, I had a feature where the player (EntityA) was followed a swarm of baddies (EntitiesB-Z). In thier update loop, the baddy entities would look at EntityA’s position, and update their direction to point at that. However, if the player’s entity & components were deleted early in the update tick (if they died), the baddy entities would still check for a position. I’d usually expect them to hit an an OutOfRangeException and the game would crash, but instead, they’d just look at the new 0th element in the PositionComponent collection - meaning they’d change their direction towards some arbitrary Baddy’s location (or maybe a scenery entity like a bridge) and swarm towards that instead.

My implementation here isn’t great. The CPU is going to be switching between each manager three times per loop. A better technique might be to send _componentManager the entire list of _entitiesToRemove, have it process them all at once, and then pass the list into systemManager, and finally onto _entityManager.

It’s also not a great approach because, as you can see in the ComponentManager.EntityDestroyed() method, it’s very busy. It looks through all the component types, and then inspects their dictionaries for keys, and then removes them if found. Better data structures would avoid this problem. It’s fine for now, but if lots of entities are destroyed at once, the end of the update tick is slow.

Entity, Component, & Service Registration

Working with the ECS inside of MonoGame’s GameMain class is farily pleasant. Before using my ECS, I’d frequently have multiple thousand lines of code in this file - and they were messily interdependent.

public GameMain() : base()
{
    _ecs = new EcsCoordinatorService();
}

protected override void Initialize()
{
    Vector2 playerStartPosition = new Vector2(1400, 1300);  

    RegisterComponentTypes();
    RegisterSystemTypes();
    RegisterPlayer(playerStartPosition, playerStartTile);
    RegisterBaddies(200);

    base.Initialize()
}
protected override void Update(GameTime gameTime)
{
    _inputHandle.Update(gameTime);

    _ecs.UpdateSystems(gameTime);
    _ecs.DestroyEntitiesInQueue();
}

protected override void Draw(GameTime gameTime)
{
    var entityQueryResult = _ecs.GetEntitiesWith<DrawUntexturedComponent, TransformComponent>();
    foreach (var (entity, drawable, transformable) in entityQueryResult){
        // check it's on screen, and if so, draw the entity
    }

}

With new EcsCoordinatorService, I instantiate the coordinator, the three domain managers, and the necessary data stores for them. Then in MonoGame’s Initialize method, I register Component and System types. For Component types, the order of registration isn’t important. They just get added into a data storage structure, and they’re queried differently by different systems. It’s setup so that Component registration is entirely optional. If the Coordinator is ever asked to add a Component type T which hasn’t been registered yet, it’ll just call self.RegisterComponent<T>() on your behalf. That could cause a performance hit in the middle of the game loop though, as it might cause the underlying data structures to be reorganised, so I prefer pre-registration.

void RegisterComponentTypes()
{
    _ecs.RegisterComponent<TransformComponent>();
    // ...etc
    _ecs.RegisterComponent<LifespanComponent>();
}

For systems, the order of registration is important. The order in which Systems are run depends on when they were registered. Earlier I described a feature where the baddies look at the player. In that implementation, I need the direction to be changed by EnemyLookAtPlayerSystem before it’s position is changed by MovementSystem.

void RegisterSystemTypes()
{
    _ecs.RegisterSystem(new LifespanSystem(_ecs));
    _ecs.RegisterSystem(new EnemyLookAtPlayerSystem(_ecs));
    // ...etc
    _ecs.RegisterSystem(new MovementSystem(_ecs));
}

For the _ecs.RegisterSystem(IEcsSystem system) method, I just have the Coordinator maintaining a List<IEcsSystem>.

Annoyingly I need to pass the _ecs into each of the Systems. _ecs is a singleton-ish object in this app, and it has global scope, so maybe I should just make it a global and be done with it. I can sense some audible gasps coming from Clean Code people at that suggestion.

Ideally I’d also have something in the EcsCoordinator that allowed the implementer to adjust the order on Components and Systems after registration, but I’ve not come across the use-case for it yet, so it’s been a low priority for me so far.

While these registrations look neat and tidy in the GameMain file, registering an entity, and attaching all of its relevant components is a bit of a disaster. Here’s the full implementation for registering one player in a tile-based fantasy game I was toying around with:

private void RegisterPlayer(Vector2 startPosition, Vector2 startTile)
{
    var player = _ecs.CreateEntity("main_player");
    _ecs.AddComponentToEntity(player, new MainPlayerComponent());
    _ecs.AddComponentToEntity(player, new MovementComponent());
    _ecs.AddComponentToEntity(player, new TilewiseMovementComponent { TargetTile = startTile, CurrentTile = startTile });
    _ecs.AddComponentToEntity(player, new FantasyGameInputIntentComponent());
    _ecs.AddComponentToEntity(player, new TransformComponent
    {
        Position = startPosition,
        Direction = Vector2.UnitX,
        Scale = new Vector2(1, 1),
        UnscaledHeight = chunkSize,
        UnscaledWidth = chunkSize,
    });
    _ecs.AddComponentToEntity(player, new CollideComponent
    {
        CollisionBox = new Rectangle((int)startPosition.X, (int)startPosition.Y, chunkSize, chunkSize),
        IsColliding = false,
        CollidableType = CollideType.Player
    });
    _ecs.AddComponentToEntity(player, new DrawUntexturedComponent
    {
        Color = Color.CornflowerBlue,
        Texture = TextureHelper.CreateTexture(GraphicsDevice, chunkSize, chunkSize, Color.White),
        LayerDepth = 0.5f
    });
}

It feels like a lot of cruft & repeated similar code, which was something I wanted to avoid when building with ECS in mind. At the moment, I don’t write much of this code to register entirely new entities, so I’ve not felt the need to adjust the way it works yet. It’d be nice to chain it all together in a fluent API, like:

private void RegisterPlayer(Vector2 startPosition, Vector2 startTile)
{
    var player = _ecs.CreateEntity("main_player")
                     .With<MainPlayerComponent>()
                     .With<MovementComponent>()
                     .With<TilewiseMovementComponent>(...props)
                     .With<FantasyGameInputIntentComponent>()
                     .With<TransformComponent>(...transformProps)
                     .With<CollideComponent>(...collideProps)
                     .With<DrawUntexturedComponent>(...drawProps);
}

Updating data

The ECS’s Update logic is nice. Inside of GameMain, I can call _ecs.UpdateSystems(gameTime);, then behind the scenes the Coordinator finds each registered Service, and calls its Update(GameTime gameTime) method. So the Ecs method looks like this:

internal void UpdateSystems(GameTime gameTime)
{
    var deltaTime = gameTime.ElapsedGameTime.Milliseconds;
    foreach (var system in _systems)
    {
        system.Update(deltaTime, gameTime);
    }
}

and that code just calls the System<T>.Update() code:

public void Update(float deltaTime, GameTime gameTime)
{
    var entitiesWithLifespan =
        _ecs.GetEntitiesWith<LifespanComponent>();

    foreach (var (entityId, lifespanComp) in entitiesWithLifespan)
    {
        var nextLifespanComp = lifespanComp;
        nextLifespanComp.AgeMs += (int)deltaTime;

        if (nextLifespanComp.AgeMs >= nextLifespanComp.LifetimeMs)
        {
            _ecs.QueueEntityForDestruction(entityId, reason: "Lifespan expired");
        }
        else
        {
            _ecs.UpdateComponent(entityId, nextLifespanComp);
        }
    }
}

Though I like how this appears in GameMain, my code in the actual implementaiton has a few obvious problems. I’m doing this var nextLifespanComp = lifespanComp, because I’m iterating over a collection, and can’t (figure out how to) modify a member of a loop’s iteration variable. This newing on every iteration of the loop creates unnecessary pressure on the GC, so in some of my Systems, I’m declaring a private member at the top of the class (like private LifespanComponent nextLifespanComp), and overwriting that at the start of each loop iteration. Bit of a maintenance hassle for the developer, but at least the GC isn’t cleaning up 3,000 new LifespanComponents 60x per second.

Component Query

The sytems I’ve shown so far have dealt with a single Component at a time, but my GetEntitiesWith method can accept up to 6 different Types as an argument. Here’s an example of a system gathering a lot of the Player entity data in a tiled RPG game.

var playerQuery = _ecs.GetEntitiesWith<MainPlayerComponent, TilewiseMovementComponent, MovementComponent, TransformComponent, FantasyGameInputIntentComponent, DrawUntexturedComponent>();

This is acheived through my ComponentQuery feature. The following code is the two-type getter, but it can be expanded out to n types - it’s just a hassle to do so.

    public IEnumerable<(int, T1, T2)> GetEntitiesWith<T1, T2>()
        where T1 : struct, IEcsComponent
        where T2 : struct, IEcsComponent
    {
        var result = GetEntitiesWithComponent<T1>()
                                    .With<T2>()
                                    .Execute();
        foreach (var tuple in result)
        {
            yield return (tuple.Item1,
                            (T1)tuple.Item2[0],
                            (T2)tuple.Item2[1]);
        }
    }

At the moment, I’m supporting up to 6 types. For the size of games I’m making, if I need more than 6 components, I’m probably doing too much in the calling System, and should look to reduce its scope a little.

The GetEntititiesWith<T1>().With<T1>()...With<Tn>() sits at the end of this ComponentQuery system. I’ve left the comments in below, because it’s abstruse enough that it needs them.

public List<(int, IEcsComponent[])> Execute()
{
    var result = new List<(int, IEcsComponent[])>();

    // Find the component type with the fewest entities to minimize iterations.
    var smallestComponentType = _componentManager.GetSmallestComponentType(_requiredComponentTypes);

    // Iterate over the entities that have the least common component.
    foreach (var entityId in _componentManager._components[smallestComponentType].Keys)
    {
        bool hasAllComponents = true;
        var componentsForEntity = new IEcsComponent[_requiredComponentTypes.Count];
        int componentIndex = 0;

        // Check if this entity has all other required components.
        foreach (var componentType in _requiredComponentTypes)
        {
            // Check if the entity has the required component.
            if (_componentManager._components.TryGetValue(componentType, out var entityComponents) &&
                entityComponents.TryGetValue(entityId, out var component))
            {
                // Store the component in the array at the correct index.
                componentsForEntity[componentIndex++] = component;
            }
            else
            {
                // Entity is missing a required component, skip to the next entity.
                hasAllComponents = false;
                break;
            }
        }

        // If the entity has all required components, add it to the result list.
        if (hasAllComponents)
        {
            // During testing, you might want to return the _getEntityById result.
            // in production, that's a big performance hit, so it's commented out.
            // Uncomment the following line if you need the full entity details (inc 
            // the accurate debug_name).
            // var entity = _entityManager.GetEntityById(entityId);
            // result.Add((entity, componentsForEntity));

            result.Add((entityId, componentsForEntity));
        }
    }
    return result;
}

This is another method that I dislike the implementation of, but haven’t had time to return to yet. It searches for all Entities which have all of the Components described in the _requiredComponentTypes list (the types I put into .With<T1>().With<T2>().With<TNext...>()).

First I sort the ComponentType collections by their Count, lowest to highest. The idea here is that, if I’m looking for PlayerInputComponent and TransformComponent, there’s only going ot be one Entity in the game with PlayerInputComponent (the entity representing the main character). That means I can reduce the rest of the method’s search-space down from all of the entities to exactly one entity.

I think at that point, I should probably just recursively call GetSmalestComponentType, popping the current lowest type off the _requiredComponentTypes until that list is depleted, at which point I should have the correct result (I think), or a much reduced search-space. But instead of doing that, I start looping through all remaining Entities which exist in the smallestComponentType collection of Entities.

Dictionary Storage

One of the main objectives of all of the ECS implementations I’ve seen in the wild is that they allow for memory packing. In effect that means that the games Component data for the game are all stored in contiguous memory, with no gaps.

Take my TransformComponent for example, it’s a simple struct with a Vector2 Position, Vector2 Scale, and Vector2 Direction. Vector2 in MonoGame are 8 bytes each, so the entire struct is 24bytes

 public struct TransformComponent : IEcsComponent
{
    public required Vector2 Position;
    public required Vector2 Scale;
    public required Vector2 Direction;
}

unsafe
{
    Console.WriteLine($"Transform size: {sizeof(TransformComponent)}");
}

The output in the console is “Transform size: 24”, as expected. Packing them into contiguous memory means the location of the next element in the array is 24bytes after the location of the current one (and the nth element in the array is at memory location n*24).

My implementation doesn’t follow this pattern. I’d intended to at the start of the project, but configuring all of the arrays was going to take a while, and I wanted to make a little game as a first priority. So I added this instead:

internal Dictionary<Type, Dictionary<int, IEcsComponent>> _components;

Where the first Key is the typeof(IEcsComponent), and then the internal dictionary’s key is the EntityId, and then that dictionary’s values are the Component data. This means that my Component data are scattered all over the place in memory.

The Dictionaries are storing references to Components, rather than the Components themselves. So when I execute some code like

foreach (var transform in transformComponents){
    //adjust transform
}

My processor is doing a tonne of work in memory lookups. If I’d taken the time to lay out all of the transforms in contiguous array, the processor would need to do much less of that type of work. I’d love to implment this and benchmark it one day, as I suspect my processor is spending a very large portion of execution time just managing the memory.


There’s more I can discuss here about memory boxing and Single Instruction Multiple Data (SIMD) optomisations, but I think this post is long enough already, and has left me with a good amount of // TODO type stuff already.