Crate component_group

source ·
Expand description

This crate defines the ComponentGroup trait. This trait is used to make managing a group of specs::Component instances easier. This is useful for when you have several components that are often created, read, and updated together. You can use this trait to easily move an entire group of components between instances of specs::World.

This crate also provides a custom derive (documented below) that you can use to automatically implement the trait. This removes any of the boilerplate you may have needed to write in order to implement the trait yourself and makes modifying your group of components much easier.

// Don't forget to add the component_group crate to your Cargo.toml file!
use component_group::ComponentGroup;

// These components are just for demonstration purposes. You should swap them
// out for your own.

// Components need to implement Clone to use the automatic derive.
#[derive(Debug, Clone, Component)]
#[storage(VecStorage)]
pub struct Position {x: i32, y: i32}

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

#[derive(Debug, Clone, Component)]
#[storage(VecStorage)]
pub struct Health(u32);

#[derive(Debug, Clone, Component)]
#[storage(HashMapStorage)]
pub struct Animation {frame: usize}

// This is all of the code you need to write to define the group and its operations!
#[derive(ComponentGroup)]
struct PlayerComponents {
    position: Position,
    velocity: Velocity,
    health: Health,
    // This optional component is allowed to not be present
    animation: Option<Animation>,
}

// Now you can easily add all of these components to an entity, load them all
// from the world, or even update them all at once!

See the documentation for ComponentGroup for the exact operations you can now perform on the PlayerComponents struct. The rest of the documentation below goes into the motivation behind this crate and details about how to use it.

Table of Contents

Motivation

The ComponentGroup trait makes operating on many components at once much easier and less error-prone. Trying to update all of your code every time you add a new component to an entity is very difficult to manage. By grouping all of the components together in a single struct, you can better manage all of them and make fewer mistakes when you edit your code. The following is an example of what your code might look like without this trait.

// Rust 2018 edition
use specs::{World, Builder, Entity, Component, VecStorage, ReadStorage, WriteStorage, Join};
use specs::error::Error as SpecsError;
use specs_derive::Component;

// Let's setup some components to add to our World
#[derive(Debug, Component)]
#[storage(VecStorage)]
pub struct Position {x: i32, y: i32}

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

#[derive(Debug, Component)]
#[storage(VecStorage)]
pub struct Health(u32);

fn main() -> Result<(), SpecsError> {
    // Start the player on level 1
    let mut level1 = World::new();
    // Add the player to the level
    level1.create_entity()
        // This player only has three components right now, but imagine what could happen
        // to this code as that number grows
        .with(Position {x: 12, y: 59})
        .with(Velocity {x: -1, y: 2})
        .with(Health(5))
        .build();

    // ...

    // Player needs to move on to the next level
    let mut level2 = World::new();
    // Somehow find the player in the world it was just in
    let player_entity = find_player_entity(&level1);
    // Need to fetch all the components of the player. Be careful to keep this in sync with
    // the code above!
    let (positions, velocities, healths) = level1.system_data::<(
        ReadStorage<Position>,
        ReadStorage<Velocity>,
        ReadStorage<Health>,
    )>();
    // If any of these fields were Clone, we could call Option::cloned on the result
    // of `get(entity)` and avoid some of this boilerplate
    let position = positions.get(player_entity).map(|pos| Position {x: pos.x, y: pos.y})
        .expect("expected a Position component to be present");
    let velocity = velocities.get(player_entity).map(|vel| Velocity {x: vel.x, y: vel.y})
        .expect("expected a Velocity component to be present");
    let health = healths.get(player_entity).map(|health| Health(health.0))
        .expect("expected a Health component to be present");
    // Now we can add everything to the new level we created
    level2.create_entity()
        .with(position)
        .with(velocity)
        .with(health)
        .build();

    // ...

    // Player needs to go back to previous level
    // Find the player in level **2**
    let player_entity = find_player_entity(&level2);
    // That means that we need to now duplicate everything from above again!
    // This time we're fetching from level2, not level1
    let (positions, velocities, healths) = level2.system_data::<(
        ReadStorage<Position>,
        ReadStorage<Velocity>,
        ReadStorage<Health>,
    )>();
    let position = positions.get(player_entity).map(|pos| Position {x: pos.x, y: pos.y})
        .expect("expected a Position component to be present");
    let velocity = velocities.get(player_entity).map(|vel| Velocity {x: vel.x, y: vel.y})
        .expect("expected a Velocity component to be present");
    let health = healths.get(player_entity).map(|health| Health(health.0))
        .expect("expected a Health component to be present");
    // Now that we have the components, we need to re-add them to level **1** for the right
    // entity.
    let player_entity = find_player_entity(&level1);
    // Now we need to fetch write storages for every component, essentially duplicating the
    // code again! This time making sure we get the storages in level **1**
    let (mut positions, mut velocities, mut healths) = level1.system_data::<(
        WriteStorage<Position>,
        WriteStorage<Velocity>,
        WriteStorage<Health>,
    )>();

    positions.insert(player_entity, position)?;
    velocities.insert(player_entity, velocity)?;
    healths.insert(player_entity, health)?;

    Ok(())
}

