Joining components

In the last chapter, we learned how to access resources using SystemData. To access our components with it, we can just request a ReadStorage and use Storage::get to retrieve the component associated to an entity. This works quite well if you want to access a single component, but what if you want to iterate over many components? Maybe some of them are required, others might be optional and maybe there is even a need to exclude some components? If we wanted to do that using only Storage::get, the code would become very ugly. So instead we worked out a way to conveniently specify that. This concept is known as "joining".

Basic joining

We've already seen some basic examples of joining in the last chapters, for example we saw how to join over two storages:

for (pos, vel) in (&mut pos_storage, &vel_storage).join() {
    *pos += *vel;
}

This simply iterates over the position and velocity components of all entities that have both these components. That means all the specified components are required.

Sometimes, we want not only get the components of entities, but also the entity value themselves. To do that, we can simply join over &EntitiesRes.

for (ent, pos, vel) in (&*entities, &mut pos_storage, &vel_storage).join() {
    println!("Processing entity: {:?}", ent);
    *pos += *vel;
}

The returned entity value can also be used to get a component from a storage as usual.

Optional components

The previous example will iterate over all entities that have all the components we need, but what if we want to iterate over an entity whether it has a component or not?

To do that, we can wrap the Storage with maybe(): it wraps the Storage in a MaybeJoin struct which, rather than returning a component directly, returns None if the component is missing and Some(T) if it's there.

for (pos, vel, mass) in 
    (&mut pos_storage, &vel_storage, (&mut mass_storage).maybe()).join() {
    println!("Processing entity: {:?}", ent);
    *pos += *vel;

    if let Some(mass) = mass {
        let x = *vel / 300_000_000.0;
        let y = 1 - x * x;
        let y = y.sqrt();
        mass.current = mass.constant / y;
    }
}

In this example we iterate over all entities with a position and a velocity and perform the calculation for the new position as usual. However, in case the entity has a mass, we also calculate the current mass based on the velocity. Thus, mass is an optional component here.

WARNING: Do not have a join of only MaybeJoins. Otherwise the join will iterate over every single index of the bitset. If you want a join with all MaybeJoins, add an EntitiesRes to the join as well to bound the join to all entities that are alive.

Manually fetching components with Storage::get()

Even though join()ing over maybe() should be preferred because it can optimize how entities are iterated, it's always possible to fetch a component manually using Storage::get() or Storage::get_mut(). For example, say that you want to damage a target entity every tick, but only if it has an Health:

for (target, damage) in (&target_storage, &damage_storage).join() {
  
    let target_health: Option<&mut Health> = health_storage.get_mut(target.ent);
    if let Some(target_health) = target_health {
      target_health.current -= damage.value;      
    }
}

Even though this is a somewhat contrived example, this is a common pattern when entities interact.

Excluding components

If you want to filter your selection by excluding all entities with a certain component type, you can use the not operator (!) on the respective component storage. Its return value is a unit (()).

for (ent, pos, vel, ()) in (
    &*entities,
    &mut pos_storage,
    &vel_storage,
    !&frozen_storage,
).join() {
    println!("Processing entity: {:?}", ent);
    *pos += *vel;
}

This will simply iterate over all entities that

  • have a position
  • have a velocity
  • do not have a Frozen component

How joining works

You can call join() on everything that implements the Join trait. The method call always returns an iterator. Join is implemented for

  • &ReadStorage / &WriteStorage (gives back a reference to the components)
  • &mut WriteStorage (gives back a mutable reference to the components)
  • &EntitiesRes (returns Entity values)
  • bitsets

We think the last point here is pretty interesting, because it allows for even more flexibility, as you will see in the next section.

Joining over bitsets

Specs is using hibitset, a library which provides layered bitsets (those were part of Specs once, but it was decided that a separate library could be useful for others).

These bitsets are used with the component storages to determine which entities the storage provides a component value for. Also, Entities is using bitsets, too. You can even create your own bitsets and add or remove entity ids:

use hibitset::{BitSet, BitSetLike};

let mut bitset = BitSet::new();
bitset.add(entity1.id());
bitset.add(entity2.id());

BitSets can be combined using the standard binary operators, &, | and ^. Additionally, you can negate them using !. This allows you to combine and filter components in multiple ways.


This chapter has been all about looping over components; but we can do more than sequential iteration! Let's look at some parallel code in the next chapter.