Denys Kryvytskyi Software Engineer

Entity-Component system

"The way you organize data directly impacts performance"
- Robert Nystrom

The article turned out to be quite large, so here is the content:

Intro

Game entities (game objects) are the basic building blocks of a game. There are a lot of different approaches and techniques to build a system behind game entities. The game development community has increasingly adopted the Entity-Component System (ECS) architecture for building modern game engines and games.

But before we dive into ECS architecture I would like to mention other approaches as well.

What could we do?

Jason Gregory (lead game engine developer from Naughty Dog) has an excellent introduction to an entity component system in his book “Game Engine Architecture”. He uses the term “runtime object model” to describe a game entity. I like how he highlights two basic architectural styles:

  • Object-centric
  • Property-centric

Let’s take a closer look at them.

Object-centric

In this architecture, each logical game object is implemented as an instance of a class or interconnected class instances. Many different designs are possible using this approach. I will show you two of them just to get the main idea.

"Is-A" design. Unreal Engine's Actor

The first one uses well-known “is-a” type of a class hierarchy that is based on inheritance and polymorphism. In this type of hierarchy, a derived class is a more specific version of its base class and can be used anywhere the base class is expected. It’s the most intuitive and straightforward way to represent a collection of interrelated game object types. Using this design our game class hierarchy may look like this:

Is-A class hierarchy example diagram

A game object class hierarchy usually begins small and simple, and in that form, it can be a powerful and intuitive way to describe a collection of game object types. However, as class hierarchies grow, they have a complex structure. Let’s see just a little part of the AActor base class hierarchy used in Unreal Engine (actually it has more than 300 inherited classes):

Unreal Engine's AActor class hierarchy example diagram

Problems with "is-a" design

This so-called monolithic class hierarchy tends to cause problems for the game development team for a wide range of reasons:

  • The deeper a class lies within a class hierarchy, the harder it is to understand, maintain and modify. This is because to understand a class, you need to understand all of its parent classes as well.
  • One of the biggest problems with any hierarchy is that it can only classify objects along a single “axis” — according to one particular set of criteria — at each tree level. Once the criteria have been chosen for a particular hierarchy, it becomes difficult or impossible to classify along an entirely different set of “axes”.
  • Multiple inheritance can be the key and solution to resolving logic problems in such a tree hierarchy. However, multiple inheritance in C++ poses several practical problems. For example, it can lead to an object that contains multiple copies of its base class members — a condition known as the “deadly diamond” or “diamond of death”. So, game developers usually limit the use of multiple inheritance in their class hierarchy.

Deadly diamond
Deadly diamond

  • Dynamic binding (virtual function calls) leads to a slight overhead in terms of performance, as the compiler needs to generate extra code to support dynamic binding. Moreover, it affects memory too because an instance of an inherited class also contains a pointer (vPointer) to the virtual table (vTable) associated with it in its memory layout, as you can see in the diagram below. Of course, you could say “This overhead is often outweighed by the advantages of code flexibility and maintainability that come with using virtual functions” but it’s definitely a tradeoff when developing a complex architecture.

vTable
Memory layout of the class instance

"Has-A" design

Converting the “is-a” relationship into the “has-a” one can be a useful technique for reducing the width, depth, and complexity of a game’s class hierarchy. Let’s imagine we have a game object that should have render, animation, ai, and physics logic. Using the “has-a” technique we could make classes relationships looks like this:

has-a example

In code, it may look like this:

class GameObject {
    MeshInstance* m_meshInstance { nullptr };
    AnimationController* m_animController { nullptr };
    Transform* m_transform { nullptr };
    RigidBody* m_rigidBody { nullptr };
};

Another flexible approach is to implement GameObject with a generic component array:

Gameobject composition

The code may look like this:

class GameObject {
   // ...
   void AddComponent(std::shared_ptr<GenericComponent> component);
   
   std::unordered_map<ComponentId, std::shared_ptr<GenericComponent>> m_components;
   // ...
};

The benefit of this technique is that we can create different game object types using the same class.

Problems with "has-a" design

Let’s imagine we used the “has-a” design for our entity component system and our main game loop may look like so:

while (!gameOver)
{
   // Process AI.
   for (int i = 0; i < numEntities; i++)
   {
      gameobjects[i]->GetAIComponent()->update();
   }
   // Update physics.
   for (int i = 0; i < numEntities; i++)
   {
      gameobjects[i]->GetPhysicsComponent()->update();
   }
   // Update animations.
   for (int i = 0; i < numEntities; i++)
   {
      gameobjects[i]->GetAnimationComponent()->update();
   }
   // Draw to screen.
   for (int i = 0; i < numEntities; i++)
   {
      gameobjects[i]->GetRenderComponent()->render();
   }
}

