entity component system

an architectural pattern
Combine simple and reusable parts to build complex applications.
PNG
SVG
hide notes
The entity component system Design Pattern
Frequency
Complexity

The Entity Component System is a design pattern often used for game development but is not limited to it. Instead, the pattern can bring benefits whenever an architecture needs to be flexible and typical OOP methods like inheritance may be too limiting or would require the implementation of too many classes.

Due to its data-oriented approach, the pattern has a bit of an entry barrier for developers used to OOP paradigms. However, after getting familiar with its main concepts, it allows for creating very flexible and scalable architectures.

But let's first look at why static inheritance can sometimes be problematic when designing complex class hierarchies.

The Problem: Static Inheritance and Combinatorical Explosion

Many OOP-based languages let you define static inheritance trees, where derived classes inherit behavior from their base classes to reuse functionality. The base class provides the implementation, and sub-classes can either use it directly or override the relevant methods if a more specific behavior is desired.

While this works well most of the time, there are cases where a derived class may rely on behavior implemented by several base classes. Since multiple inheritance is prohibited by many of today's popular OOP languages (for good reason), you have to decide for one base class you derive from and replicate the other one's implementation, resulting in duplicate code.

Let's look at a small example:

Assume we want to create a video game where players can interact with their environment. We define the different behaviors of our game objects on a very granular level, e.g., there are objects the player can damage or destroy, other objects can be moved, and there are also more abstract concepts, like objects the player can interact with and start a conversation.

We could implement these behaviors as base classes and let our game objects derive them to reuse their implementation. In that way, a door would be a stationary object the user could destroy, while a stone would be something the player could move but could not damage.

Three classes representing different behaviors and three game objects, each deriving one behavior.
An architecture where basic concepts are implemented as behaviors. Objects that use one of these behaviors can directly inherit it and reuse the implementation.

However, most games are (luckily) not that simple, and their objects will likely support more than a single behavior. Take, for instance, a chair: While it's a moveable furniture, players can likely destroy it with a single hammer strike.

We can even go one step further and define a non-player character, a person walking around who the player can talk to - or even attack if the conversation doesn't go as expected. This game object would require all three of our previously defined behaviors.

If our programming language supports just single inheritance, we can only solve this by deriving a new behavior from one base class while duplicating the implementations of the others..

Additional behavior classes that implement combinations of different behaviors.
In single inheritance systems, every combination of behaviors would require a new base class. This leads to an explosion of base classes for every new behavior we introduce.

But we must do this for every possible behavior combination: Just imagine that we add some more behaviors like Storeable, Consumable or Burnable, this really starts to become a bunch of work...this design gets ugly really soon as the number of classes explodes with any further behavior we want to add to our game!

Here, inheritance may not be the best approach. The Entity-Component-System provides a much better solution based on composition. Let's see how it works.

The Entity Component System

Instead of using inheritance for behavior reuse, as we've seen in the example, the Entity-Component-System (or short, ECS) pattern combines different strategies: It favors composition over inheritance, separation of concerns, and follows a very data-oriented approach.

Let's have a closer look at each of these strategies:

Composition over Inheritance

Instead of static inheritance, the ECS pattern establishes a has a relationship between its objects and their behavior. For this, it distinguishes between two different types of objects, Entities and Components.

Components are the classes representing the behaviors we've seen before. Each component stands for a specific concept relevant to our world. Components are simple data structures containing only attributes that hold a state. They don't include any business logic. We will later see why this is important.

Entities represent all distinguishable and uniquely identifiable elements in our application or game. All elements we previously labeled Game Objects, like chairs, stones or non-player characters are considered entities. While in typical OOP designs, these objects would implement rich behavior and store various state variables, in the ECS pattern, they contain only a single key attribute. But they have another important feature: They can act as containers for components, i.e., entities can hold an arbitrary number of components that define their behavior.

Entity referencing three different component classes.
Entities can hold several objects that define their behavior.

This composition-based approach makes the system very flexible. Instead of having a static class hierarchy, we can combine various components to build new entities, just as we would construct something out of Lego bricks. Our chair entity would now simply hold references to a Position Component and a Health Component, making it both moveable and damageable.

Also, we could even add or remove components at runtime, letting entities adopt or lose behavior while they evolve.

Separation of Concerns

However, so far, our system is relatively dumb: It can store data, but we don't have a place where to put our application or game logic that manipulates these data. That's where the S of the ECS pattern comes into play, the System.

Systems represent specific subsystems of our program that operate on components and alter their state. In the most basic setup, one system is assigned to each component type, e.g., a Movement-System would be responsible for updating the position stored inside a Moveable Component, but scenarios where a single system can manipulate components of different types are also possible.

Designing such a system is up to you. Whether you want to implement it as a complex object or just through a simple function depends on your preferences or the actual use case. Several variations are possible here.

An entity class referencing components and a system class that operates on these components.
Systems operate on components and change their state. While it's typcial to have one system per component, there can also be systems that can handle different types of components if necessary.

By splitting up the behavior logic into two parts, data (Components) and implementation (Systems), the ECS achieves a very strong separation of concerns. This even goes beyond typical OOP or Domain-Driven design practices, where a single object should encapsulate its data and the operations modifying it. In ECS, component objects are only simple structures holding state variables.

Data-Orientation

Thanks to the strict separation between data and logic, the data can be optimally aligned in memory, allowing fast sequential access. This can be essential for performance-critical applications, like games with large 3D worlds consisting of several 1000s of objects. For this, all components of a particular type can be stored in their own array. Loading these arrays in memory during processing can then happen very fast, making this approach very efficient.

Entities would then only store the array indices of the components they are composed of.

Components are stored in different arrays and entities hold the indices of the components they need.
Organizing components in different arrays per type makes accessing and iterating them very efficient. Instead of referencing the components directly, entities can just store their array index.

However, this is mainly an implementation detail. Whether your application organizes its data in such a way is, again, up to you.

Some more things to consider

Extendability

A significant benefit of this pattern is that the architecture can be extended without altering existing parts. Adding new features can often be achieved by adding a new component-system pair, leaving the rest of the application untouched.

This is better than static inheritance, where existing code must often be updated when adding a new class into the inheritance tree.

An entity-component-system where a new component and system are added.
The architecture can easily be extended without changing existing code. New features can be implemented by adding new components and systems.

Flagged Entities

One question is whether entities need an attribute that specifies which kind of object they represent. Ideally, this should not be necessary as the actual type of an object should play a subordinate role compared to its behaviors. In other words, knowing what an object does should be more relevant than what it is. If you still need a way to distinguish entities of specific types, you could add some marker component - an empty data structure whose sole purpose is to flag entities of a particular type. The Unity Engine uses a similar concept they call Archetypes.

Finding the right Granularity

Finding the right granularity for components and systems is not trivial. If your components are too small, you may have a dependant state spread across several objects. If your components are too large, handling and maintaining them becomes more challenging. The basic rule saying one system per component type may only work for simple scenarios, and more complex situations may require systems that can operate on components of various kinds at once. In most cases, you may need several iterations to find the granularity optimal for your use case.

Implementing Logic inside Components

There are two main reasons why the ECS separates behavior logic into systems. First, as we've already seen, it allows for faster memory access when processing several components simultaneously. The other reason is that it enables the decoupling of components from each other, as only the systems may need to access different components.

However, a more simplified variant of this pattern puts the behavior logic into the components, thus eliminating the systems. While this can be a viable solution, it may also complicate some things: Since the system as an orchestrator is not available anymore, components have to handle many tasks on their own, e.g., getting access to other components or sending notifications about state changes.