Entity-Component-System

During my first year at The Game Assembly, I developed a component system similar to the one in Unity for handling objects in the game world. It allowed for game objects to be defined through composition over inheritance at runtime, which made development a lot more streamlined. The system was used for all three games I took part in during the second semester of the education, and was even used by another group for one of their projects. However, despite its success, I decided to deviate from traditional object-oriented component systems when it came down to writing an engine from scratch for the game Loa of Death, and opting instead for an Entity-Component-System (ECS) solution. In this article, I will be referring to object-oriented component systems like the one Unity has as component systems, and data-oriented component systems as ECS.

The reasons for moving away from component systems were:

  • Despite being based on principles of composition over inheritance, the components themselves still inherited from a base class. This made them a lot more difficult to debug, since their type could be ambiguous.

  • Behavior was implemented on the components themselves through virtual functions. This, in combination with the fact that components were updated object-by-object instead of component-by-component, meant that frequent instruction cache misses were inevitable.

  • The components pertaining to an object were stored through base class pointers held in a list in that object, meaning that components had to be either individually heap-allocated or pooled according to type. This meant that frequent data cache misses were also inevitable.

ECS solved these problems through:

  • Being based on data-oriented principles, meaning that data is treated as data and not abstracted away behind objects.

  • The data and the behavior are always separated clearly in the code, making optimization and debugging easy.

  • Components are pooled in memory minimizing the number of cache misses.

Design

When I started designing my ECS I came up with a list of requirements it had to meet:

  • Entities needed to be IDs so that raw pointers to objects wouldn't be thrown around.

  • Managing entities and components needed to always be quick and have a time complexity of O(1) so that such operations would perform reliably and predictably.

  • Data (components) and behavior (systems) needed to always be clearly separated.

  • Components needed to be packed in memory to optimize the cache.

  • Systems needed to update components type-wise rather than entity-wise and it needed to be apparent which components each system updated so that data transformations could easily be performed asynchronously.

  • Most importantly, the interface needed to be easy to use for the other programmers, who hadn't been exposed to data-oriented programming before.

Entity

using Entity = uint32_t;
constexpr uint32_t MAX_ENTITIES = 768;

The entity itself is an ID. All data pertaining to an object is stored in its components, which the entity ID maps to.

To be able to create and destroy entities, I needed to keep track of which IDs were available. As described in the requirements list, IDs needed to be retrieved and subsequently returned with minimum overhead, to prevent FPS spikes when spawning/despawning large amounts of entities. I chose to solve this problem using a linked list-like data structure. I created a class to manage entities, EntityService, which contains an array of size MAX_ENTITIES and type Entity. Each index in the array corresponds to that entity ID and points to the next available ID. Lastly, a variable keeps track of the index of the first available entity in the list.

class EntityService
{
public:
  // ...

private:
  Entity myAvailableEntitiesLL[MAX_ENTITIES];
  Entity myFirstAvailableEntity;
};

To get an available entity ID, I simply have to get the first available entity and then set myFirstAvailableEntity to the entity pointed to by the current first available one. To return an entity ID, I do the reverse. This method means that creating and destroying objects is essentially just swapping some integers around in memory.

Get entity:

Entity EntityService::GetEntity()
{
  assert(myFirstAvailableEntity < MAX_ENTITIES && "There are no available entities.");

  Entity newEntity = myFirstAvailableEntity;
  myFirstAvailableEntity = myAvailableEntitiesLL[myFirstAvailableEntity];

  return newEntity;
}

Return entity:

void EntityService::ReturnEntity(Entity anEntity)
{
  assert(anEntity < MAX_ENTITIES && "Entity out of range.");
  assert(myAvailableEntitiesLL[anEntity] >= MAX_ENTITIES && "Attempting to return already available entity.");

  myAvailableEntitiesLL[anEntity] = myFirstAvailableEntity;
  myFirstAvailableEntity = anEntity;
}

Component

Components are implemented as C-style structs, to make sure they only consist of data. This is an example from Toys or Sus:

struct SpotLightComponent
{
  v3f offset;
  CU::Color color;
  float range = 5.0f;
  float angle = 70.0f;
  float baseIntensity = 1.0f;
  LightFlags shadowType = LightFlags_NoShadow;
};