Nothing wrong at first glance. But let’s talk about cache. A cache is a small chunk of contiguous memory that is built into the CPU chip. Data is transferred between main memory and cache in blocks of fixed size, called cache lines. If the next byte of data you need happens to be in that chunk, the CPU reads it straight from the cache, which is much faster than hitting RAM. Successfully finding a piece of data in the cache is called a cache hit. If it can’t find it in there and has to go to main memory, that’s a cache miss.

When we use the “has-a” design, finding memory locations the CPU needs to process may look like so:

Data locality pointer chasing

I took this image from Robert Nystrom’s great book “Game Programming Patterns”. He described entity component system and problems with CPU caching in the Data Locality pattern chapter.

Briefly speaking, we have no idea how components of every type are lying in the memory. They are definitely not placed сontiguously in memory and so we will have cache misses and the performance of our game will be affected.

What we can do to improve our performance and make our system more cache-friendly? Let’s see a property-centric approach.

Property-centric approach and data locality

Jason Gregory’s Property-centric approach and Robert Nystrom’s Data locality pattern are all about the same way to organize memory for game entities.

Instead of having game objects as holders for components, we’ll have a big array for each type of component: a flat array of AI components, another for physics, another for animation, and another for rendering.

Like this:

AIComponent* aiComponents = new AIComponent[MAX_ENTITIES];
PhysicsComponent* physicsComponents = new PhysicsComponent[MAX_ENTITIES];
AnimationComponent* animationComponents = new AnimationComponent[MAX_ENTITIES];
RenderComponent* renderComponents = new RenderComponent[MAX_ENTITIES];

Now our game loop may look like so:

while (!gameOver)
{
   // Process AI.
   for (int i = 0; i < numEntities; i++)
   {
      aiComponents[i].update();
   }
   // Update physics.
   for (int i = 0; i < numEntities; i++)
   {
      physicsComponents[i].update();
   }
   // Update animations.
   for (int i = 0; i < numEntities; i++)
   {
      animationComponents[i].update();
   }
   // Draw to screen.
   for (int i = 0; i < numEntities; i++)
   {
      renderComponents[i].render();
   }
  // ...
}

Instead of skipping around in memory, we’re doing a straight crawl through four contiguous arrays. Next simplified illustration of the memory layout of the game components from the “Game programming patterns” book:

Data locality component arrays

ECS introduction

Now let’s talk about ECS.

Actually, the terms Entity-Component System and ECS are often used interchangeably, but the term ECS has become more commonly used in recent years to refer specifically to a variant of the Entity-Component System architecture that is based on a data-oriented design. Briefly speaking, data-oriented design and ECS particularly are all about property-centric approach described above. I will use the ECS term from now on to describe a system that I will be implementing.

Typical ECS architecture consists of the following parts:

  • Entity. Usually, it’s represented by an integer number and is used as a unique game object id.
  • Component. Game object data represented by so-called POD (plain old data) C++ structure/class without behavior.
  • System. A system is a process that acts on all entities with the desired components. It contains game object behavior.

Architecture based on ECS principle is more multithreading-friendly as we have logically separate subsystems that can be processed in parallel, and it’s another benefit.

My implementation

I will follow the ECS architecture style described above, and I want to point out a few things my ECS to follow:

  • Not overcomplicated. I don’t want a system that is as huge as the entire engine.
  • Data-oriented design in the core.
  • Usability and extensibility. It should be easy to work with new entities and components.
  • Convenient interface to use.
  • A “System” can be represented by a class or single function. It will depend on the logic we want to have.
  • Practical-oriented. It should be usable in the real game/engine and not be just code snippets.

First iteration: MVP

In the first iteration of my ECS, I will implement Entity and Component logic and show you how it can be used. Then I will improve the system for better usability and extensibility point. Also, I will add an optional “System” part of the architecture.

You can find the final version of the ECS integrated into the game engine in my GitHub repository.

Let’s start coding.

Entity

As described above Entity is represented as an integer number (unique identifier). Also, I would add a constant to indicate an invalid entity with a value of 0. Of course, we need a function to generate an Entity id. We could use a random generator for this but I would better use monotonic increment because it can be more appropriate if we will decide to use archetypes in our architecture (see potential improvements part).

By the way, I use atomic type to synchronize access to the counter when multiple threads try to get access to it. Our code will be the next:

// Entity.h
using Entity = uint64_t;
inline constexpr Entity INVALID_ENTITY_ID = 0;

inline Entity GenerateEntityId()
{
   static std::atomic<Entity> next { INVALID_ENTITY_ID + 1 };
   return next.fetch_add(1);
}

Component

Firstly, we need to distinguish component types. I’ve decided to make ComponentTypeId, helper constants, and logic to get the next component type id:

// Component.h
#include <bitset>

using ComponentTypeId = std::uint64_t;

inline constexpr ComponentTypeId INVALID_COMPONENT_TYPE_ID = 0;
inline constexpr std::uint32_t INIT_COMPONENT_POOL_CAPACITY = 100'000;
inline constexpr std::uint32_t MAX_COMPONENT_TYPES = 64;