There is a lot of duplication in this code! Many of the duplicated pieces of code have slight differences like which world the components are being fetched from or whether we wanted a ReadStorage or a WriteStorage.The purpose of this crate is to take all of that duplication and make reusable methods that operate on all of the components at once.

Instead of having to keep track of all of this code throughout your codebase, implementing the ComponentGroup trait puts it all in one place. This makes updating your group much less error-prone because adding/modifying/removing a component to/from the group only requires modifying one area of code.

The code that needs to be written to operate on all of these components together is unavoidable given the API that specs provides, but at least you can make it easier by using this crate. We’ve eliminated the need to write most of the repetitive code you saw above by allowing you to automatically derive the ComponentGroup trait. All you need to do is define the components in the group and everything else is generated for you.

The next section of the documentation shows you how to manually implement the ComponentGroup trait for a given group of components. This is still a lot of boilerplate, but it is all grouped in one place. The section after that shows how to remove all the boilerplate by automatically deriving the trait.

Manually Implementing ComponentGroup

This example is meant to show what manually implementing this trait can be like. The basic idea is to move all of the duplicated code from above into reusable methods on a struct that groups all of the components that need to be modified together. Implementing it manually like this is still quite cumbersome, so a way to automatically derive the trait is also provided. See below for more details about that.

// Rust 2018 edition
// Don't forget to add component_group as a dependency to your Cargo.toml file!
use component_group::ComponentGroup;
use specs::{World, Builder, Entity, Entities, Component, VecStorage, ReadStorage, WriteStorage, Join};
use specs::error::Error as SpecsError;
use specs_derive::Component;

// The one benefit of implementing the trait manually is that you don't need to make the
// fields Clone like you do when automatically deriving the trait.
#[derive(Debug, Component)]
#[storage(VecStorage)]
pub struct Position {x: i32, y: i32}

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

#[derive(Debug, Component)]
#[storage(VecStorage)]
pub struct Health(u32);

pub struct PlayerComponents {
    position: Position,
    velocity: Velocity,
    health: Health,
}

impl ComponentGroup for PlayerComponents {
    type UpdateError = SpecsError;

    fn first_from_world(world: &World) -> Option<(Entity, Self)> {
        // Needs to be updated every time the struct changes
        let (entities, positions, velocities, healths) = world.system_data::<(
            Entities,
            ReadStorage<Position>,
            ReadStorage<Velocity>,
            ReadStorage<Health>,
        )>();
        (&entities, &positions, &velocities, &healths).join().next()
            .map(|(entity, pos, vel, health)| (entity, Self {
                // No need to clone because we know and can access all the fields
                position: Position {x: pos.x, y: pos.y},
                velocity: Velocity {x: vel.x, y: vel.y},
                health: Health(health.0),
            }))
    }

    fn from_world(world: &World, entity: Entity) -> Self {
        // Needs to be updated every time the struct changes
        let (positions, velocities, healths) = world.system_data::<(
            ReadStorage<Position>,
            ReadStorage<Velocity>,
            ReadStorage<Health>,
        )>();
        Self {
            // If any of these fields were Clone, we could call Option::cloned on the result
            // of `get(entity)` and avoid some of this boilerplate
            position: positions.get(entity).map(|pos| Position {x: pos.x, y: pos.y})
                .expect("expected a Position component to be present"),
            velocity: velocities.get(entity).map(|vel| Velocity {x: vel.x, y: vel.y})
                .expect("expected a Velocity component to be present"),
            health: healths.get(entity).map(|health| Health(health.0))
                .expect("expected a Health component to be present"),
        }
    }

