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.
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.
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).
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.
Before setting up the project, please make sure you're using the latest Rust version:
rustup update
Okay, now let's set up the project!
cargo new --bin my_game
Add the following line to your Cargo.toml:
[dependencies]
specs = "0.16.1"
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 enable the derive feature:
[dependencies]
specs = { version = "0.16.1", features = ["specs-derive"] }
Now you can do this:
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).
#[storage(VecStorage)] assumes <Self> as the default type parameter for the storage.
More complex type parameters can be specified explicitly:
#[derive(Component, Debug)]
#[storage(FlaggedStorage<Self, DenseVecStorage<Self>>)]
pub struct Data {
[..]
}
(see the later FlaggedStorage and modification events chapter for more details on FlaggedStorage)
But before we move on, we need to create a world in which to store all of our components.
The World
use specs::{World, WorldExt, 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.
Note:
Worldis a struct coming fromshred, an important dependency of Specs. Whenever you call functions specific to Specs, you will need to import theWorldExttrait.
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);
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, WorldExt, 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);
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
- system object (
HelloWorld) - with some name (
"hello_world"") - 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
ReadStorageandWriteStorageare implementors ofSystemDatathemselves, that's why we could use the first one for ourHelloWorldsystem 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);
Full example code
Here the code for this chapter:
use specs::{Builder, Component, DispatcherBuilder, ReadStorage,
System, VecStorage, World, WorldExt, 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);
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::Durationinstead, because you shouldn't usef32s for durations in an actual game, because they're not precise enough.
Adding this resource to our world is pretty easy:
world.insert(DeltaTime(0.05)); // Let's use some start value
To update the delta time, just use
use specs::WorldExt;
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.insert(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
SetupHandlerimplementation, you can provide this inSystemDatawithRead<'a, Resource, TheSetupHandlerType>. - By replacing
ReadandWritewithReadExpectandWriteExpect, which will cause the first dispatch of theSystemto panic unless the resource has been added manually toWorldfirst. - By using
Option<Read<'a, Resource>>, if the resource really is optional. Note that the order here is important, usingRead<'a, Option<Resource>>will not result in the same behavior (it will try to fetchOption<Resource>fromWorld, instead of doing an optional check ifResourceexists).
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, empty slots are uninitialized | commonly used components |
DefaultVecStorage | Uses a sparse Vec, empty slots contain Default | commonly used components |
Slices
Certain storages provide access to component slices:
| Storage Type | Slice type | Density | Indices |
|---|---|---|---|
DenseVecStorage | &[T] | Dense | Arbitrary |
VecStorage | &[MaybeUninit<T>] | Sparse | Entity id() |
DefaultVecStorage | &[T] | Sparse | Entity id() |
This is intended as an advanced technique. Component slices provide maximally efficient reads and writes, but they are incompatible with many of the usual abstractions which makes them more difficult to use.
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.
DenseVecStorage<T> provides as_slice() and as_mut_slice() accessors
which return &[T]. The indices in this slice do not correspond to entity
IDs, nor do they correspond to indices in any other storage, nor do they
correspond to indices in this storage at a different point in time.
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).
VecStorage<T> provides as_slice() and as_mut_slice() accessors which
return &[MaybeUninit<T>]. Consult the Storage::mask() to determine
which indices are populated. Slice indices cannot be converted to Entity
values because they lack a generation counter, but they do correspond to
Entity::id()s, so indices can be used to collate between multiple
VecStorages.
DefaultVecStorage
This storage works exactly like VecStorage, but instead of leaving gaps
uninitialized, it fills them with the component's default value. This
requires the component to impl Default, and it results in more memory
writes than VecStorage.
DefaultVecStorage provides as_slice() and as_mut_slice() accessors
which return &[T]. Storage::mask() can be used to determine which
indices are in active use, but all indices are fully initialized, so the
mask() is not necessary for safety. DefaultVecStorage indices all
correspond with each other, with VecStorage indices, and with
Entity::id()s.
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)] 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::Entitieswithspecs::EntitiesRes. While the latter one is the actual resource, the former one is a type definition forRead<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.
For example, if you wanted to delete an entity based after a period of time you could write something similar like this.
#![allow(unused)] fn main() { pub struct Life { life: f32, } struct DecaySys; impl<'a> System<'a> for DecaySys { type SystemData = (Entities<'a>, WriteStorage<'a, Life>); fn run(&mut self, (entities, mut life): Self::SystemData) { for (e, life) in (&entities, &mut life).join() { if life < 0.0 { entities.delete(e); } else { life -= 1.0; } } } } }
Just remember after dynamic entity deletion, a call to
World::maintainis 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
LazyUpdatea call toWorld::maintainis 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 specs;
use specs::prelude::*;
// `shred` needs to be in scope for the `SystemData` derive.
use specs::shred;
#[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>,
}
Make sure to enable the shred-derive feature in your Cargo.toml:
specs = { version = "*", features = ["shred-derive"] }
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, insert, etc. for all (with some exceptions)
components and resources found. The setup function can be found in
the following locations:
ReadStorage,WriteStorage,Read,WriteSystemDataSystemRunNowDispatcherParSeq
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:
use specs::prelude::*;
#[derive(Default)]
struct Gravity;
struct Velocity;
impl Component for Velocity {
type Storage = VecStorage<Self>;
}
struct SimulationSystem;
impl<'a> System<'a> for SimulationSystem {
type SystemData = (Read<'a, Gravity>, WriteStorage<'a, Velocity>);
fn run(&mut self, _: Self::SystemData) {}
}
fn main() {
let mut world = World::new();
world.insert(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);
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);
for _ in 0..5 {
world.create_entity().with(Velocity).build();
}
dispatcher.dispatch(&mut world);
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::*;
#[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, world: &mut World) {
Self::SystemData::setup(world);
self.reader = Some(world.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(world);, 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;
}
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
Frozencomponent
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(returnsEntityvalues)- 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:
- They're always executed at the end of dispatch.
- 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.insert(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 be 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 storageComponentEvent::Modified- will be sent when a component is fetched mutably from the storageComponentEvent::Removed- will be sent when a component is removed from the storage
Gotcha: Iterating FlaggedStorage Mutably
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.
For example, this will cause all comps components to be flagged as modified:
// **Never do this** if `comps` uses `FlaggedStorage`.
//
// This will flag all components as modified regardless of whether the inner
// loop actually modified the component.
for comp in (&mut comps).join() {
// ...
}
Instead, you will want to either:
- Restrict the components mutably iterated over, for example by joining with a
BitSetor another component storage. - Iterating over the components use a
RestrictedStorageand only fetch the component as mutable if/when needed.
RestrictedStorage
If you need to iterate over a FlaggedStorage mutably and don't want every
component to be marked as modified, you can use a RestrictedStorage and only
fetch the component as mutable if/when needed.
for (entity, mut comp) in (&entities, &mut comps.restrict_mut()).join() {
// Check whether this component should be modified, without fetching it as
// mutable.
if comp.get().condition < 5 {
let mut comp = comp.get_mut();
// ...
}
}
Start and Stop event emission
Sometimes you may want to perform some operations on the storage, but you don't want that these operations produce any event.
You can use the function storage.set_event_emission(false) to suppress the
event writing for of any action. When you want to re activate them you can
simply call storage.set_event_emission(true).
See FlaggedStorage Doc for more into.
Saveload
saveload is a module that provides mechanisms to serialize and deserialize a
World, it makes use of the popular serde library and requires the feature
flag serde to be enabled for specs in your Cargo.toml file.
At a high level, it works by defining a Marker component as well as a
MarkerAllocator resource. Marked entities will be the only ones subject to
serialization and deserialization.
saveload also defines SerializeComponents and DeserializeComponents,
these do the heavy lifting of exporting and importing.
Let's go over everything, point by point:
Marker and MarkerAllocator
Marker and MarkerAllocator<M: Marker> are actually traits, simple
implementations are available with SimpleMarker<T: ?Sized> and
SimpleMarkerAllocator<T: ?Sized>, which you may use multiple times with
Zero Sized Types.
struct NetworkSync;
struct FilePersistent;
fn main() {
let mut world = World::new();
world.register::<SimpleMarker<NetworkSync>>();
world.insert(SimpleMarkerAllocator::<NetworkSync>::default());
world.register::<SimpleMarker<FilePersistent>>();
world.insert(SimpleMarkerAllocator::<FilePersistent>::default());
world
.create_entity()
.marked::<SimpleMarker<NetworkSync>>()
.marked::<SimpleMarker<FilePersistent>>()
.build();
}
You may also roll your own implementations like so:
use specs::{prelude::*, saveload::{MarkedBuilder, Marker, MarkerAllocator}};
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
#[derive(serde::Serialize, serde::Deserialize)]
struct MyMarker(u64);
impl Component for MyMarker {
type Storage = VecStorage<Self>;
}
impl Marker for MyMarker {
type Identifier = u64;
type Allocator = MyMarkerAllocator;
fn id(&self) -> u64 {
self.0
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct MyMarkerAllocator(std::collections::HashMap<u64, Entity>);
impl MarkerAllocator<MyMarker> for MyMarkerAllocator {
fn allocate(&mut self, entity: Entity, id: Option<u64>) -> MyMarker {
let id = id.unwrap_or_else(|| self.unused_key()));
self.0.insert(id, entity);
MyMarker(id)
}
fn retrieve_entity_internal(&self, id: u64) -> Option<Entity> {
self.0.get(&id).cloned()
}
fn maintain(
&mut self,
entities: &EntitiesRes,
storage: &ReadStorage<MyMarker>,
) {
// naive and possibly costly implementation, the techniques in
// chapter 12 would be useful here!
self.0 = (entities, storage)
.join()
.map(|(entity, marker)| (marker.0, entity))
.collect();
}
}
fn main() {
let mut world = World::new();
world.register::<MyMarker>();
world.insert(MyMarkerAllocator::default());
world
.create_entity()
.marked::<MyMarker>()
.build();
}
Note that the trait MarkedBuilder must be imported to mark entities during
creation, it is implemented for EntityBuilder and LazyBuilder. Marking an
entity that is already present is straightforward:
fn mark_entity(
entity: Entity,
mut allocator: Write<SimpleMarkerAllocator<A>>,
mut storage: WriteStorage<SimpleMarker<A>>,
) {
use MarkerAllocator; // for MarkerAllocator::mark
match allocator.mark(entity, &mut storage) {
None => println!("entity was dead before it could be marked"),
Some((_, false)) => println!("entity was already marked"),
Some((_, true)) => println!("entity successfully marked"),
}
}
Serialization and Deserialization
As previously mentioned, SerializeComponents and DeserializeComponents are
the two heavy lifters. They're traits as well, however, that's just an
implementation detail, they are used like functions. They're implemented over
tuples of up to 16 ReadStorage/WriteStorage.
Here is an example showing how to serialize:
specs::saveload::SerializeComponents
::<Infallible, SimpleMarker<A>>
::serialize(
&(position_storage, mass_storage), // tuple of ReadStorage<'a, _>
&entities, // Entities<'a>
&marker_storage, // ReadStorage<'a, SimpleMarker<A>>
&mut serializer, // serde::Serializer
) // returns Result<Serializer::Ok, Serializer::Error>
and now, how to deserialize:
specs::saveload::DeserializeComponents
::<Infallible, SimpleMarker<A>>
::deserialize(
&mut (position_storage, mass_storage), // tuple of WriteStorage<'a, _>
&entities, // Entities<'a>
&mut marker_storage, // WriteStorage<'a SimpleMarker<A>>
&mut marker_allocator, // Write<'a, SimpleMarkerAllocator<A>>
&mut deserializer, // serde::Deserializer
) // returns Result<(), Deserializer::Error>
As you can see, all parameters but one are SystemData, the easiest way to
access those would be through systems (chapter on this subject) or by
calling World::system_data:
let (
entities,
mut marker_storage,
mut marker_allocator,
mut position_storage,
mut mass_storage,
) = world.system_data::<(
Entities,
WriteStorage<SimpleMarker<A>>,
Write<SimpleMarkerAllocator<A>>,
WriteStorage<Position>,
WriteStorage<Mass>,
)>();
Each Component that you will read from and write to must implement
ConvertSaveload, it's a benign trait and is implemented for all Components
that are Clone + serde::Serialize + serde::DeserializeOwned, however you may
need to implement it (or derive it using specs-derive). In which case, you
may introduce more bounds to the first generic parameter and will need to
replace Infallible with a custom type, this custom type must implement
From<<TheComponent as ConvertSaveload>::Error> for all Components,
basically.
Troubleshooting
Tried to fetch a resource, but the resource does not exist.
This is the most common issue you will face as a new user of Specs.
This panic will occur whenever a System is first dispatched, and one or
more of the components and/or resources it uses is missing from World.
There are a few main reasons for this occurring:
- Forgetting to call
setupafter building aDispatcherorParSeq. Make sure this is always run before the first dispatch. - Not adding mandatory resources to
World. You can usually find these by searching for occurrences ofReadExpectandWriteExpect. - Manually requesting components/resources from
World(not inside aSystem), where the component/resource is not used by anySystems, which is most common when using theEntityBuilder. This is an artifact of howsetupworks, it will only add what is found inside the usedSystems. If you use other components/resources, you need to manually register/add these toWorld.