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.