Component operators
When retrieving entities' components, until now we've seen how we can access them for reading or writing using:
for (pos, vel) in entities.iter_for::<(Write<Position>, Read<Velocity>)>(){
}
Or similar syntaxes when using storages or foreach systems. In any case Read
and Write
are what we call in rin component operators, they allow us to retrieve components that follow certain conditions.
In the previous example we where retrieving all the entities that have both Position
and Velocity
, but there's other relations, conditions, that we can express with operators in order to select a certain set of entities and their components.
One of the paradigms often mentioned in ECS literature to explain ECS is SQL the old database language that allow to query data by expressing the columns and conditions between those columns of data. Rin tries to adapt some of those ideas by using operators which allow to express certain basic relations between the data in order to give more powerful tools to query entities and their components.
Let's see what are the available operators and how they can be used. Most of this chapter comes directly from the operators documentation but it's here as a way to make it more accessible and to point out the importance of this concept when working with rin.
Entity
This is one of the simples iterators. As we explained in the first chapters, in ECS the entities, are nothing more than a number, an ID that is related to several components. Although most of the time knowing about that number is not necesary, sometimes we might want to use it.
For example in a creation system we could want to delete every entity that match certain conditions, let's say everything that has a component Age
that is more than 100. Since we can't iterate over components and remove entities at the same time, in this case we need to store the entities and then later use them to delete them:
#[derive(Clone, Copy, Debug, Serialize, Deserialize, Component)]
struct Age(usize);
fn remove_old(mut entities: CreationProxy, resources: Resources) {
let to_delete = entities.iter_for::<(Entity, Read<Age>)>()
.filter_map(|(entity, age)| if age.0 > 100 {
Some(entity)
}else{
None
}).collect::<Vec<Entity>>();
for entity in &to_delete {
entities.remove_entity(entity);
}
}
Other uses for retrieving an entity is querying individual components or use them as a filter for another query...
Read / Write
Read
and Write
are the most common operators they matches all entities that contain this component for read only access in the case of Read
for pos in entities.iter_for::<Read<Position>>(){
//...
}
or for read/write access in the case of Write
for pos in entities.iter_for::<Write<Position>>(){
//...
}
Both examples will iterate over all entities that have a position component and return a reference to the position in the case of Read
and a mutable reference to the position in the case of Write
.
Has
Has
is an operator that matches every entity that has a specific component without locking it's storage
When using the Has
operator we won't have access to the actual component data, it just selects an entity that has that component. The selected data will have a () element in it's position.
The main difference with just using Read
is that it's a bit faster but also it can be used simultaneously with a system writing to the same component. So a system using Read
and another using Write
to the same component won't run in parallel but a system using Has
and another using Write
can run simultaneously.
For example:
for (pos, _) in entities.iter_for::<(Read<Position>, Has<Velocity>)>(){
//...
}
Will select all entities that have Position and Velocity but we can't access the velocity, the second element in the returned tuple is just a (), an empty type of data in Rust.
ReadOption / WriteOption
ReadOption
and WriteOption
match every entity no matter if it has the component or not. ReadOption
will then return an Option
of a reference to the component, and WriteOption
an Option
of a mutable reference.
The returned value will be None
if an entity doesn't have that element. For example:
for (pos, vel) in entities.iter_for::<(ReadOption<Position>, Read<Velocity>)>(){
if let Some(pos) = pos {
// ...
}
}
Will select all entities that optionally have Position
and have a Velocity
component. pos
will be None
for those entities that don't have Position
.
HasOption
HasOption
works similarly to ReadOption
by matching every entity no matter if it has the component or not but instead of returning an Option with a reference to the value it returns a boolean that is true or false depending if the entity contains that component or not.
for (has_pos, vel) in entities.iter_for::<(HasOption<Position>, Read<Velocity>)>(){
if has_pos {
// ...
}
}
Will select all entities that optionally have Position
and have a Velocity
component. pos
will be false for those that don't have Position
.
Same as with Has
HasOption
is more efficient than ReadOption
and doesn't block the storage so it can be used simultaneously with systems that write to the same component.
Not
Not
matches every entity that doesn't have this component. It doesn't make sense unless it's used in combination with some other operator since it returns an emoty ().
for (pos, _) in entities.iter_for::<(Read<Position>, Not<Velocity>)>(){
//...
}
Will select all entities that have Position but no Velocity
ReadNot
ReadNot
takes two component parameters instead of one and match every entity that has the first component and not the second.
It's equivalent to Read + Not but more convenient cause it doesn't return an extra empty component
for pos in entities.iter_for::<ReadNot<Position, Velocity>>(){
//...
}
Will select all entities that have Position but no Velocity
ReadOr
ReadOr
takes a tuple of components and matches any entity that has at least one of them. It returns a tuple of options of the selected components with the ones that the current entity has.
For example:
for (pos, vel) in entities.iter_for::<ReadOr<(Position, Velocity)>>(){
if let Some(pos) = pos {
}
if let Some(vel) = vel {
}
}
Matches all entities that have a Position
a Velocity
or both. pos
and vel
will be None for entities that don't have Position
or Velocity
respectively. At least one of them will be Some
HasOr
HasOr
works the same as ReadOr
it takes a tuple of components and matches any entity that has at least one of them but instead of returning a tuple of options of the components it just returns an tuple of booleans that are true or false depending if the current entity has that component or not.
for (has_pos, has_vel) in entities.iter_for::<HasOr<(Position, Velocity)>>(){
if has_pos {
}
if has_vel {
}
}
Matches all entities that have a Position
a Velocity
or both. pos
and vel
will be None
for entities that don't have Position
or Velocity
respectively.
Ref
In rin there's a special type of component called an NToOneComponent
that allows to reference other components. An NToOneComponent
express a relation from N entities to one component. This for example allows to use the same geometry from different entities.
To use it we need to derive NToOneComponent
instead of Component
for reference components the we can use the Ref
operator which takes the reference component and the referenced one as parameters:
#[derive(Debug)]
struct Vertex;
#[derive(NToOneComponent, Debug)]
struct GeometryRef(Entity);
#[derive(Component, Debug)]
struct Geometry(Vec<Vertex>);
let geometry = scene.new_entity()
.add(Geometry(vec![Vertex, Vertex, Vertex]))
.build();
let a = scene.new_entity()
.add(GeometryRef(geometry))
.build();
let b = scene.new_entity()
.add(GeometryRef(geometry))
.build();
for geometry in entities.iter_for::<Ref<GeometryRef, Read<Geometry>>>() {
//...
}
The example creates three entities, the first one a geometry, the second and third two entities that instead of having a geometry directly as component, have a GeometryRef
which references the actual geometry. To then access the geometry we use Ref<GeometryRef, Read<Geometry>>
which returns a reference to the geometry.
When using Ref
it's important to take into account that we are referencing the same component from several entities so using Ref
along with Write
might not have the intended consequences given that we might be modifying the same component multiple times in the same loop.
ReadRef
Ref<A, B>
just an alias for Ref<A, Read<B>>
. The previous example could also be written as:
for geometry in entities.iter_for::<ReadRef<GeometryRef, Geometry>>() {
//...
}
OneToNComponent
To understand the next operator first we need to know about another kind of special component, OneToNComponent
This kind of component stores a slice of the type instead of one per entity. To use it we just need to derive it for the corresponding component instead of using the usual Component
.
When a component is of OneToNComponent
type then operators that allow access to the data (ie: not Has*) will return a SliceView
or SliceViewMut
instead of a reference or mutable reference to the component. A SliceView
or it's mutable version are just wrappers around a slice of values and can be used as a normal rust slice, iterated, indexed, etc.
For example:
#[derive(Clone, Copy, Debug, Serialize, Deserialize, OneToNComponent)]
struct Position(Pnt2);
for positions in entities.iter_for::<Read<Position>>(){
for position in positions {
}
}
Now the Read
operator returns a slice of positions instead of a reference to one.
RefN
RefN
only works with OneToNComponent
and is similar to Ref
but instead of returning a reference to a component it works with a OneToNComponent
that points to entities and returns a SliceView of the referenced components. In a way it provides an N to N relation.
#[derive(OneToNComponent, Clone, Copy, Serialize, Deserialize)]
pub struct GeometryRef(pub Entity);
for geometries in entities.iter_for::<RefN<GeometryRef, Read<Geometry>>>() {
for geometry in geometries {
}
}
ReadAndParent / WriteAndParent
ReadAndParent
and WriteAndParent
allow to acccess an entity's component and it's parent for hierarchical components, those that derive HierarchicalComponent
instead of the usual Component
. Those components have a hierarchical structure, they are stored as a tree and when adding new components of this type to an entity they can be marked as being the child of another entity.
#[derive(HierarchicalComponent, Debug, Serialize, Deserialize)]
struct Position{x: f32, y: f32}
let e1 = scene.new_entitty()
.add(Position(0., 0.))
.build();
scene.new_entity()
.add_child(&e1, Position(1., 1.))
.build();
for (pos, parent_pos) in entities.ordered_iter_for::<ReadAndParent<Position>>(){
}
In the previous example the second entity's Position
component is added as a child of e1. When iterating over the Position
components using ReadAndParent
we get a tuple with the Position
component for the current entity and it's parent position as an Option<&Position>
if the entity doesn't have a parent the second member of the tuple will be None
.
The ReadAndParent
operator has to be used with ordered_iter_for
instead of iter_for
and it goes through the entities in hierarchical order. That way we always go first through an entity and then to it's children. That is particularly important when writing since we usually want the value of the parent to be up to date when we visit the children.
When using ordered_iter_for
the order always comes from the first operator if there's more than one.
Although more than one component can be hierarchical in a scene and even on the same entity it's not very usual.
Rin's rin::graphics::Node
for example, is a hierarchical component that represents a 3D hierarchy of objects where parents also transform their children. The global position, orientation, scale and transformation of an entity can be accessed from it's Node
but their values can be outdated unless the system depends on Node
as needs
. Then it will run after the system that updates those values in a hierarchical fashion. As a user most of the time you won't need to access the hierarchy of nodes but the fact they are stored as a tree is what allows rin to easily and efficiently keep their global components updated.