This post describes the switch of our gameplay codebase from a class hierarchy to an architecture based on the Entity-Component-System pattern (ECS). It will first explain why we undertook this refactoring and then present the new ECS framework with which we are currently developing Win That War!. The description is accompanied by some code samples as this post is voluntarily geared towards readers who are curious about the technical details of such an implementation.
The old ways
The Win That War! team is growing and the scope of the game has evolved towards a richer design than originally planned. Concretely, this means that we now need to support new game mechanics as well as new units with some unanticipated characteristics.
Some months ago, at the core of Win That War!‘s code was your typical class hierarchy. A base GameObject
implemented all the shared behavior. Additional sub-classing then progressively added new functionalities to create specialized types (Fig. 1). This was all fine and dandy, until our game designer got loose and imagined fancy units such as the Command Unit. Here is an excerpt from its specifications:
The command unit is both a building and a unit that materializes the player’s presence on the game map. The player’s buildings can only operate within the range of a Command Unit and its destruction triggers the player’s defeat.
In other words, this unit must be able to anchor itself on the ground, like a building, but also to take off in order to establish new bases on any other point of the map. Behold the first flaw of the class tree, the dreaded diamond: due to the tree-like nature of our architecture, we got to a point where it was impossible to share antagonist behaviors such as being both a static building and a mobile unit.

Fig. 1 — Simplified representation of the initial architecture. The new Command Unit does not fit in this hierarchy.
At this point, we had two options. Option 1 was to go deeper into the rabbit hole and create a new subclass of GameObject
(imagine a StaticButSometimesMobile
class alongside Mobile
and Static
). Option 2 was to think ahead and switch to a more flexible architecture. Given the loads of other units and mechanics that are planned, it became obvious that we really needed to look at alternatives. After some consideration, we chose the Entity-Component-System pattern.
Composition over inheritance
The Entity-Component-System pattern is an alternative to the canonical class hierarchy. The three ingredient of the ECS recipe are:
- Entities: the concrete instances of our game objects. For example, units, buildings, decor elements and cameras can all be entities. Conceptually, an entity can be seen as a bag of components.
- Components: the pieces of data that represent specific aspects of the entity that they belong to. For example, position, physics parameters, 3D models and health could all be components. It is the aggregation of components that defines the nature of an entity. We call this set of components the entity’s composition. A strong characteristic of the ECS pattern is that components should only contain data and no logic.
- Systems: the modules that govern the logic of specific parts of the game by manipulating related components (Fig. 2). For example, a Drawing system could examine the Position and Model components of entities in order to render them. A Physics system could examine Velocity components to mutate Position components. Another strong characteristic of the ECS pattern is that all the game logic should reside in systems and that systems are responsible for the interaction between components.

Fig. 2 — Entities (Top) can be seen as “bags of components”. Systems (Bottom) update them. There is a clean separation between data and logic.
Here are the benefits that we expect from ECS:
- Flexibility: it will be trivial to dynamically alter the nature of our entities. Want to temporarily buff a unit? Simply change the data in the appropriate component or swap it with another component altogether. Similarly, systems can be enabled/disabled at runtime, which is extremely useful to un-complexify the game for testing/debugging or to reproduce Minimal Working Examples.
- Data-driven: because ECS is so strict about the separation of data and logic, we should naturally end up with a more data-oriented application. Hence, it will be more straightforward to deserialize our on-disk game data (authored by the game designer) into game entities. Similarly, it will be easier to sync this data over the network without resorting to an intermediary form.
Practicalities
The new architecture had to mesh well with most of the code that was already in place. To this end, we designed the central data store for entities and components (the Entity Manager) to expose them in different forms to the “legacy” code and to the “new” code (which will reside in Systems).
The gist of it is that the legacy code uses stand-alone Entities with a similar API as the old stand-alone GameObjects. However, the new ECS systems leverage a more sophisticated approach in which they process views on entities that match certain compositions (each system has it own criteria).
Fig. 3 gives an overview of this architecture. Each part will be discussed in the following sections.