using ComponentMask = std::bitset<MAX_COMPONENT_TYPES>;

class ComponentTypeIdHolder {
public:
    static ComponentTypeId s_componentTypeCounter;
};

template<class ComponentType>
inline ComponentTypeId GetComponentTypeId()
{
    static ComponentTypeId id = INVALID_COMPONENT_TYPE_ID;
    if (id == INVALID_COMPONENT_TYPE_ID) {
        if (ComponentTypeIdHolder::s_componentTypeCounter < MAX_COMPONENT_TYPES)
            id = ++ComponentTypeIdHolder::s_componentTypeCounter;
        else {
            EL_CORE_ERROR("Maximum component types limit reached = {0}!", MAX_COMPONENT_TYPES);
            EL_CORE_ASSERT(false);
            return 0;
        }
    }

    return id;
}

I think there is nothing difficult to understand. We have a static variable that holds the used component type count. Also, we have a global function to get the following component type id using this variable.

Now, let’s see how we want to work with our components. We want to create a pool of components with a particular component type. Since we may have a lot of components of one type, the std::vector container will be an appropriate choice for our pool. Also, we may want to have generic add/get/remove possibilities for that. So, I will create a skeleton for our generic ComponentPool class like this:

// Component.h
template<class ComponentType>
class ComponentPool {
public:
   ComponentType& AddComponent()
   {
      // ...
   }

   void RemoveComponent(Entity entity)
   {
      // ...
   }

   ComponentType& GetComponent(Entity entity)
   {
      // ...
   }

   std::vector<ComponentType>& GetComponents()
   {
      return m_components;
   }

   const std::vector<ComponentType>& GetComponents() const
   {
      return m_components;
   }

private:
   std::vector<ComponentType> m_components;
};

How will we distinguish components per particular entity?
We need to hold information about an entity-component connection.

For that, I add a separate pool of entities that contains this type of component using vector, where an index is a component index that is the same for index from m_components pool and value - an entity id associated with this component.

And additional std::unordered_map where the key is an Entity id and value - component index from the m_components pool associated with this entity.

Here is a visualization of these relationships:

Component pool container relationships

You may ask, “Why don’t you just use vectors of components, where an index is an Entity id?”. Well, you can, but then component arrays will have a memory layout like this:

Component array sparse

There is a problem when a particular component is used infrequently, and the array stores enough memory for all active entities. It might not be the problem for small games where component arrays contain dozens or hundreds of objects per component type. But when there are thousands of objects per type and we have entities that don’t have all of the component types, I think it’s definitely not good.

It is a cause why I use an additional vector and unordered map to support entity-component connection. It will have a performance hit while processing components of different types at one for loop, but let’s talk about it later.

// Component.h
template<class ComponentType>
class ComponentPool {
// ...

private:
   std::vector<ComponentType> m_components;
   std::vector<Entity> m_entities;
   std::unordered_map<Entity, std::uint64_t> m_entityToComponentIndex;
};

Now let’s fill our functions with all the logic we need.

// Component.h
template<class ComponentType>
class ComponentPool {
public:
   ComponentPool()
   {
      m_components.reserve(INIT_COMPONENT_POOL_CAPACITY);
   }

   void Clear()
   {
      m_components.clear();
      m_entities.clear();
      m_entityToComponentIndex.clear();
   }

   template<typename... Args>
   ComponentType& AddComponent(Entity entity, Args&&... args)
   {
      m_entityToComponentIndex.insert({ entity, m_components.size() });
      m_components.emplace_back(std::forward<Args>(args)...);
      m_entities.emplace_back(entity);
      return m_components.back();
   }

   ComponentType& AddComponent(Entity entity, ComponentType&& component)
   {
      m_entityToComponentIndex.insert({ entity, m_components.size() });
      m_components.emplace_back(std::move(component));
      m_entities.emplace_back(entity);
      return m_components.back();
   }

   void RemoveComponent(Entity entity) override
   {
      auto it = m_entityToComponentIndex.find(entity);
      EL_CORE_ASSERT(it != m_entityToComponentIndex.end());

      const std::uint64_t componentIndex = it->second;

      if (componentIndex < m_components.size() - 1) {
         // replace dead component with the last one
         m_components[componentIndex] = std::move(m_components.back());
         m_entities[componentIndex] = std::move(m_entities.back());

         const Entity movedComponentEntityId = m_entities.back();
         m_entityToComponentIndex[movedComponentEntityId] = componentIndex; // new mapping for moved component
      }

      m_entityToComponentIndex.erase(it);

      if (!m_components.empty()) {
         m_components.pop_back();
         m_entities.pop_back();
      }
   }

   Entity GetEntity(std::uint32_t componentIndex) const override
   {
      return m_entities[componentIndex];
   }