    fn create(self, world: &mut World) -> Entity {
        // It's possible to write this code so that the compiler will at the very least
        // warn you if you forget one of the things you need to change when the struct
        // changes.

        // Using pattern matching here forces a compiler error whenever the struct changes
        let Self {position, velocity, health} = self;

        // Forgetting to add a .with() call will cause an unused variable warning
        world.create_entity()
            .with(position)
            .with(velocity)
            .with(health)
            .build()
    }

    fn update(self, world: &mut World, entity: Entity) -> Result<(), Self::UpdateError> {
        // Needs to be updated every time the struct changes
        let (mut positions, mut velocities, mut healths) = world.system_data::<(
            WriteStorage<Position>,
            WriteStorage<Velocity>,
            WriteStorage<Health>,
        )>();

        positions.insert(entity, self.position)?;
        velocities.insert(entity, self.velocity)?;
        healths.insert(entity, self.health)?;
        Ok(())
    }

    fn remove(world: &mut World, entity: Entity) -> Self {
        // Needs to be updated every time the struct changes
        let (mut positions, mut velocities, mut healths) = world.system_data::<(
            WriteStorage<Position>,
            WriteStorage<Velocity>,
            WriteStorage<Health>,
        )>();
        Self {
            // If any of these fields were Clone, we could call Option::cloned on the result
            // of `get(entity)` and avoid some of this boilerplate
            position: positions.remove(entity).map(|pos| Position {x: pos.x, y: pos.y})
                .expect("expected a Position component to be present"),
            velocity: velocities.remove(entity).map(|vel| Velocity {x: vel.x, y: vel.y})
                .expect("expected a Velocity component to be present"),
            health: healths.remove(entity).map(|health| Health(health.0))
                .expect("expected a Health component to be present"),
        }
    }
}

fn main() -> Result<(), SpecsError> {
    // Start the player on level 1
    let mut level1 = World::new();
    // Having all the components together in a struct means that Rust will enforce that you
    // never forget a field.
    // That being said, it is still possible to forget to add a component to the group. Adding
    // a component to the code above can be tricky and easy to mess up, but if you
    // automatically derive the trait, adding a component can be just a one-line change.
    let player = PlayerComponents {
        position: Position {x: 12, y: 59},
        velocity: Velocity {x: -1, y: 2},
        health: Health(5),
    };
    // Add the player to the level
    player.create(&mut level1);

    // ...

    // Player needs to move on to the next level
    let mut level2 = World::new();
    // Somehow find the player in the world it was just in
    let player_entity = find_player_entity(&level1);
    // Extract the player from the world it was just in
    let player = PlayerComponents::from_world(&level1, player_entity);
    // Add it to the next world since it hasn't been added yet
    player.create(&mut level2);

    // ...

    // Player needs to go back to previous level
    // Using first_from_world is safe when you know that there is only one entity with all
    // the components in the group
    let (_, player) = PlayerComponents::first_from_world(&level2).unwrap();
    let player_entity = find_player_entity(&level1);
    // Move the player back
    player.update(&mut level1, player_entity)?;

    Ok(())
}

Automatically Implementing ComponentGroup

You can also automatically implement the ComponentGroup trait using #[derive(ComponentGroup)]. This removes all the boilerplate you saw in the example above and automatically provides each of the methods in ComponentGroup. All fields in the struct must implement Clone so that they can be copied within the methods that get implemented.

// Rust 2018 edition
use component_group::ComponentGroup;
use specs::{World, Component, VecStorage};
use specs::error::Error as SpecsError;
use specs_derive::Component;

// Note that components need to be Clone to use the automatic derive
#[derive(Debug, Clone, Component)]
#[storage(VecStorage)]
pub struct Position {x: i32, y: i32}

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

#[derive(Debug, Clone, Component)]
#[storage(VecStorage)]
pub struct Health(u32);

// This is all of the code you need to write to define the group and all of its operations!
#[derive(ComponentGroup)]
struct PlayerComponents {
    position: Position,
    velocity: Velocity,
    health: Health,
}

fn main() -> Result<(), SpecsError> {
    // Start the player on level 1
    let mut level1 = World::new();
    // Having all the components together in a struct means that Rust will enforce that you
    // never forget a field. You can still forget to add a component to the group, but at
    // least that is just a one-line change thanks to the custom derive.
    let player = PlayerComponents {
        position: Position {x: 12, y: 59},
        velocity: Velocity {x: -1, y: 2},
        health: Health(5),
    };
    // Add the player to the level
    player.create(&mut level1);

    // ...

    // Player needs to move on to the next level
    let mut level2 = World::new();
    // Somehow find the player in the world it was just in
    let player_entity = find_player_entity(&level1);
    // Extract the player from the world it was just in
    let player = PlayerComponents::from_world(&level1, player_entity);
    // Add it to the next world since it hasn't been added yet
    player.create(&mut level2);

    // ...

    // Player needs to go back to previous level
    // Using first_from_world is safe when you know that there is only one entity with all
    // the components in the group
    let (_, player) = PlayerComponents::first_from_world(&level2).unwrap();
    let player_entity = find_player_entity(&level1);
    // Move the player back
    player.update(&mut level1, player_entity)?;

    Ok(())
}

