Links

EnTT

Introduction

There are two ways of integrating an entity component system with an Simulate simulation:
This page will only discuss EnTT, if you want to use the Simulate native ECS in your simulation, there is a separate guide . We support all of the functionality provided by the EnTT ECS (that is covered by the basic_registry class. The experience of using the Simulate EnTT API should feel familiar if one has previously tried the EnTT API as the methods are readily available in the aether::entt namespace.

Walkthrough

Defining Components

In user_definitions.hh the components which group together data are defined. These component types are then grouped in to a tuple. This tuple is referred as a collection. In the following example two components are defined, the first one contains data that is going to be used in all entities. The second component contains the data necessary for basic physics to be applied.
struct component_common {
uint64_t id;
colour_t colour;
};
struct component_physics {
vec3f position;
vec3f velocity;
};
AETHER_SERDE_DERIVE_TRIVIAL(component_common)
AETHER_SERDE_DERIVE_TRIVIAL(component_physics)
typedef std::tuple<
component_common,
component_physics> component_types;
As entity data moves across machines in Simulate, it is necessary to convert components to and from a format that can be sent across machines. Components that are “trivial” (i.e. are valid to copy using memcpy) can have serialization and deserialization routines defined for them using the macro AETHER_SERDE_DERIVE_TRIVIAL. For more complex types, you can provide custom serialisation functions.

Local entities and ghosts

Every worker cell in Simulate has its own EnTT registry. The ghost entities in a worker have the aether::entt::component::ghost component, while the local ones don’t. The methods num_agents_ghost and num_agents_local from the entt_store return the number of ghosts and the number of local entities in that specific registry (associated with the worker).
It is vital that you only change the local entities of a worker. If you try changing the ghost entities, the behaviour is undefined and we strongly advise against this.
Thus, when manipulating the entities of a registry or doing views or groups, remember to exclude the ghost entities registry.view<agent_common, agent_move>(entt::exclude<aether::entt::component::ghost>); or use local_iterator_type iterate_local(); from the store.

Declaring the EnTT ECS

In simulate.hh, the EnTT store is defined to be used as the state of a simulation cell. It must include octree_traits (see Overview) and the handover type.
#include <aether/entt.hh>
#include <aether/entt/serialization.hh>
#include <entt/entt.hpp>
struct entt_store_traits {
using octree_traits = octree_traits;
using handover_type = aether::default_handover;
using store_type = entt_store;
template<typename Writer>
using serializer_type = agent_serializer<Writer>;
template<typename Reader>
using deserializer_type = agent_deserializer<Reader>;
};
class user_cell_state_impl : public aether::entity_based_user_state_base<entt_store_traits>
{
}

Using the EnTT ECS

Entities are created in simulate.cc. In the following example, the initialise_world function (see Simulation/General under technical documentation) is used to create some entities at random positions in the world at the start of the simulation, but they can be created at other points such as in receive_messages or cell_tick as part of game logic.
void user_cell_state_impl::initialise_world(
const aether_state_type &aether_state
) {
// Get the entt store and the registry from the aether wrapper around entt
entt_store &store = state.get_store();
aether::entt::registry_type &registry = store.registry;
// Creating 10 new entities
for (size_t i = 0; i < 10; ++i) {
// Create a new entity using the usual EnTT syntax
entt::entity entity = registry.create();
// Add components using the usual EnTT syntax
registry.emplace<agent_move>(
entity,
aether::vec2f { rnd_position(gen), rnd_position(gen) },
aether::vec2f { 0.0f, 0.0f }
);
registry.emplace<agent_common>(
entity,
i + 1,
species_color(species),
0,
ENTITY_SIZE
);
}
}
To delete the entities from the simulation, one can create a system for it (this particular one removes all entities that have an agent_common component, excluding the ones that have an agent_playercomponent):
struct remove_entities_test_system {
void operator()(
const aether::cell_state<octree_traits>& aether_state,
user_cell_state_impl& state,
float delta_time
) {
auto &registry = state.get_store().registry;
auto num_players = registry.size<agent_player>();
// Delete all entities
registry.view<agent_common>(entt::exclude<agent_player>)
.each([&registry](const auto entity, auto && comm) {
registry.destroy(entity);
});
}
}
And call it every tick with: remove_entities_test_system{}(aether_state, *this, delta_time); in the cell_tick method.
Hopefully, as you noticed, the familiar EnTT API through which you can do views, groups and usual entity/component manipulation is easily accessible through the registry associated with the current worker cell. Systems thus have no special syntax needed, they aren’t registered, but we recommend calling them from cell_tick, in a similar fashion to the () operator of remove_entities_test_system.

Serialisation

AETHER_SERDE_DERIVE_TRIVIAL can still be used for simple components. For custom component serialisation, refer to Custom Component Serialisation. If serde is not suitable, a custom implementation of the aether::entt::component_serialization_context and aether::entt::component_deserialization_context interfaces can be used instead.
All the methods available to Simulate's version of EnTT ECS (v3.5.2) (the ones from registry.hpp -- https://github.com/skypjack/entt/blob/master/src/entt/entity/registry.hpp ) are available through the aether::entt::registry_type in Simulate. It is important to use the registry associated with the worker when modifying any entity, component or state.
Therefore, creating, modifying, deleting entities and everything in-between is done via the EnTT API described here https://github.com/skypjack/entt/wiki/Crash-Course:-entity-component-system and here https://skypjack.github.io/entt/ in detail. This includes support for the registry, views, groups, sinks etc. (described in the links provided, in regards to ECS-only functionality).
Because there are multiple registries in a simulation (one for each worker) and entities move between them, users must take special care when designing signals - in particular, taking into account the behaviour of ghosts.
We don't guarantee that EnTT IDs are consistent between cells (Actually, we expect them to be different). Instead, we guarantee that if the user introduces an unique ID (or a big enough random) as a component on an entity, that component will remain the same between cells. If a simulation wide unique ID is needed, that should be stored in a component.