Game Dev Diary

What the heck is an Entity Component System anyway? #

What the heck is an Entity Component System anyway? cover

So this is the time for the ECS tangent, where I try to get anyone not super familiar with their concept up to speed on what they are. An Entity Component System is a way of structuring applications, especially games, in such a way where data is separate from functionality. This is as opposed to gluing data and functionality like in Object Oriented Programming where a class owns some data and has a bunch of functionality. An entity is just an identifier (think just an incrementing unique number like 1, 2, 3, 4, ...etc for example) A component is a set of related data, as an example we might have a HealthPoints component that contains both the maximum health and a current percentage of something's health.

The mental model #

Let's elaborate with an example. A good mental model to keep in mind is a 2-dimensional table:

A completely empty 2-dimensional table with 4 rows and 1 column

Each row is identified with a single entity, and each component is a column. So if we have 3 enemies we spawn 3 entities for them, each of those entities would have the HealthPoints component (unless there's a special enemy that has no health!). And the player character can be an entity too! And we can reuse the same HealthPoints component to describe their current health, because why wouldn't we?

We put the labels 1, 2, 3, 4 in the vertical direction into the stub-column. We add a heart header to the only column representing the HealthPoints and we fill up the rest of the column with different hearts representing the different percentages. ⁽¹⁾

So in that tiny table we have 4 rows (1, 2, 3, 4) and one column HealthPoints which is full of data for each row about the health of one of our entities. Let's add another component to the mix to make things more interesting, how about EnemyType? It can be an enum (enumeration) that has the possible variants Robot, Chicken and Poring. We can use it to figure out how to render each enemy on the screen (you don't want your robots rendering as chickens!) and we can use it to figure out other things, like say if our game has contact damage that depends on the enemy then on contact we need to figure out the EnemyType so we can subtract -42 when a robot touches you and -2002 when a Poring touches you.

Add a second column representing EnemyType. First entity has Chicken, second entity's EnemyType cell is empty, third entity has Chicken again while fourth entity has Robot. ⁽¹⁾

Notice something though? Our player doesn't have an EnemyType! This means our table (remember our mental model) has a hole in the cell at the intersection of EnemyType column and the player's entity row, and those holes are THE ONLY THING which distinguishes entities from each other. In this little example we can say "An entity with EnemyType is an enemy and one without is the player"

Okay, okay, what about the last part of ECS? What the heck are systems? Well... they are everything else! So far we just have a boring table, sitting like a wet blanket, doing nothing even if it has interesting data. Systems take this data and operate on it. They can read it, modify it, or remove it or just mess around with it in almost every way you can imagine. A system can create or remove entities, attach components to them or remove components from them, or modify the values of components inside of a system and much more.

Here are some examples demonstrating some of the possible operations a system can do.

  1. A spawn_enemies() system can spawn enemies by spawning entities that have both HealthPoints and EnemyType components.
  2. A receives_damage() system can detect when your bullets (which would be entities too, by the way!) contact the enemies and update their HealthPoints component accordingly. While another system remove_dead() can run after it to detect if HealthPoints is at 0% to remove the entity (along with all its components).
  3. If our game is multiplayer allowing us to have more than one player entity and our chickens had the chickenism ability then once one of them bites one of our players the chickenify() system can associate the EnemyType component with value Chicken to the entity of the player that got bitten!
    You're a chicken now, Samantha! The Chicken enemy with entity 1 bites the player whose entity is 3 and chickenifies them. The chicken component fades in for entity 3. ⁽¹⁾

Systems #

What's rather different about systems in ECS is the way they access data: they typically ask for specific components, or more technically they query for specific components. For example a system can ask for HealthPoints component and it will get access only to the data in that component NOT the full table and NOT all the components for entities that has an associated HealthPoints component. This can be our regenerate_health() system, it only needs to know the current health values so that it can tick them all up by +1 because yes our game is boring everything has same health regeneration.

The HealthPoints column (component) is highlighted with red. ⁽¹⁾

Then there's our draw_enemy() system, it needs to know the type of the enemy (and their position on the screen of course which can be another component but we will focus solely on the HealthPoints and EnemyType for these examples). But there are a couple of difference! Our regenerate_health() component needed to both know the current HealthPoints value and be able to modify it. This is an important and extremely handy distinction. Whether a system can modify a particular component or not makes reasoning about games much easier, because if something is going wrong with the positions of the enemies on the screen, you know only the systems which explicitly ask to be able to modify the positions can be at fault for this. And since it's such a handy distinction we can give it a more succinct name: mutability. A system can query components mutably, i.e regenerate_health() queries HealthPoints mutably. Or immutably, i.e draw_enemy() queries EnemyType immutably.

The EnemyType column (component) is highlighted with green to signify the system only needs read access not write access to its data. ⁽¹⁾