   ComponentType& GetComponent(Entity entity)
   {
      auto it = m_entityToComponentIndex.find(entity);

      EL_CORE_ASSERT(it != m_entityToComponentIndex.end());

      return m_components[it->second];
   }

   std::vector<ComponentType>& GetComponents()
   {
      return m_components;
   }

   const std::vector<ComponentType>& GetComponents() const
   {
      return m_components;
   }

private:
   std::vector<ComponentType> m_components;
   std::vector<Entity> m_entities;
   std::unordered_map<Entity, std::uint64_t> m_entityToComponentIndex;
};

Actually, MVP is ready to use in a simple manner.

#include <Entity.h>
#include <Component.h>

int main()
{
   ComponentPool<AIComponent> aiComponents;
   ComponentPool<PhysicsComponent> physicsComponents;
   ComponentPool<AnimationComponent> animationComponents;
   ComponentPool<RenderComponent> renderComponents;

   Entity player = GenerateEntityId();

   RenderComponent playerRender;
   renderComponents.AddComponent(player, playerRender);
   PhysicsComponent playerPhysics;
   physicsComponents.AddComponent(player, playerPhysics);
   AnimationComponent playerAnimation;
   animationComponents.AddComponent(player, playerAnimation);

   Entity monster = GenerateEntityId();

   RenderComponent monsterRender;
   renderComponents.AddComponent(monster, monsterRender);
   PhysicsComponent monsterPhysics;
   physicsComponents.AddComponent(monster, monsterPhysics);
   AnimationComponent monsterAnimation;
   animationComponents.AddComponent(monster, monsterAnimation);

   // update
   while (!gameOver)
   {
      for (auto& aiComponent : aiComponents.GetComponents()) {
         // process ai component
      }
      
      for (auto& physicsComponent : physicsComponents.GetComponents()) {
         // process physics component
      }
      
      for (auto& animationComponent : animationComponents.GetComponents()) {
         // process animation component
      }
      
      for (auto& renderComponent : renderComponents.GetComponents()) {
         // draw to screen using render component
      }
   }
   
   // ...

   return 0
}

Such an entity-component system may be enough for developing small games or when developers can extend engine code to add custom component types (ComponentPool objects).

But I would try to make it more friendly for the engine users, so the developer won’t need to think about where and how he should add new components and how to interact with them.

Second iteration

MVP has a simple interface of using. But as I mentioned above, I would like to make a more extensibility-friendly system.

Scene

Simple games may have only one scene, but complex big games have a lot of them. A scene has its unique entities with particular components.

Firstly, we need to hold all Component Pools our scene has. I think an unordered_map container where a key is a ComponentType, and a value corresponds ComponentPool is an appropriate choice.

Secondly, we need to hold all active entities of the scene. We could keep them in the vector container. But how could we know what components are connected to a particular entity? We may use ComponentPool to check whether it has a component that is connected to the entity. But when we need to destroy an entity, we should iterate all ComponentPool instances and call the RemoveComponent function to check whether we have the entity in this ComponentPool. So, I’ve added an unordered_map container (m_entities) in the Scene class for this purpose. A key will be an Entity id and a value will be a bitmask. Every bit in bitmask corresponds to a particular ComponentType. Also, this container will help implement SceneView logic and Scene Serialization feature when we need to filter out entities with a specific signature (see potential improvements part).

Entity-component bitmask

Since the Scene class will contain ComponentPools as pointers in one container, we need to support it. I’ve made an interface class for this purpose. ComponentPool class inherits this interface.

// Component.h
struct IComponentPool {
   virtual ~IComponentPool() = default;

   virtual void Clear() = 0;
   virtual void RemoveComponent(Entity entity) = 0;
   virtual Entity GetEntity(std::uint32_t componentIndex) const = 0;
};

template<class ComponentType>
class ComponentPool final : public IComponentPool {
public:
// ...
   void Clear() override
   {
      // ...
   }

   void RemoveComponent(Entity entity) override
   {
      // ...
   }

   Entity GetEntity(std::uint32_t componentIndex) const override
   {
      // ...
   }
// ...
}

The ComponentPool class functions code is identical to the previous ComponentPool code snippet.

Now we can keep the components pool in the Scene class.

// Scene.h
class Scene {
private:
   std::unordered_map<ecs::ComponentTypeId, SharedPtr<ecs::IComponentPool>> m_componentPools;
   std::unordered_map<ecs::Entity, ecs::ComponentMask> m_entities;
};

Let’s think about the Scene interface and how we want to interact with our scene and ECS. The main points are:

  • create/destroy entity; get all entities;
// Scene.h, Scene class member function
ecs::Entity CreateEntity()
{
   const ecs::Entity id = ecs::GenerateEntityId();
   m_entities.insert({ id, ecs::ComponentMask() });

   return id;
}

const std::unordered_map<ecs::Entity, ecs::ComponentMask>& GetEntities() const
{
   return m_entities;
}