Optional Components

You can also use Option to ignore part of the group if it isn’t specified during creation or if it isn’t available in the World during extraction. If the field is None, a call to update will remove that component for that entity from the component’s storage.

// (same components as before)

#[derive(Debug, Clone, Component)]
#[storage(HashMapStorage)]
pub struct Animation {frame: usize}

#[derive(ComponentGroup)]
struct PlayerComponents {
    position: Position,
    velocity: Velocity,
    health: Health,
    // This optional component is allowed to not be present
    animation: Option<Animation>,
}

fn main() -> Result<(), SpecsError> {
    // Start the player on level 1
    let mut level1 = World::new();
    let player = PlayerComponents {
        position: Position {x: 12, y: 59},
        velocity: Velocity {x: -1, y: 2},
        health: Health(5),
        // Since this field is None, the Animation component will not be added when the entity
        // is created
        animation: None, // Not animated to begin with
    };
    player.create(&mut level1);

    // ...

    // Player needs to move on to the next level
    let mut level2 = World::new();
    // If an Animation component was added between the call to create() and this next call,
    // the field will be set to Some(animation_component) where animation_component is the
    // instance of the Animation component that was added. Otherwise, the field will be None.
    let player_entity = find_player_entity(&level1);
    let player = PlayerComponents::from_world(&level1, player_entity);
    player.create(&mut level2);

    // ...

    // Player needs to go back to previous level
    // The Animation component may have changed/added/removed, but we don't need to worry
    // about that here! The behaviour is the same as above.
    let (_, player) = PlayerComponents::first_from_world(&level2).unwrap();
    let player_entity = find_player_entity(&level1);
    // If the animation field is not None, we will call Storage::insert and add it to the
    // component's storage. Otherwise, we will call Storage::remove and get rid of it.
    player.update(&mut level1, player_entity)?;

    Ok(())
}

Note: The way we match for the Option type is very naive right now. Using Option<YourComponent> as the type of your field will work, but using std::option::Option<YourComponent> will not.

Fetching Multiple Component Group Instances

In the future, when Generic Associated Types (GATs) are implemented, this trait may be updated as follows:

pub trait ComponentGroup: Sized {
    type UpdateError;
    type GroupIter<'a>;

    // Extracts all instances of this group of components from the world.
    fn all_from_world<'a>(world: &'a World) -> Self::GroupIter<'a>;
    // ...other methods...
}

It just isn’t possible to express this as part of the trait right now. Adding this would be a breaking change, so that update would not occur without a new major version being released.

As a workaround, you can add the method yourself using the impl Trait feature:

#[derive(ComponentGroup)]
struct PlayerComponents {
    position: Position,
    velocity: Velocity,
    health: Health,
}

impl PlayerComponents {
    pub fn all_from_world<'a>(world: &'a World) -> impl Iterator<Item=Self> + 'a {
        // ...implement this...
    }
}

fn main() {
    let mut level1 = World::new();
    // ...do stuff...

    for group in PlayerComponents::all_from_world(&level1) {
        // ...do stuff with each group...
    }
}

Generic Component Groups

It is possible to use the ComponentGroup trait and custom derive with generic structs. Just make sure to add Send + Sync + Component + Clone trait bounds to the generic type parameters or you will get a compile error. (The Send + Sync part is required by the specs crate.)

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

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

#[derive(Debug, Clone, Component)]
#[storage(VecStorage)]
pub struct AngularVelocity {deg: f64}

// Don't forget the trait bounds!
#[derive(ComponentGroup)]
struct PlayerComponents<V: Send + Sync + Component + Clone> {
    position: Position,
    velocity: V,
}

// Can use this to provide different component groups that share most of their structure
type RunningPlayer = PlayerComponents<Velocity>;
type SpinningPlayer = PlayerComponents<AngularVelocity>;

Traits

Represents a group of specs::Component fields that can be added or extracted from a specs::World.