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.