void DestroyEntity(ecs::Entity entity)
{
   auto entityIt = m_entities.find(entity);

   if (entityIt != m_entities.end()) {
      const auto& componentMask = entityIt->second;
      for (size_t i = 0; i < componentMask.size(); ++i) {
            if (componentMask.test(i)) {
               m_componentPools[i]->RemoveComponent(entity);
            }
      }
      m_entities.erase(entityIt);
   }
}
  • register new component type;
// Scene.h, Scene class member function
template<typename ComponentType>
void RegisterComponent()
{
   const ecs::ComponentTypeId componentTypeId = ecs::GetComponentTypeId<ComponentType>();
   auto it = m_componentPools.find(componentTypeId);
   if (it == m_componentPools.end()) {
      m_componentPools.insert({ componentTypeId, MakeSharedPtr<ecs::ComponentPool<ComponentType>>() });
   } else {
      EL_CORE_WARN("Component type is already registered with id = {0}", componentTypeId);
   }
}
  • add component with or without arguments. We need to support both default constructor and constructor with arguments for our components structures;
// Scene.h, Scene class member function
template<typename ComponentType, typename... Args>
ComponentType& AddComponent(ecs::Entity entity, Args&&... args)
{
   const ecs::ComponentTypeId componentTypeId = ecs::GetComponentTypeId<ComponentType>();

   auto it = m_componentPools.find(componentTypeId);
   EL_CORE_ASSERT(it != m_componentPools.end(), "Failed to add component. Component type isn't registered.");

   auto entityIt = m_entities.find(entity);
   EL_CORE_ASSERT(entityIt != m_entities.end(), "The entity is not found.");
   entityIt->second.set(componentTypeId);

   return std::static_pointer_cast<ecs::ComponentPool<ComponentType>>(it->second)->AddComponent(entity, std::forward<Args>(args)...);
};

template<typename ComponentType>
ComponentType& AddComponent(ecs::Entity entity, ComponentType&& component)
{
   const ecs::ComponentTypeId componentTypeId = ecs::GetComponentTypeId<ComponentType>();

   auto it = m_componentPools.find(componentTypeId);
   EL_CORE_ASSERT(it != m_componentPools.end(), "Failed to add component. Component type isn't registered.");

   auto entityIt = m_entities.find(entity);
   EL_CORE_ASSERT(entityIt != m_entities.end(), "The entity is not found.");
   entityIt->second.set(componentTypeId);

   return std::static_pointer_cast<ecs::ComponentPool<ComponentType>>(it->second)->AddComponent(entity, std::forward<ComponentType>(component));
};
  • remove component;
// Scene.h, Scene class member function
template<typename ComponentType>
void RemoveComponent(ecs::Entity entity)
{
   auto entityIt = m_entities.find(entity);
   EL_CORE_ASSERT(entityIt != m_entities.end(), "The entity isn't found.");

   const ecs::ComponentTypeId componentTypeId = ecs::GetComponentTypeId<ComponentType>();
   auto it = m_componentPools.find(componentTypeId);
   EL_CORE_ASSERT(it != m_componentPools.end(), "Failed to remove component. Component type isn't registered");

   entityIt->second.set(componentTypeId, false);
   it->second->RemoveComponent(entity);
};
  • check whether we have a component for the entity;
// Scene.h, Scene class member function
template<typename ComponentType>
bool HasComponent(ecs::Entity entity)
{
   auto entityIt = m_entities.find(entity);
   EL_CORE_ASSERT(entityIt != m_entities.end(), "The entity isn't found");

   const ecs::ComponentTypeId componentTypeId = ecs::GetComponentTypeId<ComponentType>();
   return entityIt->second.test(componentTypeId);
}
  • get component by entity id;
// Scene.h, Scene class member function
template<typename ComponentType>
ComponentType& GetComponent(ecs::Entity entity)
{
   auto entityIt = m_entities.find(entity);
   EL_CORE_ASSERT(entityIt != m_entities.end(), "The entity isn't found");

   const ecs::ComponentTypeId componentTypeId = ecs::GetComponentTypeId<ComponentType>();
   EL_CORE_ASSERT(entityIt->second.test(componentTypeId), "The entity hasn't component of this type.")

   auto it = m_componentPools.find(componentTypeId);
   EL_CORE_ASSERT(it != m_componentPools.end(), "Failed to get component. Component type isn't registered.")

   return std::static_pointer_cast<ecs::ComponentPool<ComponentType>>(it->second)->GetComponent(entity);
}
  • get all components of a particular type;
// Scene.h, Scene class member function
template<typename ComponentType>
std::vector<ComponentType>& GetComponents() const
{
   const ecs::ComponentTypeId componentTypeId = ecs::GetComponentTypeId<ComponentType>();
   auto it = m_componentPools.find(componentTypeId);
   EL_CORE_ASSERT(it != m_componentPools.end(), "Failed to get components. Component type isn't registered.");

   return std::static_pointer_cast<ecs::ComponentPool<ComponentType>>(it->second)->GetComponents();
}
  • check whether component type is registered;