To store the components, I wrote a class called ComponentList, which takes a template argument of which component it stores. Internally, the class has an array of components of size MAX_ENTITIES. This array is always kept dense, to make sequential access fast. This is accomplished by, when removing a component, swapping it for the last component in the list, and then decrementing the size. This works fine for sequential access. However, for random access, I needed a different approach, since components don't map to their entity IDs and they can move in memory. I decided to implement a sparse set-like data structure to handle random access. I added two extra arrays to the class: one to map entity IDs to component indices and the other for mapping component indices to entity IDs.

Adding a component:

template <class ComponentType>
inline ComponentType& ComponentList<ComponentType>::AddComponent(Entity anEntity)
{
  assert(anEntity < MAX_ENTITIES && "Entity out of range.");

  const uint32_t componentIndex = myComponentsSize++;
  myComponents[componentIndex] = ComponentType();
  myMapEntityToComponent[anEntity] = componentIndex;
  myMapComponentToEntity[componentIndex] = anEntity;

  return myComponents[componentIndex];
}

Removing a component:

template <class ComponentType>
inline void ComponentList<ComponentType>::RemoveComponent(Entity anEntity)
{
  assert(anEntity < MAX_ENTITIES && "Entity out of range.");

  --myComponentsSize;
  const uint32_t componentIndex = myMapEntityToComponent[anEntity];
  myComponents[componentIndex] = myComponents[myComponentsSize];
  myMapEntityToComponent[myMapComponentToEntity[myComponentsSize]] = componentIndex;
  myMapComponentToEntity[componentIndex] = myMapComponentToEntity[myComponentsSize];
}

System

Systems are just functions operating on ComponentList instances. Systems have no state of their own, and they cannot alter state outside their scope. Meaning that you always know that a system will only operate on the data that is provided in the function's argument list. This means that multiple systems can run asynchronously, without ever operating on the same data, as long as they take in different component lists.

Here is a short example of a system from the demo. This system operates on both TransformComponent and ModelComponent in the same loop, meaning that only one of them can be accessed sequentially and the other needs to be accessed through the random access pattern. This means using the component-to-entity map in the model list to look up which entity contains the current ModelComponent, and then looking up that entity's TransformComponent using the entity-to-component map in the transform list.

void Systems::Render(
  ComponentList<TransformComponent>* someTransformComps,
  ComponentList<ModelComponent>* someModelComps)
{
    ModelComponent* modelList = someModelComps->GetDenseComponents();

    const uint32_t count = someModelComps->GetSize();
    for (uint32_t compIndex = 0U; compIndex < count; ++compIndex)
    {
        const TransformComponent& trs = someTransformComps->GetComponent(someModelComps->GetEntityFromComponent(compIndex));
        const ModelComponent& model = modelList[compIndex];

        DrawModel(*ModelManager::GetModel(model.myModel), trs.myPosition, model.myScale, model.myColor);
    }
}

Result

The ECS has been used in two games: Loa of Death and Toys or Sus. In both games, it has performed impeccably, and has been received well by the other programmers who were able to pick it up and start using it with minimal friction. A big part of ECS which I haven't touched on in this article is that it's trivial to make data-driven, since all objects are defined at runtime. I go into more detail on how I did this for Toys or Sus in the article Editor.

Demo

To showcase the ECS in action, I put together a small demo using raylib, which I compiled to WebAssembly using the Emscripten compiler. Rotate the camera by holding down left/right click and moving the mouse and zoom in and out using the scroll wheel. You can add and remove entities using the buttons to the right.

Mobile support may be limited.

Full source code can be found on GitHub.

Future improvements

These are some improvements to the ECS I would like to implement in the future:

  • Support for component lists of different sizes, not just MAX_ENTITIES. Since I'm using a sparse set, this would not be difficult to implement and it would save a lot of memory when only a few components of a certain type are needed.

  • Similarly, I would like to implement support for growing component lists, when I don't know at compile time how many components of a type I'm going to need. For instance, when behavior is defined at runtime through visual scripting.

  • Integrate a job system, to make it even easier to multi-thread systems.