Skip to content

Entity Component System (ECS)

ERS uses an Entity-Component-System (ECS) to structure simulation objects. An ECS consists of three important elements:

  • Entities: An entity represents a general-purpose object.
  • Components: A component gives certain properties to an entity. It holds data or creates behavior.
  • Systems: A system operates on entities with certain components.

The advantage of using an ECS is the ability to re-use components to create different objects, and efficient memory usage (which ERS will take care off for you).

Entities

In an ECS, objects are represented as entities. In ERS, each entity is stored as an unsigned 64-bit integer (uint64_t), its EntityID. The value 0 is reserved for a specific entity: the invalid entity. The invalid entity is used to indicate entities that do not exist, or is used as a placeholder before an entity is created.

Entities can represent any object in a simulation. For example, an entity could represent a (part of) a conveyor or a product on the conveyor. Entities are structured in a hierarchical tree where each entity can have zero or one parent, and zero or more children. This can be useful to create a clearer overview of all entities in a model or create behavior between entities based on their relations to each other. These relations are ordered: iterating over the children of an entity always happens in the same order, and siblings refer to their previous or next sibling (RelationComponent). Entities do not hold data or do anything unless you add components to them. However, entities can have a name (NameComponent).

If you are familiar with Enterprise Dynamics (ED) or Pedestrian Dynamics (PD), entities are similar to atoms, except that atoms can actually hold data and functionality / behavior on their own.

To create an entity, use the CreateEntity method on the submodel on which you wish to create the entity. This function has a couple of different variants so you can choose to give it a name and/or create it as a child to a given parent entity. Of course, you can always change the name or the parent later, after the entity is already created. You can also search for entities with FindEntity and delete entities with DestroyEntity, both defined on SubModel.

// Create an entity
Ers::SubModel subModel = Ers::SubModel::Get();
EntityID someEntity = subModel.CreateEntity("SomeEntity");

// Search for an entity
EntityID foundEntity = subModel.FindEntity("SomeEntity");
assert(someEntity == foundEntity); // Evaluates to true

// Destroy an entity
subModel.DestroyEntity(someEntity);
using Entity = UInt64;

// Create an entity
SubModel subModel = SubModel.Get();
Entity someEntity = subModel.CreateEntity("SomeEntity");

// Search for an entity
Entity foundEntity = subModel.FindEntity("SomeEntity");
Debug.Assert(someEntity == foundEntity); // Evaluates to true

// Destroy an entity
subModel.DestroyEntity(someEntity);

Components

As mentioned, entities don't do anything unless you add components to them. An entity can have zero or more components attached to them. However, it can have only one component of each type. ERS uses two types of components: data components and script behavior components.

There are functions to add a component to an entity, check if an entity has a certain component, get a certain component attached to an entity, and remove a component from an entity:

Ers::SubModel& subModel = Ers::Get();
EntityID entity = subModel.CreateEntity("SomeEntity");

// Add a transform component to the given entity.
// Notice the component is automatically returned so you can use it without having to call GetComponent.
auto* transform = subModel.AddComponent<Ers::TransformComponent>(entity);
// Check whether the given entity has a transform component
bool hasComponent = subModel.HasComponent<Ers::TransformComponent>(entity);
// Get a component from the given entity.
auto* transform = subModel.GetComponent<Ers::TransformComponent>(entity);
// Remove a component from the given entity.
subModel.RemoveComponent<Ers::TransformComponent>(entity);
using Entity = UInt64;

SubModel subModel = SubModel.Get();
Entity entity = subModel.CreateEntity("SomeEntity");

// Add a transform component to the given entity.
// Notice the component is automatically returned so you can use it without having to call GetComponent.
Ref<TransformComponent> transform = subModel.AddComponent<TransformComponent>(entity);
// Check whether the given entity has a transform component
bool hasComponent = subModel.HasComponent<TransformComponent>(entity);
// Get a component from the given entity.
Ref<TransformComponent> transform = subModel.GetComponent<TransformComponent>(entity);
// Remove a component from the given entity.
subModel.RemoveComponent<TransformComponent>(entity);

// In C#, it is also possible to use these function on the entity itself:
Ref<TransformComponent> transform = entity.GetComponent<TransformComponent>();

C# note

As you may have noticed, the functions in C# return a Ref<T>, where T is the component type. See Ref<T> in the C# section for more information.

When you define your own components, they also need to be registered in ERS:

Ers::ComponentRegistry<MyComponent>::Register();
ComponentRegistry<MyComponent>.Register();

Data Components

A data component only holds data, but does not perform any actions. For example, the built-in NameComponent gives entities a name and the TransformComponent gives entities a position, rotation and scale.