// Scene.h, Scene class member function
template<typename ComponentType>
bool IsComponentTypeRegistered() const
{
   const ecs::ComponentTypeId componentTypeId = ecs::GetComponentTypeId<ComponentType>();
   auto it = m_componentPools.find(componentTypeId);

   return it != m_componentPools.end();
}
  • get component pool by type;
// Scene.h, Scene class member function
template<typename ComponentType>
SharedPtr<ecs::ComponentPool<ComponentType>> GetComponentPool()
{
   const ecs::ComponentTypeId componentTypeId = ecs::GetComponentTypeId<ComponentType>();
   auto it = m_componentPools.find(componentTypeId);
   EL_CORE_ASSERT(it != m_componentPools.end(), "Failed to get components. Component type isn't registered.");

   return std::static_pointer_cast<ecs::ComponentPool<ComponentType>>(it->second);
}
  • get entity by component index.
// Scene.h, Scene class member function
template<typename ComponentType>
ecs::Entity GetEntity(std::uint32_t componentIndex) const
{
   const ecs::ComponentTypeId componentTypeId = ecs::GetComponentTypeId<ComponentType>();
   auto it = m_componentPools.find(componentTypeId);
   EL_CORE_ASSERT(it != m_componentPools.end(), "Failed to get entity by component index. Component type isn't registered.");

   return it->second->GetEntity(componentIndex);
}

Now, you can easily interact with ECS through the Scene interface like this:

   // ...
   // create scene
   Scene scene;

   // register components we need (add ComponenPool objects to the Scene container)
   scene.RegisterComponent<TrasformComponent>();
   scene.RegisterComponent<MeshComponent>();
   scene.RegisterComponent<AIComponent>();
   scene.RegisterComponent<PhysicsComponent>();
   scene.RegisterComponent<AnimationComponent>();

   // fill player entity with components
   Entity player = scene.CreateEntity();
   scene.AddComponent<RenderComponent>(player, /* args ...*/);
   scene.AddComponent<PhysicsComponent>(player, /* args ...*/);
   AnimationComponent animComponent;
   // ...
   // some work with animComponent
   // ...
   scene.AddComponent<AnimationComponent>(player, animComponent);

   // fill monster entity with components
   Entity monster = scene.CreateEntity();
   scene.AddComponent<RenderComponent>(monster, /* args ...*/);
   auto& aicomponent = scene.AddComponent<AIComponent>(monster);
   // ... 
   // some work with ai component
   // ...
   scene.AddComponent<AnimationComponent>(monster, /* args ...*/);
   scene.AddComponent<PhysicsComponent>(monster, /* args ...*/);


   // update loop
   while (!gameOver) {
      for (auto& aiComponent : scene->GetComponents<AiComponent>()) {
         // process ai component
      }

      for (auto& physicsComponent : scene->GetComponents<PhysicsComponent>()) {
         // process physics component
      }

      for (auto& animationComponent : scene->GetComponents<AnimationComponent>()) {
         // process animation component
      }

      // on renderer stage, entities with two component types are processed
      auto& meshComponentPool = scene->GetComponentPool<MeshComponent>();
      auto& transformComponentPool = scene->GetComponentPool<TransformComponent>();

      for (uint32_t index = 0; index < meshComponentPool->GetComponents().size(); ++index) {
         auto& meshComponent = meshComponentPool->GetComponent(index);
         const ecs::Entity entity = meshComponentPool->GetEntity(index);
         auto& transformComponent = transformComponentPool->GetComponent(entity);
         // draw to screen 
      }
   }

// ...

Systems

As you saw in the example above you need no “System” to interact with the Entity-Component system consistently. But “System” could be useful for complex game/game engine architecture structures. It’s just another abstractions layer.

There are different approaches to implementing the System part of ECS. You may register a system with particular component types that will be used there, filter out entities on the pre-update stage, for example, etc.

But I still adhere to minimalistic ECS architecture to avoid too much abstraction.

There are next points our “System” part should follow:

  • System interface class with OnInit, OnShutdown, OnUpdate, and OnRender functions which correspond to different runtime stages.
  • Systems objects will be contained inside the Scene class in the std::vector container.
  • It should be registered manually using the Scene class.
  • It should have a pointer to the Scene instance for convenient access to the entities and components.

Let’s start with System interface class.

class IComponentSystem {
public:
   IComponentSystem(Scene* scenePtr)
      : m_pScene(scenePtr)
   {
      OnInit();
   }

   virtual ~IComponentSystem() = default;

   virtual void OnInit() { }
   virtual void OnShutdown() { }
   virtual void OnUpdate(float dt) { }
   virtual void OnRender(float dt) { }

protected:
   Scene* m_pScene { nullptr };
};

Now, let’s add a container that will hold our system objects inside the Scene class. And implement the RegisterSystem function to register our systems.

