Skip to content

Entity Component System (Ecs)

For more information about the ECS architecture, see:

Registry

The Viewer uses a Registry (alias for entt::registry) to store all entities and their data. To manipulate entities and their components directly, use the object:

auto & registry = viewer.registry();
Viewer class exposes API that simplifies interaction with the Registry, e.g. Viewer::show_mesh.

Entity

Unique identifier - it's just that. It's used to identify a unique "object" or "entity". Lagrange UI defines a Entity alias. Internally implemented as std::uint32_t.

To create a new entity, use:

Entity new_entity = registry.create();

To destroy:

registry.destroy(entity);

Components

Any data that is attached to an Entity. Uniquely identified by template typename <T> and Entity.

Components don't have logic, that means no code. They only store data and implicitly define behavior. Ideally, the components should be structs with no functions. However, it may be beneficial to have setters/getters as member functions in some cases.

To attach a component of type MyComponent to an entity :

// When it doesn't exist
registry.emplace<MyComponent>(entity, MyComponent(42))

// When it might exist already
registry.emplace_or_replace<MyComponent>(entity, MyComponent(42))

To retrieve a component:

// If it exists already
MyComponent & c = registry.get<MyComponent>(entity);

// If you're not sure it exists
MyComponent * c = registry.try_get<MyComponent>(entity);
//or
if(registry.has<MyComponent>()){
    MyComponent& c = registry.get<MyComponent>(entity);
}

Tag Components

"Empty" components may be used to tag entities, e.g. Selected, Hovered, etc. These types however must have non-zero size:

struct Hidden {
    bool dummy;
}

Systems

Systems are the logic of the application. They are defined as functions that iterate over entities that have specified components only. For example, running this system:

registry.view<Velocity, Position>().each([](Entity e, Velocity & velocity, Transform & transform){
    transform.local = Eigen::Translation3f(velocity) * transform.local;
});
will iterate over all entities that have both Velocity and Transform and apply the velocity vector to the transform.

Lagrange UI defines System as alias to std::function<void(Registry&)>, that is, a function that does something with the Registry. Typically these will be defined as:

System my_system = [](Registry &w){
    w.view<Component1, Component2, ...>.each([](Entity e, Component1 & c1, Component2 & c2, ...){
        //
    });
};

Context Variables

Systems do not have data. However, it's often useful to have some state associated with a given system, e.g. for caching. Sometimes it's useful that this state be shared among several systems. Instead of storing this state in some single instance of a component, we can use context variables. These can be thought of as singleton components - only one instance of a Type can exist at a given time.

InputState is such a singleton component. At the beginning of the frame, it is filled with key/mouse information, including last mouse position, mouse delta, active keybinds, etc.:

void update_input_system(Registry & registry){
    InputState & input_state =  registry.ctx_or_set<InputState>();
    input_state.mouse_pos = ...
    input_state.mouse_delta = ...
    input_state.keybinds.update(...);
}

It can then be used by any other system down the line:

void print_mouse_position(Registry & registry){
    const auto & input_state = registry.ctx<InputState>();

    lagrange::logger().info("Mouse position: {}", input_state.mouse_pos);
}

Design Considerations

Rules to follow when designing components and systems:

  • Components have no functions, only data
  • Systems have no data
  • State associated with systems is stored as context variable (registry.ctx<T>())