Links

Hadean Simuate ECS

Overview

Simulate provides a custom ECS that you can use in place of EnTT. This page describes what you need to know about using the Simulate ECS.
The Simulate ECS is now considered legacy, and should not be used on new projects. Instead, prefer the EnTT ECS.

Hadean Simulate ECS Requirements

Components are typically simple structs, and with many ECS implementations this is all you need to use them as components. Simulate additionally requires components to be serialisable. For simple structs that are trivially copyable, you can use the AETHER_SERDE_DERIVE_TRIVIAL macro to implement this serialisation for you. For non-trivial types, you must implement either serde_visit or both serde_load and serde_save. These methods allow you to fully control how components are serialised. See Custom Component Serialisation for more details on how to do this.

Walkthrough

Defining Components

In user_definitions.hh the components which group together data are defined. These component types are then grouped into a tuple, which is referred to as a collection. In the following example two components are defined:
  • The first contains data that is going to be used in all entities
  • The second contains the data necessary for basic physics to be applied
user_definitions.hh
// ...
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 serialisation and deserialisation routines defined for them using the macro AETHER_SERDE_DERIVE_TRIVIAL.

Declaring the ECS

In simulate.hh a new ECS type is defined to be used as the state of a simulation cell. It is templated on the octree traits and the component types tuple.
simulate.hh
#include <aether/demo/ecs/ecs.hh>
#include "user_definitions.hh"
// ...
using user_cell_state = aether::ecs<octree_traits, component_types>;
// ...
Entities are created in simulate.cc In the following example the initialise_world function is used to create 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.
simulate.cc
#include <aether/demo/ecs/ecs.hh>
#include "user_definitions.hh
// ...
void user_cell_state_impl::initialise_world(const aether_state_type &aether_state) {
auto &state = get_store();
// Create an update se in which to accumulate the new entity data. This
// works as an RAII structure, committing the accumulated changes to the
// state when the update instance is destroyed.
auto update = state.create_update_set();
// Create a random number generator
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> rnd_position(0.0, CELL_SIDE_LENGTH);
std::uniform_real_distribution<> rnd_velocity(-1.0, 1.0);
// We'll create 10 new entities
for (int i = 0; i < 10; i++) {
// Create new entity
auto entity = update.new_entity_local();
// Add the common data to the entity
auto common = entity.create_component<c_common>();
common ->id = generate_unique_id();
common->colour = colour(1.0, 0.0, 0.0);
// Add the physics data to the entity
auto physics = entity.create_component<c_physics>();
physics->position = {
rnd_position(),
rnd_position(),
rnd_position()
};
physics->velocity = {
rnd_velocity(),
rnd_velocity(),
rnd_velocity()
}
}
}
// ...
When an entity needs to be removed from the simulation it can be removed following the below example.
void user_cell_state_impl::remove_entity(uint64_t id_to_remove)
{
auto entities = state.local_entities<c_physics, c_common>();
auto update = state.create_update_set();
for (auto entity : entities) {
auto& id = entity.get<c_common>()->id;
if (id == id_to_remove){
update.drop_entity(entity);
}
}
}
Components can be added to an entity as shown in the example below
simulate.hh
void user_cell_state_impl::add_component(uint64_t id_to_add){
auto entities = state.local_entities<c_common>();
for (auto entity : entities) {
auto& id = entity.get<c_common>()->id;
if (id == id_to_add){
// Add the physics data to the entity
auto physics = entity.create_component<c_physics>();
physics->position = {
rnd_position(),
rnd_position(),
rnd_position()
};
physics->velocity = {
rnd_velocity(),
rnd_velocity(),
rnd_velocity()
};
}
}
}

Defining Systems

System functionality is implemented by defining a functor that will be called to update the data in the entities on every tick. In this example the physics component is updated given a constant gravitational force.
// ...
struct physics_update_system {
// The accessed_components type is used to indicate which components this system uses
using accessed_components = std::tuple<c_physics>;
// A constrained_ecs is then used to provide access to just the accessed components
using ecs_type = aether::constrained_ecs<user_cell_state, accessed_components>;
// Type aliases to aid readability
using aether_state_type = aether_cell_state<octree_traits>;
// The functor takes the cell state, ecs data and a time delta for the current tick
// as parameters
void operator()(
const aether_state_type& aether_state,
ecs_type& state,
float delta_time) {
// Iterate over all the entities and initialise acceleration to zero
for (auto entity: state.local_entities<c_physics>()) {
// Fetch the physics component for this entity
auto entity_physics = entity.get<c_physics>();
// Update the velocity given a gravitational force
entity_physics->velocity += vec3f(0.0, -9.8, 0.0) * delta_time;
// Update the position based on the new velocity
entity_physics->position += entity_physics->velocity * delta_time;
}
}
};
// ...
****
Systems declare the ECS components they will access using the accessed_components member type. When the ECS calls operator(), it will provide a constrained_ecs which has a similar interface, but only allows access to the specified components. Having systems declare the components they reference allows for implementations to run multiple systems in parallel (though Simulate ECS does not do this currently).
Systems are registered with the ECS in the initialise_cell function.
simulate.cc
// ...
void user_cell_state_impl::initialise_cell(const aether_state_type &aether_state) {
auto &state = get_store();
// ...
state.add_system<physics_update_system>();
// ...
}
// ...
The systems are then executed to update the state of all entities in the cell_tick function.
simulate.cc
// ...
void user_cell_state_impl::cell_tick(const aether_state_type &aether_state, float delta_time) {
auto &state = get_store();
// ...
state.tick(aether_state, delta_time);
// ...
}
// ...