Introduction

Welcome to The Specs Book, an introduction to ECS and the Specs API. This book is targeted at beginners; guiding you through all the difficulties of setting up, building, and structuring a game with an ECS.

Specs is an ECS library that allows parallel system execution, with both low overhead and high flexibility, different storage types and a type-level system data model. It is mainly used for games and simulations, where it allows to structure code using composition over inheritance.

Additional documentation is available on docs.rs:

You don't yet know what an ECS is all about? The next section is for you! In case you already know what an ECS is, just skip it.

What's an ECS?

The term ECS is a shorthand for Entity-component system. These are the three core concepts. Each entity is associated with some components. Those entities and components are processed by systems. This way, you have your data (components) completely separated from the behaviour (systems). An entity just logically groups components; so a Velocity component can be applied to the Position component of the same entity.

ECS is sometimes seen as a counterpart to Object-Oriented Programming. I wouldn't say that's one hundred percent true, but let me give you some comparisons.

In OOP, your player might look like this (I've used Java for the example):

public class Player extends Character {
    private final Transform transform;
    private final Inventory inventory;
}

There are several limitations here:

  • There is either no multiple inheritance or it brings other problems with it, like the diamond problem; moreover, you have to think about "is the player a collider or does it have a collider?"
  • You cannot easily extend the player with modding; all the attributes are hardcoded.
  • Imagine you want to add a NPC, which looks like this:
public class Npc extends Character {
    private final Transform transform;
    private final Inventory inventory;
    private final boolean isFriendly;
}

Now you have stuff duplicated; you would have to write mostly identical code for your player and the NPC, even though e.g. they both share a transform.

Entity-component relationship

This is where ECS comes into play: Components are associated with entities; you can just insert components, whenever you like. One entity may or may not have a certain component. You can see an Entity as an ID into component tables, as illustrated in the diagram below. We could theoretically store all the components together with the entity, but that would be very inefficient; you'll see how these tables work in chapter 5.

This is how an Entity is implemented; it's just

struct Entity(u32, Generation);

where the first field is the id and the second one is the generation, used to check if the entity has been deleted.

Here's another illustration of the relationship between components and entities. Force, Mass and Velocity are all components here.

Component tables

Entity 1 has each of those components, Entity 2 only a Force, etc.

Now we're only missing the last character in ECS - the "S" for System. Whereas components and entities are purely data, systems contain all the logic of your application. A system typically iterates over all entities that fulfill specific constraints, like "has both a force and a mass". Based on this data a system will execute code, e.g. produce a velocity out of the force and the mass. This is the additional advantage I wanted to point out with the Player / Npc example; in an ECS, you can simply add new attributes to entities and that's also how you define behaviour in Specs (this is called data-driven programming).

System flow

By simply adding a force to an entity that has a mass, you can make it move, because a Velocity will be produced for it.

Where to use an ECS?

In case you were looking for a general-purpose library for doing things the data-oriented way, I have to disappoint you; there are none. ECS libraries are best-suited for creating games or simulations, but they do not magically make your code more data-oriented.


Okay, now that you were given a rough overview, let's continue to Chapter 2 where we'll build our first actual application with Specs.

Hello, World!

Setting up

First of all, thanks for trying out specs. Let's set it up first. Add the following line to your Cargo.toml:

[dependencies]
specs = "0.14.0"

And add this to your crate root (main.rs or lib.rs):

extern crate specs;

Components

Let's start by creating some data:

use specs::{Component, VecStorage};

#[derive(Debug)]
struct Position {
    x: f32,
    y: f32,
}

impl Component for Position {
    type Storage = VecStorage<Self>;
}

#[derive(Debug)]
struct Velocity {
    x: f32,
    y: f32,
}

impl Component for Velocity {
    type Storage = VecStorage<Self>;
}

These will be our two component types. Optionally, the specs-derive crate provides a convenient custom #[derive] you can use to define component types more succinctly.

But first, you will need to add specs-derive to your crate

[dependencies]
specs = "0.14.0"
specs-derive = "0.4.0"

Now you can use this:

extern crate specs;
#[macro_use]
extern crate specs_derive;

use specs::{Component, VecStorage};

#[derive(Component, Debug)]
#[storage(VecStorage)]
struct Position {
    x: f32,
    y: f32,
}

#[derive(Component, Debug)]
#[storage(VecStorage)]
struct Velocity {
    x: f32,
    y: f32,
}

If the #[storage(...)] attribute is omitted, the given component will be stored in a DenseVecStorage by default. But for this example, we are explicitly asking for these components to be kept in a VecStorage instead (see the later storages chapter for more details). But before we move on, we need to create a world in which to store all of our components.

The World

use specs::{World, Builder};

let mut world = World::new();
world.register::<Position>();
world.register::<Velocity>();

This will create component storages for Positions and Velocitys.

let ball = world.create_entity().with(Position { x: 4.0, y: 7.0 }).build();

Now you have an Entity, associated with a position.

So far this is pretty boring. We just have some data, but we don't do anything with it. Let's change that!

The system

use specs::System;

struct HelloWorld;

impl<'a> System<'a> for HelloWorld {
    type SystemData = ();

    fn run(&mut self, data: Self::SystemData) {}
}

This is what a system looks like. Though it doesn't do anything (yet). Let's talk about this dummy implementation first. The SystemData is an associated type which specifies which components we need in order to run the system.

Let's see how we can read our Position components:

use specs::{ReadStorage, System};

struct HelloWorld;

impl<'a> System<'a> for HelloWorld {
    type SystemData = ReadStorage<'a, Position>;

    fn run(&mut self, position: Self::SystemData) {
        use specs::Join;

        for position in position.join() {
            println!("Hello, {:?}", &position);
        }
    }
}

Note that all components that a system accesses must be registered with world.register::<Component>() before that system is run, or you will get a panic. This will usually be done automatically during setup, but we'll come back to that in a later chapter.

There are many other types you can use as system data. Please see the System Data Chapter for more information.

Running the system

This just iterates through all the components and prints them. To execute the system, you can use RunNow like this:

use specs::RunNow;

let mut hello_world = HelloWorld;
hello_world.run_now(&world.res);
world.maintain();

The world.maintain() is not completely necessary here. Calling maintain should be done in general, however. If entities are created or deleted while a system is running, calling maintain will record the changes in its internal data structure.

Full example code

Here the complete example of this chapter:

use specs::{Builder, Component, ReadStorage, System, VecStorage, World, RunNow};

#[derive(Debug)]
struct Position {
    x: f32,
    y: f32,
}

impl Component for Position {
    type Storage = VecStorage<Self>;
}

#[derive(Debug)]
struct Velocity {
    x: f32,
    y: f32,
}

impl Component for Velocity {
    type Storage = VecStorage<Self>;
}

struct HelloWorld;

impl<'a> System<'a> for HelloWorld {
    type SystemData = ReadStorage<'a, Position>;

    fn run(&mut self, position: Self::SystemData) {
        use specs::Join;

        for position in position.join() {
            println!("Hello, {:?}", &position);
        }
    }
}

fn main() {
    let mut world = World::new();
    world.register::<Position>();
    world.register::<Velocity>();

    world.create_entity().with(Position { x: 4.0, y: 7.0 }).build();

    let mut hello_world = HelloWorld;
    hello_world.run_now(&world.res);
    world.maintain();
}

This was a pretty basic example so far. A key feature we haven't seen is the Dispatcher, which allows us to configure systems to run in parallel (and it offers some other nice features, too).

Let's see how that works in Chapter 3: Dispatcher.

Dispatcher

When to use a Dispatcher

The Dispatcher allows you to automatically parallelize system execution where possible, using the fork-join model to split up the work and merge the result at the end. It requires a bit more planning and may have a little bit more overhead, but it's pretty convenient, especially when you're building a big game where you don't want to do this manually.

Building a dispatcher

First of all, we have to build such a dispatcher.

use specs::DispatcherBuilder;

let mut dispatcher = DispatcherBuilder::new()
    .with(HelloWorld, "hello_world", &[])
    .build();

Let's see what this does. After creating the builder, we add a new

  1. system object (HelloWorld)
  2. with some name ("hello_world"")
  3. and no dependencies (&[]).

The name can be used to specify that system as a dependency of another one. But we don't have a second system yet.

Creating another system

struct UpdatePos;

impl<'a> System<'a> for UpdatePos {
    type SystemData = (ReadStorage<'a, Velocity>,
                       WriteStorage<'a, Position>);
}

Let's talk about the system data first. What you see here is a tuple, which we are using as our SystemData. In fact, SystemData is implemented for all tuples with up to 26 other types implementing SystemData in it.

Notice that ReadStorage and WriteStorage are implementors of SystemData themselves, that's why we could use the first one for our HelloWorld system without wrapping it in a tuple; for more information see the Chapter about system data.

To complete the implementation block, here's the run method:

    fn run(&mut self, (vel, mut pos): Self::SystemData) {
        use specs::Join;
        for (vel, pos) in (&vel, &mut pos).join() {
            pos.x += vel.x * 0.05;
            pos.y += vel.y * 0.05;
        }
    }

Now the .join() method also makes sense: it joins the two component storages, so that you either get no new element or a new element with both components, meaning that entities with only a Position, only a Velocity or none of them will be skipped. The 0.05 fakes the so called delta time which is the time needed for one frame. We have to hardcode it right now, because it's not a component (it's the same for every entity). The solution to this are Resources, see the next Chapter.

Adding a system with a dependency

Okay, we'll add two more systems after the HelloWorld system:

    .with(UpdatePos, "update_pos", &["hello_world"])
    .with(HelloWorld, "hello_updated", &["update_pos"])

The UpdatePos system now depends on the HelloWorld system and will only be executed after the dependency has finished. The final HelloWorld system prints the resulting updated positions.

Now to execute all the systems, just do

dispatcher.dispatch(&mut world.res);

Full example code

Here the code for this chapter:

use specs::{Builder, Component, DispatcherBuilder, ReadStorage,
            System, VecStorage, World, WriteStorage};

#[derive(Debug)]
struct Position {
    x: f32,
    y: f32,
}

impl Component for Position {
    type Storage = VecStorage<Self>;
}

#[derive(Debug)]
struct Velocity {
    x: f32,
    y: f32,
}

impl Component for Velocity {
    type Storage = VecStorage<Self>;
}

struct HelloWorld;

impl<'a> System<'a> for HelloWorld {
    type SystemData = ReadStorage<'a, Position>;

    fn run(&mut self, position: Self::SystemData) {
        use specs::Join;

        for position in position.join() {
            println!("Hello, {:?}", &position);
        }
    }
}

struct UpdatePos;

impl<'a> System<'a> for UpdatePos {
    type SystemData = (ReadStorage<'a, Velocity>,
                       WriteStorage<'a, Position>);

    fn run(&mut self, (vel, mut pos): Self::SystemData) {
        use specs::Join;
        for (vel, pos) in (&vel, &mut pos).join() {
            pos.x += vel.x * 0.05;
            pos.y += vel.y * 0.05;
        }
    }
}

fn main() {
    let mut world = World::new();
    world.register::<Position>();
    world.register::<Velocity>();

    // Only the second entity will get a position update,
    // because the first one does not have a velocity.
    world.create_entity().with(Position { x: 4.0, y: 7.0 }).build();
    world
        .create_entity()
        .with(Position { x: 2.0, y: 5.0 })
        .with(Velocity { x: 0.1, y: 0.2 })
        .build();

    let mut dispatcher = DispatcherBuilder::new()
        .with(HelloWorld, "hello_world", &[])
        .with(UpdatePos, "update_pos", &["hello_world"])
        .with(HelloWorld, "hello_updated", &["update_pos"])
        .build();

    dispatcher.dispatch(&mut world.res);
    world.maintain();
}

The next chapter will be a really short chapter about Resources, a way to share data between systems which only exist independent of entities (as opposed to 0..1 times per entity).

Resources

This (short) chapter will explain the concept of resources, data which is shared between systems.

First of all, when would you need resources? There's actually a great example in chapter 3, where we just faked the delta time when applying the velocity. Let's see how we can do this the right way.

#[derive(Default)]
struct DeltaTime(f32);

Note: In practice you may want to use std::time::Duration instead, because you shouldn't use f32s for durations in an actual game, because they're not precise enough.

Adding this resource to our world is pretty easy:

world.add_resource(DeltaTime(0.05)); // Let's use some start value

To update the delta time, just use

let mut delta = world.write_resource::<DeltaTime>();
*delta = DeltaTime(0.04);

Accessing resources from a system

As you might have guessed, there's a type implementing system data specifically for resources. It's called Read (or Write for write access).

So we can now rewrite our system:

use specs::{Read, ReadStorage, System, WriteStorage};

struct UpdatePos;

impl<'a> System<'a> for UpdatePos {
    type SystemData = (Read<'a, DeltaTime>,
                       ReadStorage<'a, Velocity>,
                       WriteStorage<'a, Position>);

    fn run(&mut self, data: Self::SystemData) {
        let (delta, vel, mut pos) = data;

        // `Read` implements `Deref`, so it
        // coerces to `&DeltaTime`.
        let delta = delta.0;

        for (vel, pos) in (&vel, &mut pos).join() {
            pos.x += vel.x * delta;
            pos.y += vel.y * delta;
        }
    }
}

Note that all resources that a system accesses must be registered with world.add_resource(resource) before that system is run, or you will get a panic. If the resource has a Default implementation, this step is usually done during setup, but again we will come back to this in a later chapter.

For more information on SystemData, see the system data chapter.

Default for resources

As we have learned in previous chapters, to fetch a Resource in our SystemData, we use Read or Write. However, there is one issue we have not mentioned yet, and that is the fact that Read and Write require Default to be implemented on the resource. This is because Specs will automatically try to add a Default version of a resource to the World during setup (we will come back to the setup stage in the next chapter). But how do we handle the case when we can't implement Default for our resource?

There are actually three ways of doing this:

  • Using a custom SetupHandler implementation, you can provide this in SystemData with Read<'a, Resource, TheSetupHandlerType>.
  • By replacing Read and Write with ReadExpect and WriteExpect, which will cause the first dispatch of the System to panic unless the resource has been added manually to World first.
  • By using Option<Read<'a, Resource>>, if the resource really is optional. Note that the order here is important, using Read<'a, Option<Resource>> will not result in the same behavior (it will try to fetch Option<Resource> from World, instead of doing an optional check if Resource exists).

In the next chapter, you will learn about the different storages and when to use which one.

Storages

Specs contains a bunch of different storages, all built and optimized for different use cases. But let's see some basics first.

Storage basics

What you specify in a component impl-block is an UnprotectedStorage. Each UnprotectedStorage exposes an unsafe getter which does not perform any checks whether the requested index for the component is valid (the id of an entity is the index of its component). To allow checking them and speeding up iteration, we have something called hierarchical bitsets, provided by hibitset.

Note: In case you don't know anything about bitsets, you can safely skip the following section about it. Just keep in mind that we have some mask which tracks for which entities a component exists.

How does it speed up the iteration? A hierarchical bitset is essentially a multi-layer bitset, where each upper layer "summarizes" multiple bits of the underlying layers. That means as soon as one of the underlying bits is 1, the upper one also becomes 1, so that we can skip a whole range of indices if an upper bit is 0 in that section. In case it's 1, we go down by one layer and perform the same steps again (it currently has 4 layers).

Storage overview

Here a list of the storages with a short description and a link to the corresponding heading.

Storage Type Description Optimized for
BTreeStorage Works with a BTreeMap no particular case
DenseVecStorage Uses a redirection table fairly often used components
HashMapStorage Uses a HashMap rare components
NullStorage Can flag entities doesn't depend on rarity
VecStorage Uses a sparse Vec commonly used components

BTreeStorage

It works using a BTreeMap and it's meant to be the default storage in case you're not sure which one to pick, because it fits all scenarios fairly well.

DenseVecStorage

This storage uses two Vecs, one containing the actual data and the other one which provides a mapping from the entity id to the index for the data vec (it's a redirection table). This is useful when your component is bigger than a usize because it consumes less RAM.

HashMapStorage

This should be used for components which are associated with very few entities, because it provides a lower insertion cost and is packed together more tightly. You should not use it for frequently used components, because the hashing cost would definitely be noticeable.

NullStorage

As already described in the overview, the NullStorage does itself only contain a user-defined ZST (=Zero Sized Type; a struct with no data in it, like struct Synced;). Because it's wrapped in a so-called MaskedStorage, insertions and deletions modify the mask, so it can be used for flagging entities (like in this example for marking an entity as Synced, which could be used to only synchronize some of the entities over the network).

VecStorage

This one has only one vector (as opposed to the DenseVecStorage). It just leaves uninitialized gaps where we don't have any component. Therefore it would be a waste of memory to use this storage for rare components, but it's best suited for commonly used components (like transform values).

System Data

Every system can request data which it needs to run. This data can be specified using the System::SystemData type. Typical implementors of the SystemData trait are ReadStorage, WriteStorage, Read, Write, ReadExpect, WriteExpect and Entities. A tuple of types implementing SystemData automatically also implements SystemData. This means you can specify your System::SystemData as follows:


# #![allow(unused_variables)]
#fn main() {
struct Sys;

impl<'a> System<'a> for Sys {
    type SystemData = (WriteStorage<'a, Pos>, ReadStorage<'a, Vel>);
    
    fn run(&mut self, (pos, vel): Self::SystemData) {
        /* ... */
    }
}
#}

It is very important that you don't request both a ReadStorage and a WriteStorage for the same component or a Read and a Write for the same resource. This is just like the borrowing rules of Rust, where you can't borrow something mutably and immutably at the same time. In Specs, we have to check this at runtime, thus you'll get a panic if you don't follow this rule.

Accessing Entities

You want to create/delete entities from a system? There is good news for you. You can use Entities to do that. It implements SystemData so just put it in your SystemData tuple.

Don't confuse specs::Entities with specs::EntitiesRes. While the latter one is the actual resource, the former one is a type definition for Read<Entities>.

Please note that you may never write to these Entities, so only use Read. Even though it's immutable, you can atomically create and delete entities with it. Just use the .create() and .delete() methods, respectively. After dynamic entity deletion, a call to World::maintain is necessary in order to make the changes persistent and delete associated components.

Adding and removing components

Adding or removing components can be done by modifying either the component storage directly with a WriteStorage or lazily using the LazyUpdate resource.

use specs::{Component, Read, LazyUpdate, NullStorage, System, Entities, WriteStorage};

struct Stone;
impl Component for Stone {
    type Storage = NullStorage<Self>;
}

struct StoneCreator;
impl<'a> System<'a> for StoneCreator {
    type SystemData = (
        Entities<'a>,
        WriteStorage<'a, Stone>,
        Read<'a, LazyUpdate>,
    );

    fn run(&mut self, (entities, mut stones, updater): Self::SystemData) {
        let stone = entities.create();

        // 1) Either we insert the component by writing to its storage
        stones.insert(stone, Stone);

        // 2) or we can lazily insert it with `LazyUpdate`
        updater.insert(stone, Stone);
    }
}

Note: After using LazyUpdate a call to World::maintain is necessary to actually execute the changes.

SetupHandler / Default for resources

Please refer to the resources chapter for automatic creation of resources.

Specifying SystemData

As mentioned earlier, SystemData is implemented for tuples up to 26 elements. Should you ever need more, you could even nest these tuples. However, at some point it becomes hard to keep track of all the elements. That's why you can also create your own SystemData bundle using a struct:

extern crate shred;
#[macro_use]
extern crate shred_derive;
extern crate specs;

use specs::prelude::*;

#[derive(SystemData)]
pub struct MySystemData<'a> {
    positions: ReadStorage<'a, Position>,
    velocities: ReadStorage<'a, Velocity>,
    forces: ReadStorage<'a, Force>,

    delta: Read<'a, DeltaTime>,
    game_state: Write<'a, GameState>,
}

The setup stage

So far for all our component storages and resources, we've been adding them to the World manually. In Specs, this is not required if you use setup. This is a manually invoked stage that goes through SystemData and calls register, add_resource, etc. for all (with some exceptions) components and resources found. The setup function can be found in the following locations:

  • ReadStorage, WriteStorage, Read, Write
  • SystemData
  • System
  • RunNow
  • Dispatcher
  • ParSeq

During setup, all components encountered will be registered, and all resources that have a Default implementation or a custom SetupHandler will be added. Note that resources encountered in ReadExpect and WriteExpect will not be added to the World automatically.

The recommended way to use setup is to run it on Dispatcher or ParSeq after the system graph is built, but before the first dispatch. This will go through all Systems in the graph, and call setup on each.

Let's say you began by registering Components and Resources first:


struct Gravity;

struct Velocity;

impl Component for Position {
    type Storage = VecStorage<Self>;
}

struct SimulationSystem;

impl<'a> System<'a> for SimulationSystem {
    type SystemData = (Read<'a, Gravity>, WriteStorage<'a, Velocity>);

    fn run(_, _) {}
}

fn main() {
    let mut world = World::new();
    world.add_resource(Gravity);
    world.register::<Velocity>();

    for _ in 0..5 {
        world.create_entity().with(Velocity).build();
    }

    let mut dispatcher = DispatcherBuilder::new()
        .with(SimulationSystem, "simulation", &[])
        .build();

    dispatcher.dispatch(&mut world.res);
    world.maintain();
}

You could get rid of that phase by calling setup() and re-ordering your main function:

fn main() {
    let mut world = World::new();
    let mut dispatcher = DispatcherBuilder::new()
        .with(SimulationSystem, "simulation", &[])
        .build();

    dispatcher.setup(&mut world.res);

    for _ in 0..5 {
        world.create_entity().with(Velocity).build();
    }

    dispatcher.dispatch(&mut world.res);
    world.maintain();
}

Custom setup functionality

The good qualities of setup don't end here however. We can also use setup to create our non-Default resources, and also to initialize our Systems! We do this by custom implementing the setup function in our System.

Let's say we have a System that process events, using shrev::EventChannel:

struct Sys {
    reader: ReaderId<Event>,
}

impl<'a> System<'a> for Sys {
    type SystemData = Read<'a, EventChannel<Event>>;

    fn run(&mut self, events: Self::SystemData) {
        for event in events.read(&mut self.reader) {
            [..]
        }
    }
}

This looks pretty OK, but there is a problem here if we want to use setup. The issue is that Sys needs a ReaderId on creation, but to get a ReaderId, we need EventChannel<Event> to be initialized. This means the user of Sys need to create the EventChannel themselves and add it manually to the World. We can do better!

use specs::prelude::Resources;

#[derive(Default)]
struct Sys {
    reader: Option<ReaderId<Event>>,
}

impl<'a> System<'a> for Sys {
    type SystemData = Read<'a, EventChannel<Event>>;

    fn run(&mut self, events: Self::SystemData) {
        for event in events.read(&mut self.reader.as_mut().unwrap()) {
            [..]
        }
    }

    fn setup(&mut self, res: &mut Resources) {
        use specs::prelude::SystemData;
        Self::SystemData::setup(res);
        self.reader = Some(res.fetch_mut::<EventChannel<Event>>().register_reader());
    }
}

This is much better; we can now use setup to fully initialize Sys without requiring our users to create and add resources manually to World!

If we override the setup function on a System, it is vitally important that we remember to add Self::SystemData::setup(res);, or setup will not be performed for the Systems SystemData. This could cause panics during setup or during the first dispatch.

Setting up in bulk

In the case of libraries making use of specs, it is sometimes helpful to provide a way to add many things at once. It's generally recommended to provide a standalone function to register multiple Components/Resources at once, while allowing the user to add individual systems by themselves.

fn add_physics_engine(world: &mut World, config: LibraryConfig) -> Result<(), LibraryError> {
    world.register::<Velocity>();
    // etc
}

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;
}

Optional components

If we iterate over the &EntitiesRes as shown above, we can simply use the returned Entity values to get components from storages as usual.

for (ent, pos, vel) in (&*entities, &mut pos_storage, &vel_storage).join() {
    println!("Processing entity: {:?}", ent);
    *pos += *vel;
    
    let mass: Option<&mut Mass> = mass_storage.get_mut(ent);
    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.

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.

Parallel Join

As mentioned in the chapter dedicated to how to dispatch systems, Specs automatically parallelizes system execution when there are non-conflicting system data requirements (Two Systems conflict if their SystemData needs access to the same resource where at least one of them needs write access to it).

Basic parallelization

What isn't automatically parallelized by Specs are the joins made within a single system:

    fn run(&mut self, (vel, mut pos): Self::SystemData) {
        use specs::Join;
        // This loop runs sequentially on a single thread.
        for (vel, pos) in (&vel, &mut pos).join() {
            pos.x += vel.x * 0.05;
            pos.y += vel.y * 0.05;
        }
    }

This means that, if there are hundreds of thousands of entities and only a few systems that actually can be executed in parallel, then the full power of CPU cores cannot be fully utilized.

To fix this potential inefficiency and to parallelize the joining, the join method call can be exchanged for par_join:

fn run(&mut self, (vel, mut pos): Self::SystemData) {
    use rayon::prelude::*;
    use specs::ParJoin;

    // Parallel joining behaves similarly to normal joining
    // with the difference that iteration can potentially be
    // executed in parallel by a thread pool.
    (&vel, &mut pos)
        .par_join()
        .for_each(|(vel, pos)| {
            pos.x += vel.x * 0.05;
            pos.y += vel.y * 0.05;
        });
}

There is always overhead in parallelization, so you should carefully profile to see if there are benefits in the switch. If you have only a few things to iterate over then sequential join is faster.

The par_join method produces a type implementing rayon's ParallelIterator trait which provides lots of helper methods to manipulate the iteration, the same way the normal Iterator trait does.

Rendering

Rendering is often a little bit tricky when you're dealing with a multi-threaded ECS. That's why we have something called "thread-local systems".

There are two things to keep in mind about thread-local systems:

  1. They're always executed at the end of dispatch.
  2. They cannot have dependencies; you just add them in the order you want them to run.

Adding one is a simple line added to the builder code:

DispatcherBuilder::new()
    .with_thread_local(RenderSys);

Amethyst

As for Amethyst, it's very easy because Specs is already integrated. So there's no special effort required, just look at the current examples.

Piston

Piston has an event loop which looks like this:

while let Some(event) = window.poll_event() {
    // Handle event
}

Now, we'd like to do as much as possible in the ECS, so we feed in input as a resource. This is what your code could look like:

struct ResizeEvents(Vec<(u32, u32)>);

world.add_resource(ResizeEvents(Vec::new()));

while let Some(event) = window.poll_event() {
    match event {
        Input::Resize(x, y) => world.write_resource::<ResizeEvents>().0.push((x, y)),
        // ...
    }
}

The actual dispatching should happen every time the Input::Update event occurs.


If you want a section for your game engine added, feel free to submit a PR!

Advanced strategies for components

So now that we have a fairly good grasp on the basics of Specs, it's time that we start experimenting with more advanced patterns!

Marker components

Say we want to add a drag force to only some entities that have velocity, but let other entities move about freely without drag.

The most common way is to use a marker component for this. A marker component is a component without any data that can be added to entities to "mark" them for processing, and can then be used to narrow down result sets using Join.

Some code for the drag example to clarify:

#[derive(Component)]
#[storage(NullStorage)]
pub struct Drag;

#[derive(Component)]
pub struct Position {
    pub pos: [f32; 3],
}

#[derive(Component)]
pub struct Velocity {
    pub velocity: [f32; 3],
}

struct Sys {
    drag: f32,
}

impl<'a> System<'a> for Sys {
    type SystemData = (
        ReadStorage<'a, Drag>,
        ReadStorage<'a, Velocity>,
        WriteStorage<'a, Position>,
    );
    
    fn run(&mut self, (drag, velocity, mut position): Self::SystemData) {
        // Update positions with drag
        for (pos, vel, _) in (&mut position, &velocity, &drag).join() {
            pos += vel - self.drag * vel * vel;
        }
        // Update positions without drag
        for (pos, vel, _) in (&mut position, &velocity, !&drag).join() {
            pos += vel;
        }
    } 
}

Using NullStorage is recommended for marker components, since they don't contain any data and as such will not consume any memory. This means we can represent them using only a bitset. Note that NullStorage will only work for components that are ZST (i.e. a struct without fields).

Modeling entity relationships and hierarchy

A common use case where we need a relationship between entities is having a third person camera following the player around. We can model this using a targeting component referencing the player entity.

A simple implementation might look something like this:


#[derive(Component)]
pub struct Target {
    target: Entity,
    offset: Vector3,
}

pub struct FollowTargetSys;

impl<'a> System<'a> for FollowTargetSys {
    type SystemData = (
        Entities<'a>,
        ReadStorage<'a, Target>,
        WriteStorage<'a, Transform>,
    );
    
    fn run(&mut self, (entity, target, transform): Self::SystemData) {
        for (entity, t) in (&*entity, &target).join() {
            let new_transform = transform.get(t.target).cloned().unwrap() + t.offset;
            *transform.get_mut(entity).unwrap() = new_transform;
        }
    }
}

We could also model this as a resource (more about that in the next section), but it could be useful to be able to have multiple entities following targets, so modeling this with a component makes sense. This could in extension be used to model large scale hierarchical structure (scene graphs). For a generic implementation of such a hierarchical system, check out the crate specs-hierarchy.

Entity targeting

Imagine we're building a team based FPS game, and we want to add a spectator mode, where the spectator can pick a player to follow. In this scenario each player will have a camera defined that is following them around, and what we want to do is to pick the camera that we should use to render the scene on the spectator screen.

The easiest way to deal with this problem is to have a resource with a target entity, that we can use to fetch the actual camera entity.

pub struct ActiveCamera(Entity);

pub struct Render;

impl<'a> System<'a> for Render {
    type SystemData = (
        Read<'a, ActiveCamera>,
        ReadStorage<'a, Camera>,
        ReadStorage<'a, Transform>,
        ReadStorage<'a, Mesh>,
    );
    
    fn run(&mut self, (active_cam, camera, transform, mesh) : Self::SystemData) {
        let camera = camera.get(active_cam.0).unwrap();
        let view_matrix = transform.get(active_cam.0).unwrap().invert();
        // Set projection and view matrix uniforms
        for (mesh, transform) in (&mesh, &transform).join() {
            // Set world transform matrix
            // Render mesh
        }
    }
}

By doing this, whenever the spectator chooses a new player to follow, we simply change what Entity is referenced in the ActiveCamera resource, and the scene will be rendered from that viewpoint instead.

Sorting entities based on component value

In a lot of scenarios we encounter a need to sort entities based on either a component's value, or a combination of component values. There are a couple of ways to deal with this problem. The first and most straightforward is to just sort Join results.

let data = (&entities, &comps).join().collect::<Vec<_>>();
data.sort_by(|&a, &b| ...);
for entity in data.iter().map(|d| d.0) {
    // Here we get entities in sorted order
}

There are a couple of limitations with this approach, the first being that we will always process all matched entities every frame (if this is called in a System somewhere). This can be fixed by using FlaggedStorage to maintain a sorted Entity list in the System. We will talk more about FlaggedStorage in the next chapter.

The second limitation is that we do a Vec allocation every time, however this can be alleviated by having a Vec in the System struct that we reuse every frame. Since we are likely to keep a fairly steady amount of entities in most situations this could work well.

FlaggedStorage and modification events

In most games you will have many entities, but from frame to frame there will usually be components that will only need to updated when something related is modified.

To avoid a lot of unnecessary computation when updating components it would be nice if we could somehow check for only those entities that are updated and recalculate only those.

We might also need to keep an external resource in sync with changes to components in Specs World, and we only want to propagate actual changes, not do a full sync every frame.

This is where FlaggedStorage comes into play. By wrapping a component's actual storage in a FlaggedStorage, we can subscribe to modification events, and easily populate bitsets with only the entities that have actually changed.

Let's look at some code:

pub struct Data {
    [..]
}

impl Component for Data {
    type Storage = FlaggedStorage<Self, DenseVecStorage<Self>>;
}

#[derive(Default)]
pub struct Sys {
    pub dirty: BitSet,
    pub reader_id: Option<ReaderId<ComponentEvent>>,
}

impl<'a> System<'a> for Sys {
    type SystemData = (
        ReadStorage<'a, Data>,
        WriteStorage<'a, SomeOtherData>,
    );

    fn run(&mut self, (data, mut some_other_data): Self::SystemData) {
        self.dirty.clear();

        let events = data.channel().read(self.reader_id.as_mut().unwrap());

        // Note that we could use separate bitsets here, we only use one to
        // simplify the example
        for event in events {
            match event {
                ComponentEvent::Modified(id) | ComponentEvent::Inserted(id) => {
                    self.dirty.add(*id);
                }
                 // We don't need to take this event into account since
                 // removed components will be filtered out by the join;
                 // if you want to, you can use `self.dirty.remove(*id);`
                 // so the bit set only contains IDs that still exist
                 ComponentEvent::Removed(_) => (),
            }
        }

        for (d, other, _) in (&data, &mut some_other_data, &self.dirty).join() {
            // Mutate `other` based on the update data in `d`
        }
    }

    fn setup(&mut self, res: &mut Resources) {
        Self::SystemData::setup(res);
        self.reader_id = Some(WriteStorage::<Data>::fetch(&res).register_reader());
    }
}

There are three different event types that we can receive:

  • ComponentEvent::Inserted - will be sent when a component is added to the storage
  • ComponentEvent::Modified - will be sent when a component is fetched mutably from the storage
  • ComponentEvent::Removed - will be sent when a component is removed from the storage

Note that because of how ComponentEvent works, if you iterate mutably over a component storage using Join, all entities that are fetched by the Join will be flagged as modified even if nothing was updated in them.