Another difference from the regenerate_health() example is that our column has holes in them! Oh no! What happens now? Does our draw_enemy() system crash because of the hole? Or maybe we have to add hole checks everywhere in our game, oh the terror... Nope! Entity Component Systems are extremely brilliant and will let your system safely iterate over the EnemyType components while jumping any holes there are. Effectively acting as a query that says "I want only the EnemyType components that are full"

The same as above image but hole is no longer highlighted. Representing the data passed to our system, our system implicitly know the component it's dealing with is EnemyType and also receives all the EnemyType components that exist. The hole at EnemyType for entity 2 is not passed to our system and we don't need to do null checks. ⁽¹⁾

Suddenly your friend contacts you saying they tried the latest version of the game you sent them and they found it extremely unrealistic that the robots regenerate as fast as chickens, and that chickens should regenerate WAY faster. You nod heartily in agreement, this won't do.

So you go update regenerate_health() system, the system now needs to be able to modify the health but also needs to know the enemy type, so you ask the ECS for HealthPoints component mutably, and EnemyType immutably. By default such a compound query is typically considered an AND query. What you actually asked for is "the HealthPoints(mutably) and EnemyType components for entities that has both an associated HealthPoints and an associated EnemyType." Or more simply "query the intersection of HealthPoints and EnemyType components." If you're familiar with databases, especially relational ones, this might start sounding very similar to database queries, because it is.

The ECS figuring out which components to return by filtering away components from entities that don't have either an associated HealthPoints or EnemyType, in our case only entity 2 has no associated EnemyType and every component has HealthPoints so our system will iterate over (HealthPoints, EnemyType) for entities 1, 3 and 4. ⁽¹⁾

Now you can check what's the enemy type and if it's a chicken you regenerate +100 health for it but only regenerate +1 for the robot. You test it out and looks like it's working but it seems you introduced a bug by mistake! Your original regenerate_health() component was designed to regenerate health for all entities with health, including the player! Now this new system only works for enemies. There are a bunch of solutions for this, so why don't we explore all of the ones I could think of?

And so this is a taste from the many different types of queries that are at your disposal to use while constructing systems! And with that all of the magical letters of ECS have been explained, you have the main toolkit you need to go venture into a new world of game making. This isn't the full toolkit though! There's a lot of extra fluff we will explore soon. However, before I let you go there's one last important thing that you should keep in your mind whenever you think about Entity Component Systems.

If you think about your computer memory (AKA RAM) as a group of boxes then those boxes will be laid out in a line⁽²⁾. This means it's not possible to represent a 2D table in memory without doing some flattening. Since it's a two dimensional table there are two ways to go about this, either we can put the rows after each other:

[[HealthPoints1, EnemyType1], [HealthPoints2, EnemyType2], [HealthPoints3, EnemyType3], [HealthPoints4, EnemyType4]]

or we can put columns after each other:

[[HealthPoints1, HealthPoints2, HealthPoints3, HealthPoints4], [EnemyType1, EnemyType2, EnemyType3, EnemyType4]]

As you have seen from our examples we want to be able to iterate over all HealthPoints in order, this is what enables our ECS to do all those cool things like queries we described above. This is why in an ECS we use column based flattening because it lets the iterations be faster. Having to jump through memory to find the next HealthPoints is not as fast as having them sequentially in memory next to each other. Column based flattening is more technically known as "column storage" or "columnar store". Another common name for these two ways of laying data is Array of Structures (row based flattening) and Structure of Arrays (column based flattening) feel free to Google that if you're curious about the tradeoffs of each way!

This brings us to the important distinction I have tried to keep very clear while explaining things previously, an entity is just an identifier into those contiguous sets of components. It's the 1, 2, 3 or 4. If we used row based storage then an entity could instead be described as [HealthPoints1, EnemyType1] but we don't!

And this is why the terminology is a bit confusing. You will often hear people say "add a component to an entity," or "insert a component into an entity" which are technically not possible and "an entity that has an EnemyType component" which is technically not accurate. But what they mean is "associate a component with an entity" or "relate a component to an entity" and "an entity that identifies an EnemyType component". You can probably see why the former examples are more used than the latter examples as the latter ones are a bit of a mouthful. And please do feel free to use the former examples for just that reason (even I do that!) while keeping this very important distinction in mind because it will make you think about an ECS with a more accurate mental model.

And now, my friend, venture forth into the new world of game making! ❤️

  1. Evil Villain, Gear, Heart, Chicken, and Robot Twitter emojis by Twitter used and derived from under the terms and conditions of CC BY 4.0 license.↩️↩️↩️↩️↩️↩️
  2. This is a pretty huge simplification; memory layouts in modern computers are much more complex.↩️