-
Notifications
You must be signed in to change notification settings - Fork 32
ECS
The application makes use of an Entity Component System (ECS) to manage the objects within the scene. In an ECS, every object is represented as an entity. Every entity consists of zero or more components, which contain state or data. Components can be added, deleted and mutated at runtime.
The registry is our implementation of an ECS. In the registry, entities are represented as numbers according to the corresponding registry traits
. Registry traits are identified with a unique template parameter and contain information on the type of number used to representent entities (e.g. long
or char
). The registry is desiged to be the one and only interface to interact with entities. Therefore only the registry can create and destroy entities, only the registry can add, remove and get components.
Registry traits contain three types:
-
representation_type
: This represents the type of an entity as it is being stored in the registry. The user should not work with this type. Therepresentation_type
should be double the size of theentity_type
, because the first part will be used to store the parent of the entity, and the second part is used for the unique identifier of the entity. -
entity_type
: This represents the type of an entity and is used for the unique identifier of every entity. The size of this type limits the amount of possible entities in the registry. -
components_type
: Like entities, every component type has a unique identifier of this type. The size of this type limits the amount of possible component types in the registry. Registry traits contain two fields: -
entity_mask
: This field is used to mask out the parent or the entity from arepresentation_type
. Its value is anentity_type
with all bits set to one. -
parent_shift
: This field is used to binary shift arepresentation_type
until only the parent remains. Its value is always the amount of bits of the ´entity_type´.
Right now there are three types of registries defined, of which only Registry64
is used in the applications.
typedef Registry<std::uint16_t> Registry16
typedef Registry<std::uint32_t> Registry32
typedef Registry<std::uint64_t> Registry64
Registries can be created as follows:
// Creates an empty registry with registry_traits<std::uint64_t>
Registry<std::uint64_t> registry;
// Or you can use the predefined alias
Registry64 registry;
The registry is used to create and destroy entities:
// Creates an entity with zero components and no parent. Returns the identifier of the created entity.
auto id = registry.create();
// Destroys the entity and releases its identifier to be reused in the future.
registry.destroy(entity);
You can also supply a parent entity when creating a new entity:
auto parent = registry.create();
auto child = registry.create(parent);
The registry has a parenting system built in. Internal, entities are stored as a number containing both the parent and the entity identifier. This way, entities have direct access to their parent. The following functions are used for this purpose, they are available for both entity_type
and representation_type
entities:
// Returns the parent of the entity.
auto parent = registry.getParent(entity);
// Returns the entity identifier of the entity.
// In case of an `entity_type`, this function is the identity function.
auto self = registry.getSelf(entity);
// Sets the parent of the entity to the given identifier. Returns whether the action was successful.
bool success = registry.setParent(entity, parent);
// This returns an iterator which iterates over all entities,
// and skips every entity whose parent is not equal to the entity.
auto iterator = registry.getChildren(entity);
Components are used to add state or behaviour to entities. Every component has a unique identifier of type components_type
within the registry. Every component needs to extend the RefCountable
struct. This is because instead of storing a raw component pointer, the registry stores an intrusive_ptr
. This behaves the same as a shared_ptr
but avoids the necessity of storing (and copying or moving) the reference count in the shared_ptr
object. The intrusive_ptr
is also alias Ref
for simplicity.
Components are added using functions similar to emplace_back
of std::vector
. Removal of components only happens if the reference count reaches zero.
struct Name : RefCountable {
std::string name;
Name(const std::string& name) : name(name) {}
}
// Creates a new component of type `Name` using the supplied parameters and adds it to the entity
Ref<Name> component = registry.add<Name>(entity, "Leonardo da Vinci");
// Removes the component of type `Name` from the given entity, returns whether the erasure was successful.
bool removed = registry.remove<Name>(entity);
There are multiple ways to get components, this are ways to get a single component:
// Gets a `Ref` of the `Name` component of the entity.
Ref<Name> name = registry.get<Name>(entity);
// Gets a `Ref` of the `Name` component of the entity.
// If no such component is found, a new one is created and returned.
Ref<Name> name = registy.getOrAdd<Name>(entity, "Vincent van Gogh");
// Gets a **copy** of the `Name` component of the entity.
// If no such component is found, the default value is returned instead.
Name name = registry.getOr<Name>(entity, "Che Guevara");
Every component has a unique identifier which is used to internally index datastructures. The following functions are used to get this index and the name of the component corresponding to that index
// Returns the index of the `Name` component.
component_type index = registry.getComponentIndex<Name>();
// Returns a string view of the name of the given component
std::string_view name = registry.getComponentName(index);
// Or equivalent
std::string_view name = registry.getComponentName<Name>();
Component identifiers are generated in a first-come first-served fashion. By using the init
function, one can specify a desired order. Any new components will get their identifier as usual.
// Internally, the component index for `Name` will be 0, the one for `Material` will be 1, and so on.
registry.init<Name, Material, Mesh>();
Some general purpose functions.
// Returns whether the given entity exists in the registry.
bool exists = registry.contains(entity);
// Returns whether the given entity has a `Name` component.
bool hasName = registry.has<Name>(entity);
Views are iterators over entities or components. Views can contain filters, transformations or just be plain iterators. The previously mentioned getChildren
method actually returns a view. The most general view iterates over all entities in the registry:
for (auto entity : registry)
...
// This filter taken an iterator of the datatype you want to iterate over, in this case the set of entities.
// This filter only allows entities whose parent is not equal to 0, ie. every entity who has a parent.
auto parentFilter = [®istry] (const Registry64::entity_set_iterator& iterator) {
return registry.getParent(*iterator) != 0;
};
// A view iterator over the non-leaf entities.
auto view = registry.filter(parentFilter);
// This transform is called when the view iterator is dereferenced and transforms the container iterator
// into a type of your choice. This transform returns the parent of the entity you are iterating over.
auto parentTransform = [®istry] (const Registry64::entity_set_iterator& iterator) {
return registry.getParent(*iterator);
};
// Iterates over every entity and returns its parent when dereferencing the iterator.
auto view = registry.transform(parentTransform);
auto parent = *view.begin();
// These two concepts can also be used together. This view iterates over every entity who is a parent.
auto view = registry.filter_transform(parentFilter, parentTransform);
The logic of these types of view are already implemented in the registry.
// This view iterates over all entities containing both a `Name`, `Material` and a `Mesh` component.
auto entities = registry.view<Name, Material, Mesh>(entity);
This is actually just syntactic sugar for the following syntax.
auto entities = registry.view<conjunction<Name, Material, Mesh>>();
Here we call conjunction
the view type. The reason for this is that there will be more complex view types in the future like disjunction
or exclusive
. Every view type has a dedicated get
function which is optimized for that view type. For example, for the conjunction
view type, it is already known that the currently iterated entity already exists, and that the entity has every component in the view type. Therefore we can skip safety checks resulting in double the performance.
auto entities = registry.view<Name, Material, Mesh>();
for (auto e : entities)
Ref<Name> name = view.get<Name>(e);
Other useful views:
// Like previously mentioned:
auto children = registry.getChildren(entity);
// This view iterates over all components of a given entity.
// Dereferencing the iterator results in a pair containing the component index and a `Ref` to a `RefCountable`.
// Casting from a `Ref<RefCountable>`to the desired component is done using an `intrusive_cast`.
auto componentIterator = getComponents(entity);