diff --git a/.gitignore b/.gitignore index 603a928a2..0155b025f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ Cargo.lock # IDEs / Editor *.iml .idea + +# Website content +/bin +/public diff --git a/docs/public/reference/.nojekyll b/docs/public/reference/.nojekyll new file mode 100644 index 000000000..863121594 --- /dev/null +++ b/docs/public/reference/.nojekyll @@ -0,0 +1 @@ +This file makes sure that Github Pages doesn't process mdBook's output. \ No newline at end of file diff --git a/docs/public/reference/01_system.html b/docs/public/reference/01_system.html new file mode 100644 index 000000000..a07b2a54d --- /dev/null +++ b/docs/public/reference/01_system.html @@ -0,0 +1,196 @@ + + +
+ + +This is the reference for Specs, a Rust Entity Component System library.
+If you're looking for tutorials instead, there's a book for that +here, too.
+ +This is the reference for Specs, a Rust Entity Component System library.
+If you're looking for tutorials instead, there's a book for that +here, too.
+ +This is the reference for Specs, a Rust Entity Component System library.
+If you're looking for tutorials instead, there's a book for that +here, too.
+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
:
There also is a reference-style documentation available here:
+ +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.
+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:
+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.
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.
+ +World
!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;
+
+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.
World
use specs::{World, Builder};
+
+let mut world = World::new();
+world.register::<Position>();
+world.register::<Velocity>();
+
+This will create component storages for Position
s and Velocity
s.
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!
+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.
+
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.
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
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.
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
+HelloWorld
)"hello_world""
)&[]
).The name can be used to specify that system +as a dependency of another one. But we don't have a second +system yet.
+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
andWriteStorage
are implementors ofSystemData
+themselves, that's why we could use the first one for ourHelloWorld
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 Resource
s, see
+the next Chapter.
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);
+
+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 Resource
s,
+a way to share data between systems which only exist independent of
+entities (as opposed to 0..1 times per entity).
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 usef32
s 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);
+
+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 resourcesAs 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:
+SetupHandler
implementation, you can provide this in SystemData
+with Read<'a, Resource, TheSetupHandlerType>
.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.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.
+ +Specs contains a bunch of different storages, all built and optimized for +different use cases. But let's see some basics first.
+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).
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 Vec
s, 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).
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.
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
withspecs::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. After dynamic entity deletion,
+a call to World::maintain
is necessary in order to make the changes
+persistent and delete associated 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 toWorld::maintain
+is necessary to actually execute the changes.
SetupHandler
/ Default
for resourcesPlease refer to the resources chapter for automatic creation of resources.
+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>,
+}
+
+
+ setup
stageSo 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 System
s 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();
+}
+
+
+setup
functionalityThe 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 System
s!
+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 System
s SystemData
. This could cause panics during setup or during
+the first dispatch.
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
+}
+
+
+ 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".
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;
+}
+
+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.
+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
+Frozen
componentYou 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)We think the last point here is pretty interesting, because +it allows for even more flexibility, as you will see in the next +section.
+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());
+
+BitSet
s 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.
+ +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 System
s conflict if their SystemData
needs access
+to the same resource where at least one of them needs write access to it).
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 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:
+Adding one is a simple line added to the builder code:
+DispatcherBuilder::new()
+ .with_thread_local(RenderSys);
+
+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 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!
+
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!
+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).
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
.
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.
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 eventsIn 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
+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
+storageNote 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.
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
:
There also is a reference-style documentation available here:
+ +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.
+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:
+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.
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.
+ +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
:
There also is a reference-style documentation available here:
+ +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.
+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:
+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.
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.
+World
!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;
+
+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.
World
use specs::{World, Builder};
+
+let mut world = World::new();
+world.register::<Position>();
+world.register::<Velocity>();
+
+This will create component storages for Position
s and Velocity
s.
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!
+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.
+
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.
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
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.
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
+HelloWorld
)"hello_world""
)&[]
).The name can be used to specify that system +as a dependency of another one. But we don't have a second +system yet.
+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
andWriteStorage
are implementors ofSystemData
+themselves, that's why we could use the first one for ourHelloWorld
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 Resource
s, see
+the next Chapter.
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);
+
+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 Resource
s,
+a way to share data between systems which only exist independent of
+entities (as opposed to 0..1 times per entity).
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 usef32
s 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);
+
+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 resourcesAs 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:
+SetupHandler
implementation, you can provide this in SystemData
+with Read<'a, Resource, TheSetupHandlerType>
.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.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.
+Specs contains a bunch of different storages, all built and optimized for +different use cases. But let's see some basics first.
+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).
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 Vec
s, 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).
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.
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
withspecs::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. After dynamic entity deletion,
+a call to World::maintain
is necessary in order to make the changes
+persistent and delete associated 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 toWorld::maintain
+is necessary to actually execute the changes.
SetupHandler
/ Default
for resourcesPlease refer to the resources chapter for automatic creation of resources.
+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>,
+}
+
+setup
stageSo 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 System
s 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();
+}
+
+
+setup
functionalityThe 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 System
s!
+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 System
s SystemData
. This could cause panics during setup or during
+the first dispatch.
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
+}
+
+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".
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;
+}
+
+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.
+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
+Frozen
componentYou 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)We think the last point here is pretty interesting, because +it allows for even more flexibility, as you will see in the next +section.
+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());
+
+BitSet
s 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.
+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 System
s conflict if their SystemData
needs access
+to the same resource where at least one of them needs write access to it).
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 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:
+Adding one is a simple line added to the builder code:
+DispatcherBuilder::new()
+ .with_thread_local(RenderSys);
+
+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 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!
+
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!
+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).
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
.
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.
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 eventsIn 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
+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
+storageNote 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.