Besides, we need to support system stages inside our Scene class.

// Scene.h
class Scene {
public:
   void OnInit();
   void OnShutdown();
   void OnUpdate(float dt);
   void OnRender(float dt);

   // ...
   template<typename SystemType>
   void RegisterSystem()
   {
      m_systems.emplace_back(std::make_unique<SystemType>(this));
      m_systems.back()->OnInit();
   }

   // ...

private:
   std::vector<std::unique_ptr<IComponentSystem>> m_systems;
}

// Scene.cpp
void Scene::OnInit()
{
   // can be used to register engine components and systems
   RegisterSystem<SpriteRenderSystem>();
}

void Scene::OnShutdown()
{
   for (auto& system : m_systems) {
      system->OnShutdown();
   }
}

void Scene::OnUpdate(float dt)
{
    for (auto& system : m_systems) {
        system->OnUpdate(dt);
    }
}

void Scene::OnRender(float dt)
{
    for (auto& system : m_systems) {
        system->OnRender(dt);
    }
}

Now we can use it like so:

// ...

class SpriteRenderSystem : public IComponentSystem {
public:
   SpriteRenderSystem(Scene* scenePtr) : IComponentSystem(scenePtr) { }

   void OnInit() override
   {
      // init
   }
   void OnRender(float dt) override
   {
      for (auto& sprite : m_pScene->GetComponents<SpriteComponent>()) {
         // render sprite
      }
   }

private:
   // members
}

Behavior components / scripts

Another great feature is the behavior component or just a script. It’s more similar to a script that can be attached to entities in such engines as Unity, Unreal Engine, etc.

Let’s think about how we want to use this feature. I want to add custom behavior to entities in a consistent manner, for instance.

  • It could be inherited from some class that can be passed to BehaviorComponent and processed automatically on the different runtime stages of the game.
  • I don’t want this Behavior is being executed not in runtime (e.g. Editor logic, menu, etc.).
  • I want to have a pointer to the Scene object to access ECS.
  • I want some function like GetComponent to get any component I need for an entity.

Behavior class may look like this:

class IBehavior {
public:
   IBehavior();

   virtual ~IBehavior() = default;

   virtual void OnCreate() { }
   virtual void OnDestroy() { }
   virtual void OnUpdate(float dt) { }
   virtual void OnRender(float dt) { }

   void SetEntity(ecs::Entity entity)
   {
      m_entity = entity;
   }

   template<class ComponentType>
   ComponentType& GetComponent()
   {
      return p_Scene->GetComponent<ComponentType>(m_entity);
   }

protected:
   Entity m_entity { 0 };
   elv::Scene* p_Scene { nullptr };
};

Now, we need to create BehaviorComponent that will contain a Behavior object.

struct BehaviorComponent {
   BehaviorComponent() = default;
   BehaviorComponent(BehaviorComponent&&) = default;
   ~BehaviorComponent()
   {
      if (behavior) {
         behavior->OnDestroy();
      }
   }

   BehaviorComponent& operator=(BehaviorComponent&& other) = default;

   template<class BehaviorType>
   void Bind()
   {
      instantiateBehavior = []() { return std::make_unique<BehaviorType>(); };
   }

   void Enable()
   {
      isEnabled = true;
   }

   void Disable()
   {
      isEnabled = false;
   }

public:
   std::unique_ptr<IBehavior> behavior;
   std::function<std::unique_ptr<IBehavior>()> instantiateBehavior;
   bool isEnabled { true };
};

As you may notice, I don’t create a Behavior object during component construction. I use lambda to hold behavior instantiation before we need it. Also, we need to call the OnDestroy function of the behavior object in the destructor. As we explicitly declared destructor we have to explicitly declare the move constructor and move assignment operator too because they are used in ECS components movement.

Also, I need a system that will process the functions of my Behavior class.

// BehaviorSystem.h
class BehaviorSystem final : public IComponentSystem {
public:
   BehaviorSystem(Scene* scenePtr);

   void OnInit() override;
   void OnShutdown() override;
   void OnUpdate(float dt) override;
   void OnRender(float dt) override;

private:
   SharedPtr<ComponentPool<BehaviorComponent>> m_behaviorsPool { nullptr };
};

// BehaviorSystem.cpp
void BehaviorSystem::OnInit()
{
   m_behaviorsPool = m_pScene->GetComponentPool<BehaviorComponent>();
}

void BehaviorSystem::OnShutdown()
{
   auto& behaviorComponents = m_behaviorsPool->GetComponents();
   for (uint32_t i = 0; i < behaviorComponents.size(); ++i) {
      auto& component = behaviorComponents[i];
      if (component.behavior) {
         component.behavior->OnDestroy();
      }
   }
}