It is important to know that data components cannot hold every type of data: they can only hold basic data types, like: integers, booleans, doubles, floats. Types that can be represented as one of those can also be used, such as enums. Strings can not be used in data components as they are more complex types with indirection / reference. Data components cannot use types with indirection / a reference, because those can not be safely stored in memory by ERS. For this same reason, data components can not hold classes if these are by-reference (for example, in C#). Neither can they hold lists or arrays (unless the array is fixed size by-value).

You can define your own components by defining a struct. For example, a component that specifies that a component can have a maximum capacity of some item.

#include "Ers/SubModel/DataComponent.h"

struct Capacity : public Ers::DataComponent
{
    uint64_t Current;
    uint64_t Maximum;
};
using System.Runtime.InteropServices;
using Ers;

// Ensure the members are laid out correctly for ERS to use
[StructLayout(LayoutKind.Sequential)]
public struct Capacity : IDataComponent
{
    public ulong Current;
    public ulong Maximum;
}

The simulation could now automatically take the capacity into account for any entity that has a capacity component attached to it:

Ers::SubModel& subModel = Ers::Get();
EntityID entity = subModel.FindEntity("SomeEntity");
if (entity.HasComponent<Capacity>())
{
    Capacity* capacity = subModel.GetComponent<Capacity>(entity);
    if (capacity->Current >= capacity->Maximum)
        return;
}

// Maximum capacity has not been reached, it is safe to perform some actions here
using Entity = UInt64;

Entity entity = SubModel.Get().FindEntity("SomeEntity");
if (entity.HasComponent<Capacity>())
{
    Ref<Capacity> capacity = entity.GetComponent<Capacity>();
    if (capacity.Value.Current >= capacity.Value.Maximum)
        return;
}

// Maximum capacity has not been reached, it is safe to perform some actions here

As you can see, we use the HasComponent function to first check if the entity has a capacity component before some actions are performed. If the entity does not, it is assumed it is always safe to perform the actions. If the entity does have the component, the program first checks whether the maximum capacity has not been reached. If it is not reached, the actions can be safely performed. Of course, we could also set up our simulation such that every entity that is used in this piece of code has a capacity component. In that case we don't have to use HasComponent and just get the component without checking. However, be sure this is true, otherwise the simulation will crash.

Memory layout

Data components of the same type are efficiently stored contiguously next to each other by ERS. This allows views to efficiently iterate over them. However, it also means that adding more components of the same type may cause ERS to move around the memory locations of the components if the current location does not allow the components to stay contiguous. For this reason, references to data components should be used carefully: do not store them, only use local variables that are discarded after any operations on the data component(s) are done.

Script Behaviors

A script behavior component can hold data, just like a data component, but it can also define behavior. The data it can hold is also not limited to by-value types: it can hold strings, pointers, references, arrays and lists.

The behavior you can define in a script behavior is tied to callback functions that are triggered when certain things happen to the entity the script behavior is attached to (the ConnectedEntity, which can be accessed within the script behavior). For example, OnEntered will be called after the connected entity has received a new child entity.

#include "Ers/SubModel/ScriptBehaviorComponent.h"

class MyScriptBehavior : public Ers::ScriptBehaviorComponent
{
    uint64_t ReceivedEntities = 0;

    void OnEntered(EntityID newChild) override
    {
        Ers::Logger::Info(std::format("Received entity {}", newChild));
        ReceivedEntities++;
    }
};
using Ers;

using Entity = UInt64;

public class MyScriptBehavior : ScriptBehaviorComponent
{
    ulong ReceivedEntities = 0;

    override void OnEntered(Entity newChild)
    {
        Logger.Info($"Received entity {newChild}");
        ReceivedEntities++;
    }
}

The following callback functions are available on script behaviors:

Name Parameters Description Intended use
OnCreation - Called just after the component is created. Create local variables
OnDestroy - Called once just before the component is destroyed. Unregister callbacks / destroy local variables
OnAwake - Called once just before OnStart. Resolve entities and immediate initialization of member variables
OnStart - Called once the submodel has started. Initialize distant entity dependent member variables
OnEntering newParent Called before the ConnectedEntity gains a new parent.
OnEntered newChild Called after a new child is added to ConnectedEntity.
OnExiting newParent Called before the ConnectedEntity leaves a parent.
OnExited newChild Called after a child leaves the ConnectedEntity.
OnInputChannelReady inputChannel Called after an input channel registered to the ConnectedEntity is ready to send.
OnOutputChannelReady outputChannel Called after an output channel registered to the ConnectedEntity is ready to receive.
OnRecieve inputChannel, child Called after a foreign output channel sends a child to the ConnectedEntity.
OnUpdate - Called during time step in agent-based, called when observing model in continuous Agent-based: Progression of time step.
Continuous: Progress time in the solver.
OnLateUpdate - Called after OnUpdate is called on all script behaviors Agent-based: Statistics, temporally dependent modifications on time step progression.
Continuous: Statistics, scheduling sync-events.
Serialization node Called during a saving or loading operation to save or restore the state of the component Save or load all fields on a script behavior. All data must flatten, before being written during a save operation. During a load operation read flat data, use OnAwake & OnStart to relink the model, these will be called later in the normal order.
OnSubModelMove newConnectedEntity Called when this script behavior is moved between submodels to replace Connected Entity Automatically implemented by binding. Ensures ConnectedEntity is always valid and the correct. entity.

Built-in components

The following components are built into ERS.

  • NameComponent: Gives entities a name.
  • TransformComponent: Gives entities a position, rotation and scale.
  • RelationComponent: Defines relations between entities, such as parent, child and sibling.
  • RenderComponent: Give an entity a shape, and color or texture to visualize it.
  • BoxComponent: Get or set the bounding box of an entity.
  • PathComponent: Define paths along which entities can be rendered and animated.
  • ChannelComponent: Allows entities to have structured data flow.
  • ResourceComponent: Allows entities to work with channels.

These components are automatically registered. Some components, like the NameComponent and RelationComponent are automatically added to entities when they have a name, or a parent / children.

C# tip

In C#, the name of an entity can be easily acquired using the GetName function. This function can be called directly on an entity (ulong / UInt64, see GlobalUsings).

Registering components

Custom (user-defined) components can be registered to ERS via the component registry. When a component is registered to ERS, its existence along with certain metadata is stored by ERS. Some of this metadata is automatically acquired and used to efficiently store the component in the ECS. Other metadata can be optionally added to enable certain features, such as serialization.

Because certain actions (like saving and loading using serialization) require certain metadata about the components types, all component types should be registered before the creation of any model container.

Components can be registered using the component registry:

struct CustomComponent : public Ers::DataComponent
{
    int Value1;
    float Value2;
    bool Value3;
};

Ers::ComponentRegistry<CustomComponent>::Register();
[StructLayout(LayoutKind.Sequential)]
struct CustomComponent : IDataComponent
{
    int Value1;
    float Value2;
    bool Value3;
}

ComponentRegistry<CustomComponent>.Register();

Views

Views are the systems of the ECS. They allow you to iterate over all entities which have certain components attached to them. Optionally, you can also specify which components should not be attached to them. When iterating over these entities, ERS makes sure the components can be efficiently iterated over to improve performance.

// Iterate over all entities that have both a Capacity and Transform component
auto view1 = Ers::Get().GetView<Capacity, Ers::TransformComponent>();
while (view1.Next())
{
    auto [entity, capacity, transform] = view.Get();
    // Use the entity and components here
}

// Iterate over all entities with a Capacity component, but not a Transform component
auto view2 = Ers::Get().GetView<Capacity>(Ers::ExcludedTypes<Ers::TransformComponent>{});
while (view2.Next())
{
    auto [entity, capacity] = view.Get();
    // Use the entity and component here
}
using Entity = UInt64;

// Iterate over all entities that have both a Capacity and Transform component
var view1 = SubModel.Get().GetView<Capacity, TransformComponent>();
while (view1.Next())
{
    Entity entity = view.GetEntity();
    Ref<Capacity> capacity = view.GetComponent<Capacity>();
    var transform = view.GetComponent<TransformComponent>();
    // Use the entity and components there
}
view1.Dispose() // C# views need to be manually freed

// Iterate over all entities with a Capacity component, but not a Transform component
var view2 = SubModel.Get().GetView<Capacity>(ComponentList.GetTypeArray<TransformComponent>());
while (view2.Next())
{
    Entity entity = view.GetEntity();
    Ref<Capacity> capacity = view.GetComponent<Capacity>();
    // Use the entity and component here
}
view2.Dispose(); // C# views need to be manually freed

As you can see, you first create a view and specify the included- and excluded components. After creating the view, you can iterate over all entities by called Next on the view. In the example, Next is called in a loop, but this is not required. Each time Next is called, the view advances and updates the current entity and components. Inside the loop, we access the currently iterated entity by destructuring in C++ or with GetEntity in C#. The components can be read or modified inside the loop.

Info

Views also support iteration, meaning you can use them in a range-for loop.

auto view = subModel.GetView<MyComponent>();
for (auto [entity, MyComponent] : view)
{
    // Do something with your entity and component(s)
}

Views also support enumeration, meaning you can use them in a foreach loop.

var view = subModel.GetView<MyComponent>([]);
foreach (Entity entity in view)
{
    // Do something with your entity
    // To get one of the components:
    var component = view.GetComponent<MyComponent>();
}
Was this page helpful?