Fig. 3 — Our ECS architecture. The game data is exposed to the “new” code and to the “legacy” code in different forms.
Components
1 2 3 4 5 6 7 8 9 10 11 |
class Transform : IComponent { public Matrix Tranform; } class RigidBody : IComponent { public ShapeType Shape; public float Mass; public float Velocity; } |
We use an interface to mark a class as being a component. Notice that there is no internal references to the entity that owns the component. This is a voluntary design choice: not exposing the owner forbids indelicate programmers to access unrelated data (eg. myTransform.Owner.AnUnrelatedComponent
). Even more importantly, not exposing the owner prevents to add any logic to the components that could mutate them from the inside, since their context of execution (the “neighbor components”) is unknown. The way in which logic systems do access this context in order to make components interact will be discussed in a further section.
EntityManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class EntityManager { Entity this[int index] => _entities[index]; // Entities, indexed by ID private readonly Dictionary<int, Entity> _entities; // Component "matrix", most of the game data is stored here. // The first key is the type of component. // The second key is the entity's ID. private readonly Dictionary<Type, Dictionary<int, IComponent>> _components; // Lifetime management Entity CreateEntity() {...} bool DestroyEntity(int id) {...} // Composition management TComponent CreateComponent<TComponent>(int entityId) where TComponent : IComponent {...} bool RemoveComponent<TComponent>(int entityId) where TComponent : IComponent {...} TComponent GetComponent<TComponent>(int entityId) where TComponent : IComponent {...} IEnumerable<IComponent> GetComponents(int entityId) {...} // Return "views" on all the entities matching TComposition // (More details on that part later) CompositionNode<TComposition> GetNodes() where TComposition : CompositionBase {...} } |
The entity manager contains the meat of our implementation. It has the following roles:
- Managing the entities lifetime. Users have control over the creation and destruction of entities. On creation, an entity is given a unique ID by the manager. Under the hood, entities are stored in a map in which they are indexed by this ID.
2. Composing entities. Users can create, destroy, and query components. Components are stored in a matrix-like structure where rows correspond to the various types of components, and columns correspond to entities. Querying component X of entity Y is a matter of looking at element (X, Y) in this map. Tab. 1 illustrates the kind of data that can be found inside the matrix if you were to inspect its content (this article provides a nice illustration of this layout).
3. Categorizing entities with respect to their composition. The last job of the manager is to keep track of which entities match a set of user-defined compositions. In this way, logic systems can process only a subset of the existing entities and only consider components that are consistent with their work. This mechanics is detailed in the Logic Systems section.
ENTITIES | ||||
#1 (tank) |
#2 (resource) |
#3 (decor element) |
#4 (factory) |
|
Transform |
|
|
|
|
RigidBody |
|
|
|
|
Model |
|
|
|
|
Weapon |
|
This structure is a good illustration of the flexibility provided by ECS. It really favors experimentation and quick iteration because, once all our components are defined, we can design entities with novel behaviors in a pinch. For instance, it’s trivial to weaponize a factory (give a Weapon component to #4) or to make some decor elements interactive (switch the RigidBody of #2 from static
to dynamic
) so that players can play with them.
Entity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
interface IEntity { int Id { get; } TComponent CreateComponent<TComponent>(Entity entity) where TComponent : IComponent; bool DestroyComponent<TComponent>(Entity entity) where TComponent : IComponent; TComponent GetComponent<TComponent>(Entity entity) where TComponent : IComponent; IEnumerable<IComponent> GetComponents(Entity entity); } |
An IEntity
instance exposes methods for mutating and inspecting a specific game entity. In practice, this interface is mostly used by legacy code that has not been converted to ECS. Because its usage closely resembles that of the previous architecture (a stand-alone GameObject
held all the data of a single unit/building/game thingy), we kept a similar API, which significantly reduced the amount of code rewrite.
Manipulating entities is straightforward:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var tank = world.CreateEntity(); var model = tank.CreateComponent<Model>(); model.Mesh = ...; model.Material = ...; var transform = tank.CreateComponent<Transform>(); transform.Rotation = 2 * Math.PI; var body = tank.CreateComponent<RigidBody>(); body.Mass = 1000f; var weapon = tank.CreateComponent<Weapon>(); weapon.Type = WeaponType.Laser; weapon.BurstDelay = 0.5f; |
The concrete Entity
class that we use acts as a facade for the EntityManager
. It also adds a layer of cache in order to reduce queries for the most used components, plus game-related metadata that we had to keep for legacy reasons.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
class Entity : IEntity { // The most used components are cached to avoid lookups in the manager public Transform Transform {get; private set;} public RigidBody Body {get; private set;} ... // The manager that created this entity private readonly EntityManager _manager; // Not pictured: game-specific metadata ... public CreateComponent<TComponent>() where TComponent : IComponent { // The manager does the actual work var component = _manager.CreateComponent<TComponent>(this.Id); // Cache the new component switch (typeof(TComponent)) { case Transform: Transform = component; break; case RigidBody: Body = component; break; ... } } public DestroyComponent<TComponent>()<span class="crayon-e"> where TComponent : IComponent { _manager.DestroyComponent<TComponent>(); // Un-cache the destroyed component switch (typeof(TComponent)) { case Transform: Transform = null; break; case RigidBody: Body = null; break; ... } } } |
Logic Systems
The last part of the architecture deals with the logic systems that update the entities’ components and run the game simulation. Let’s start by looking at the type of code that we wanted to avoid writing:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class SomeSystem { public void Update() { // This system is only interested in entities // with components X, Y and Z foreach (var entity in World.AllEntities) if (entity.GetComponent<X> != null && entity.GetComponent<Y> != null && entity.GetComponent<Z> != null) FinallyDoSomeWork(entity); } } |
We did not want to iterate blindly through all entities (there could be thousands), filtering them away with some predicate, only to process a fraction of them. Instead, we needed a way for each system to express which types of entities it is interested in ahead of time, so that it would only process those.
This mechanics is made possible by the formulation of compositions. A composition is a user-defined class with a number of fields of type IComponent
, as well as a reference to an entity that possesses said components.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// The base type has a reference to an owner entity abstract class CompositionNodeBase { public readonly Entity Owner; } // Sub-types add fields deriving from IComponent class ModelNode : CompositionNodeBase { public readonly Transform Transform; public readonly Model Model; } // Users can define many other composition types ... |
A composition instance, or node, can be seen as a view on an entity, exposing only components of interest. Their creation is handled by the entity manager who is already responsible for mutating the entities and thus provides the best place to also inspect them when a change in composition happens. Basically, for each existing composition type TComposition
, the manager has an internal collection CompositionNodes<TComposition>
containing one node for each entity that matches TComposition
(see code sample below).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// A collection that contains a node for each entity that // matches TComposition. The entity manager maintains as // many of those collections as there are composition types. class CompositionNodes<TComposition> where TComposition : CompositionBase { // All the matching entities are exposed via those "nodes" // that are accessible through the entity manager public IEnumerable<TComposition> Nodes => _nodes.Values; // Internally, nodes are indexed by the ID of the owner entity private readonly Dictionary<int, TComposition> _nodes; // Those events are subscribed to by logic systems to be notified // of entities that match TComposition/do not match it anymore public event Action<TComposition> OnNodeAdded; public event Action<TComposition> OnNodeRemoved; // This is called by the entity manager when a entity's composition // changes in order to check if it matches TComposition. // If it is the case, a new node with direct references to some of // the entity's components will be created. public void Inspect(IEntity entity) { // In practice, we use bitmasks to quickly check if // the entity matches TComposition var matching = Matches(entity); // The entity now matches TComposition if (matching && !_nodes.ContainsKey(entity.Id)) { // Under the hood: a bit of reflection magic to instantiate // the node and to populate its fields with references to // the entity's components var node = CreateNode(entity); _nodes.Add(entity.Id, node); OnNodeAdded?.Invoke(node); } // The entity was matching TComposition, but not anymore else if (!matching && _nodes.ContainsKey(entity.Id)) { var node = _nodes[entity.Id]; OnNodeRemoved?.Invoke(node); _nodes.Remove(entity.Id); } } } |
The logic systems can access those node collections via the EntityManager.GetNodes<TComposition>()
method that was seen previously. They can also subscribe to two events: OnNodeCreated(node)
and OnNodeDestroyed(node)
. The collection raises OnNodeCreated
when an entity matches the composition and it raises OnNodeRemoved
when an entity does not match it anymore. In this way, systems do not need to consider an entity during its whole lifetime, but only when the entity’s composition is relevant to its work.
Here is an example of a physics system written with this strategy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
// A node of this type will be automatically created for // each entity that has both a Transform and a RigidBody class PhysicsNode : CompositionNodeBase { public readonly Transform Transform; public readonly RigidBody Body; } class PhysicsSystem : SystemBase { private CompositionNodes<PhysicsNode> _physicsNodes; // Internal physics simulation private PhysicsWorld _physicsWorld; public override void Initialize() { // Get a reference to the node collection that we are interested in _physicsNodes = EntityManager.GetNodes<PhysicsNode>(); // Bind events to keep up to date and process // incoming/departing nodes _physicsNodes.OnNodeAdded += OnPhysicsNodeAdded; _PhysicsNodes.OnNodeRemoved += OnPhysicsNodeRemoved; } public override void Dispose() { _physicsNodes.OnNodeAdded -= OnPhysicsNodeAdded; _physicsNodes.OnNodeRemoved -= OnPhysicsNodeRemoved; } private void OnPhysicsNodeAdded(PhysicsNode node) { // Add the object to the physics simulation PhysicsObject physicsObject; switch (node.Body.Type) { case Static: physicsObject = new StaticBody(...); break; case Dynamic: physicsObjects = new DynamicBody(...); break; } _physicsWorld.Add(node.Entity.Id, physicsObject); } private void OnPhysicsNodeRemoved(PhysicsNode node) { // Remove the object from the physics simulation _physicsWorld.Remove(node.Entity.Id); } public override void Update(float dt) { // Physics simulation step _physicsWorld.Update(dt); // Update the entity's position from the results of the simulation foreach (var node in _physicsNodes.Nodes) { var newPos = _physicsWorld.Get(node.Entity.Id).Position; node.Transform.Position = newPos; } } } |
In this example, the system reacts to new entities having both a Transform
and a RigidBody
, and it manipulates nodes exposing only those two components. A benefit of this approach is that a programmer does not need to know the inner workings of our ECS implementation by heart to write new systems. The entity manager does all the bookkeeping by tracking which entities match which compositions, and systems just have to listen to the events that it raises.
Results
After spending several months coding gameplay within this new architecture based on ECS, some conclusions can be drawn.
- Unavoidably, refactoring a sizable codebase such as Win That War!‘s was several week’s work. Switching to this new “philosophy” also required a bit of getting used to from everyone in the team. So, all in all, the move to ECS took a bite in our schedule. However, the promise of ECS is to make up for this lost time with greater speed and flexibility during the rest of the development. It seems that ECS is already paying dividends as we now iterate more quickly on new features.
- We had to make some compromises during the switch to ECS and chose to convert specific parts of the codebase in priority. Some aspects of the game logic still adhere to the old ways of doing things and will probably not be updated due to lack of time. And this is fine, because these parts interface well with the Entity facade that we described. In any case, the code that has been converted — as well as new code — feels cleaner. It’s just simpler to write clear, explicit code, and a lot of complex use cases just seem to sort themselves out naturally.
- Finally, the ECS favors isolation, which is precious for testing/debugging purposes. For instance, we obviously don’t want to pull all of the game rules in our unit tests and some game mechanics that would be constraining in this context (eg. the need to be close a relay for a unit to be active) can be trivially disabled by turning off the systems that handle them.
To conclude, switching Win That War! to the Entity-Component-System pattern required a significative amount of work but we are already reaping some benefits. The gameplay code is more modular since various aspects of the game are now neatly decoupled from each other. Most importantly the game code feels clearer and gives us a better sense of control. For the curious, Fig. 4 shows a screenshot of Win That War! with some of the components that are actually used under the hood.
Building
component when it lands. When the unit takes off, we replace the Building
with a Motion
component and all is well in the ECS world!