void BehaviorSystem::OnUpdate(float dt)
{
   auto& behaviorComponents = m_behaviorsPool->GetComponents();
   for (uint32_t i = 0; i < behaviorComponents.size(); ++i) {
      auto& component = behaviorComponents[i];
      if (component.behavior) {
         if (component.isEnabled) {
               component.behavior->OnUpdate(dt);
         }
      } else {
         const ecs::Entity entity = m_behaviorsPool->GetEntity(i);
         component.behavior = component.instantiateBehavior();
         component.behavior->SetEntity(entity);
         component.behavior->OnCreate();
      }
   }
}
void BehaviorSystem::OnRender(float dt)
{
   for (auto& component : m_behaviorsPool->GetComponents()) {
      if (component.behavior) {
         component.behavior->OnRender(dt);
      }
   }
}

This feature can be used like so:

class PlayerBehavior : public elv::ecs::IBehavior {
public:
   void OnCreate() override
   {
      // members init or some other logic
   }

   void OnUpdate(float dt) override
   {
      TransformComponent& tr = GetComponent<elv::TransformComponent>();

      // some logic with transform component like movement, rotation, etc.
   }

private:
   // members
};

// game code
auto player = scene.CreateEntity();

scene.AddComponent<BehaviorComponent>(player).Bind<PlayerBehavior>();

Game demo

There are a lot of articles about theoretical approaches to building ECS. But practically, there are a lot of pitfalls when implementing it for the game/game engine. As I mentioned above, ECS should be more practical-oriented in this article, therefore I’ve made three classic games using my engine and the ECS system I’ve described here: Invaders, Pong, and Tron. You can find the code of the games in the repo. The invaders game is the most complex and uses the next components:

  • in-engine components:
    • Trasform
    • Sprite
    • Camera
    • Text
    • RectTransform (for the text rendering)
    • AABB
    • Tag
  • game-specific components:
    • Player
    • Enemy
    • Bullet
    • PowerUp

I’ve used the Behavior feature to make the PlayerBehavior script. Also, I’ve used “System” part of ECS to make game-specific systems:

  • BulletMovementSystem
  • EnemyControlSystem
  • PowerupsSystem
  • CombatSystem

I think it’s turned out to be a fun game (turn on the sound 😀):

Potential improvements

SceneView

There is a bunch of stuff that can be improved and changed for better performance and usability. For instance, we can add so-called SceneView (another abstraction layer) support to replace fetching entities of a particular signature. So we can replace entities iteration like so:

// Current version to iterate entities
auto& meshComponentPool = scene->GetComponentPool<MeshComponent>();
auto& transformComponentPool = scene->GetComponentPool<TransformComponent>();

for (uint32_t index = 0; index < meshcomponentPool.size(); ++index) {
   const ecs::Entity entity = meshcomponentPool->GetEntity(index);
   auto& transformComponent = transformComponentPool->GetComponent(entity);
   // draw to screen 
}

// SceneView approach
for (Entity entity : SceneView<MeshComponent, TransformComponent>(scene)) {
   auto& meshComponent = scene->GetComponent(entity);
   auto& transformComponent = scene->GetComponent(entity);
}

Archetypes

As I mentioned previously, there is a performance hit in our ECS when we process entities with a complex signature (it has more than one component per entity). This problem is caused by using unordered_map when we try to find a component of another type using entity (fetch data from other ComponentPool and memory location). It probably isn’t cached, and we have a cache miss.

To fix this problem, we need to reduce the usage of unordered_map. An alternative is an approach to implementing ECS using so-called Archetypes. Archetype is a unique combination of component types stored in contiguous arrays. We don’t need any unordered_map containers when using it because every archetype contains component pools of particular types with the same sizes, and we can easily fetch a component of another type per iteration using an entity as an index. This type of ECS is used in Unity engine for it’s Data-Oriented-Technology-Stack (DOTS).

There is a visualization of such a system.

Archetypes approach

But this approach also has a downside when we need to add or remove components from the entities that cause moving all the component data from one pool to another.

Scene serialization

There are a lot of approaches and libraries you can use to implement serialization for the scene. I will not describe the steps and code I’ve wrote here. You can find my implementation in the project repository. I’ve used the nlohmann’s JSON library to implement it. I would make a separate post about Scene serialization after implementing the Scene hierarchy and its serialization.

Final thoughts

I’ve researched different ways of building an entity-component system for a game engine. All approaches described above have their pros and cons. One is more appropriate for small games other is more performance-friendly but bad in usability. There are tradeoffs that engineers have to adhere to use a suitable type of system.

I’ve implemented ECS and used it to create some simple games (Invaders, Pong, and Tron games that you can find on my Github) to test my game engine and ECS, in particular. I think I’ve made a system that is more data-oriented and cache-friendly. Also, it follows my points of usability and future extensibility.

References

I was inspired by all of the engineers mentioned above and which will be mentioned below. I think it’s worth checking them all if you are interested in game/game engines, data-oriented programming, or want to make reliable and robust software.

comments powered by Disqus