Nomad Game Engine: Part 2 — ECS

Down with inheritance!

This post is part of a series where I’m documenting my experience building an ECS game engine from scratch. Check out the homepage for this project for more posts, information, and source code.

Entity Component System

As mentioned in my last post, the game engine I’m starting to make is going to follow ECS (Entity Component System) methodology. In this blog post I’m going to do my best to explain my implementation of ECS as simply as possible. There are many great resources that have been created by people much smarter than me explaining ECS, so you might be wondering why I’m even bothering to make a post about this.

  1. If I’m going to make a blog series about this engine, I think it’s better to have a holistic approach than to just link to a bunch of other people’s posts about a topic.
  2. There are actually many different ways to implement an ECS engine that vary quite dramatically. By making this post I’m setting ground work for the posts to come.

Entities and Components

ECS follows the principle of composition over inheritance. The following examples should be able to illustrate this concept, but if you’re curious about it, I’d highly recommend checking out this video, as it does a great job of explaining why composition over inheritance is important.

In Nomad, we have Entities, Components, and Systems (ECS). To explain these concepts, I’m going to use this example:

Three entities

In this example, we have three entities, or game objects — the player, the log, and the orb. Here are the game’s requirements:

  1. The player is controlled by the arrow keys
  2. The player and the orb both have a health value (can take damage)
  3. The player can’t walk through the log (but the orb can float over it)

In an ECS architecture, entities are assigned components based on what attributes they have. We can drill down into the requirements above to find that we have 7 basic components:

Our 7 components

This might seem like a lot of components that make the game unnecessarily complex, but we can see that each of the components is actually a very small piece of functionality, which makes it much easier to conceptualize. With these components, let’s take a look at our entities:

Entities and their components

This is the essence of ECS: an entity is simply a collection of components which provide functionality. When done properly, components can be added and removed to add or remove functionality. For example, if I wanted the orb to collide with the log and player as well, I could simply add a “Collision” component to it. If the player had an invisibility cloak, I could simply remove its “Sprite” component. Intuitive, right?

Changing how the game works is as easy as adding or removing a component!

ECS with a data-driven approach

Okay, so we’ve got a bunch of entities that have components assigned to them. How does this actually work behind the scenes? This is where Nomad gets slightly more complex. Let’s take a look at what Nomad thinks an entity is:

struct Entity {
unsigned int id;
}

Yup, that’s right. It’s just an id. That means no functionality, no implementation. Just data. How about taking a look at one of our components:

struct HealthComponent {
int currentHealth;
int maxHealth;
}

Once again, no functionality, just data. With this new knowledge, I should clarify our components:

Data oriented components

You’ll notice that now no functionality is assumed by any of these components, they’re simply bags of data.

So at this point you probably have two main questions:

  1. How are Entities and Components tied together?
  2. Where is the actual code (functionality)?

Component Managers

The answer to the first question is actually very simple. Component managers manage all components of one type and keep references to which entities own them. Here’s how the data is actually organized in Nomad:

Component Managers hold all the components (the number below each component is the entity associated with it)

Giving components their own managers as opposed to letting entities own components may seem to be an arbitrary decision, but doing so actually gives a serious performance increase. For a moment, let’s dive into the memory layout of both of these options:

Memory layout for our two possibilities

The most important information to know here is that processors love to iterate over arrays of contiguous data. The less we jump around the computer’s memory, the better.

Let’s use an example to show why the right side is much better performance-wise. Our player (and his trusty companion the orb) are fighting a boss who decides to throw a bomb that reduces everyone in the area’s health by 20% for the duration of the fight.

The bomb reduces maximum health by 20%

The pseudocode would look like this:

foreach(entity hit by bomb):
HealthComponent hp = entity.getHealth();
hp.maxHealth = hp.maxHealth * 0.8;
Memory accesses for updating a single component

If our entities held their own components, we would be jumping in memory from the “player” entity’s memory to the “orb” entity’s memory. In this loop, we are sequentially accessing random memory locations, which is not ideal. However, if we hold components contiguously in memory, we’re accessing an array of data in order, which processors love. Obviously, this example is only two components, but for the sake of argument let’s consider that a game might have hundreds of components of a given type. The performance difference between jumping around in memory to update their maximum health and simply running through an array is sizable.

Systems

Alright, so we’ve covered Entities and Components in reasonable depth. How do we actually add functionality? Where does the game code go?

The answer to that is “systems”. Entities and components are just data containers, and systems are the ones who actually modify that data. In Nomad, a system can specify a set of component types that it wishes to pay attention to. Any component that has the necessary components will be updated by the system. This might sound confusing, but it should make more sense after an example.

Movement System

The movement system is one of the most basic and necessary systems. If you take a look at the components we listed up above, you’ll notice that we had both a Transform and a Motion component. Here’s what they look like (note that in the game code they look a bit different but this should serve to illustrate the concept):

struct Transform {
int x;
int y;
}
struct Motion {
Vec2 velocity;
Vec2 acceleration;
}

The movement system is in charge of updating all entities’ positions and velocities every game tick. Therefore, the movement system states that it wants to pay attention to any entities that have both a “Transform” and a “Motion” component. As components are added and removed, the list of entities that the movement system pays attention to will change. Every update, the movement system will run something like this:

void update(int dt){
for(entity in m_entities){
TransformComponent position = entity.getTransform();
MotionComponent motion = entity.getMotion();
        position.x += motion.velocity.x;
position.y += motion.velocity.y;
        motion.velocity.x += motion.acceleration.x;
motion.velocity.y += motion.acceleration.y;
}
}

Once again let’s think of how memory is traversed in this update() function.

Movement System memory accesses

Notice a couple important things about this chart:

  1. We’re not actually accessing every “transform” component. This is because Entity #2 (The log) doesn’t have a motion component, so the Movement system doesn’t pay attention to it.
  2. Even though we’re skipping the 2nd “transform” component, our memory accesses are still using an array of data, which gives us great performance increases. As we add more entities that move, the performance gains continue to increase.

Back to our example

Let’s take a look at the systems we would need to bring our original example to life (once again, remember that a system will only pay attention to an entity that has *all* of the required components):

The systems we would need to create our game.

Based on these systems, we can see that the player entity would be part of Movement, Player Input, Collision, and Render. The log entity would be part of the Collision system and the Render system, and the orb entity would be part of the Movement, Follow, and Render systems. Note for the astute among you that collision system would normally need to take motion into account as well, but we’re leaving it out for this example.

Adding a new feature or functionality is simple, simply add components and systems as needed. Because systems are independent and only deal with a specific subset of components, the game engine has very low coupling, which makes it a lot easier to debug and plan. In addition, the majority of systems don’t actually need to be run in a certain order, so we can have different systems execute on different threads concurrently, significantly boosting our performance.

Here are a couple other implementations that might differ slightly from mine but do a good job of explaining ECS:

Current Progress

Current progress of Nomad.

Most of the boxes you see are for debugging (bounding boxes for collisions, etc.). A couple changes since my last post:

  • Collision detection now uses spatial hashing (the black squares)
  • Sprites are now drawn in z-order (that’s why the player can run both behind and in front of the tree)
  • Added the ability to sword slash
  • Added rotation to the Transform component (both fireball and sword slash use it)

Keep your eye out in the next couple weeks for my next post in the series!