Welcome to Shipyard!

Shipyard is an Entity Component System focused on usability and speed. ECS is a great way to organize logic and data.

There are two main benefits to using an ECS:

  1. Elegant approach for humans
    • Composition over inheritance
    • Separation of concerns
    • Less burdened by lifetimes
  2. Optimal design for computers
    • Spatial locality
    • Less pointer chasing

However, programming with an ECS requires thinking about data and logic in a different way than you might be used to.

How does it work?

Components hold data. Entities are simple ids used to refer to a group of components.

Systems do the heavy lifting: updating components, running side-effects, and integrating with other parts of the code.

Fundamentals

Getting Started

For most target architectures you can just add it to Cargo.toml.

For single-threaded environments (like WASM) or embedded you'll need to turn off default features and add features back in when needed.

Now that we're ready to use Shipyard, let's learn the basics!

World

World is Shipyard's core data structure: It holds all data and knows how to process systems. All operations originate from one (or more) World.

Creation

You can use new or default:

let world = World::default();
let world = World::new();

There is no need to register components, storages are created on first access.

Views

While some actions are available directly on World, you'll often interact with it through views. They allow access to one or multiple storage.
Storage access follows the same rules as Rust's borrowing: You can have as many shared accesses to a storage as you like or a single exclusive access.

You can request a view using World::borrow, World::run or in workloads (more on this in a later chapter).

For example if you want a shared access to the entities storage you can use borrow:

let world = World::new();

let entities = world.borrow::<EntitiesView>().unwrap();

Add Entity

When an entity is created you will receive a unique handle to it: an EntityId.

World

let mut world = World::new();

let empty_entity = world.add_entity(());
let single_component = world.add_entity((0u32,));
let multiple_components = world.add_entity((0u32, 1usize));

⚠️ We have to use a single element tuple (T,) to add a single component entity.

Views

let world = World::new();

let (mut entities, mut u32s, mut usizes) = world
    .borrow::<(EntitiesViewMut, ViewMut<u32>, ViewMut<usize>)>()
    .unwrap();

let empty_entity = entities.add_entity((), ());
let single_component = entities.add_entity(&mut u32s, 0);
let multiple_components = entities.add_entity((&mut u32s, &mut usizes), (0, 1));

Delete Entity

Deleting an entity deletes it from the entities storage, while also deleting all its components.

World

let mut world = World::new();

let id = world.add_entity((0u32,));

world.delete_entity(id);

View

let world = World::new();

let mut all_storages = world.borrow::<AllStoragesViewMut>().unwrap();

let id = all_storages.add_entity((0u32,));

all_storages.delete_entity(id);

Add Components

An entity can have any number of components but only one in each storage.
Adding another component of the same type will replace the existing one.

World

let mut world = World::new();

let id = world.add_entity(());

world.add_component(id, (0u32,));
world.add_component(id, (0u32, 1usize));

⚠️ We have to use a single element tuple (T,) to add a single component.

View

You'll notice that we use EntitiesView and not EntitiesViewMut to add components.
The entities storage is only used to check if the EntityId is alive.
We could of course use EntitiesViewMut, but exclusive access is not necessary.

If you don't need or want to check if the entity is alive, you can use the AddComponent::add_component_unchecked.

let world = World::new();

let id = world
    .borrow::<EntitiesViewMut>()
    .unwrap()
    .add_entity((), ());

let (entities, mut u32s, mut usizes) = world
    .borrow::<(EntitiesView, ViewMut<u32>, ViewMut<usize>)>()
    .unwrap();

entities.add_component(id, &mut u32s, 0);
entities.add_component(id, (&mut u32s, &mut usizes), (0, 1));
u32s.add_component_unchecked(id, 0);

Remove Components

Removing a component will take it out of the storage and return it.

World

let mut world = World::new();

let id = world.add_entity((0u32, 1usize));

world.remove::<(u32,)>(id);
world.remove::<(u32, usize)>(id);

⚠️ We have to use a single element tuple (T,) to remove a single component entity.

View

We have to import the Remove trait for multiple components.

let world = World::new();

let (mut entities, mut u32s, mut usizes) = world
    .borrow::<(EntitiesViewMut, ViewMut<u32>, ViewMut<usize>)>()
    .unwrap();

let id = entities.add_entity((&mut u32s, &mut usizes), (0, 1));

u32s.remove(id);
(&mut u32s, &mut usizes).remove(id);

Delete Components

Deleting a component will erase it from the storage but will not return it.

World

let mut world = World::new();

let id = world.add_entity((0u32, 1usize));

world.delete_component::<(u32,)>(id);
world.delete_component::<(u32, usize)>(id);

⚠️ We have to use a single element tuple (T,) to delete a single component entity.

All Components

let mut world = World::new();

let id = world.add_entity((0u32, 1usize));

world.strip(id);

View

We have to import the Delete trait for multiple components.

let world = World::new();

let (mut entities, mut u32s, mut usizes) = world
    .borrow::<(EntitiesViewMut, ViewMut<u32>, ViewMut<usize>)>()
    .unwrap();

let id = entities.add_entity((&mut u32s, &mut usizes), (0, 1));

u32s.delete(id);
(&mut u32s, &mut usizes).delete(id);

All Components

let world = World::new();

let mut all_storages = world.borrow::<AllStoragesViewMut>().unwrap();

let id = all_storages.add_entity((0u32, 1usize));

all_storages.strip(id);

Get and Modify Components

To access or update components you can use Get::get. It'll work with both shared and exclusive views.

let mut world = World::new();

let id = world.add_entity((0u32, 1usize));

let (mut u32s, mut usizes) = world.borrow::<(ViewMut<u32>, ViewMut<usize>)>().unwrap();

*(&mut usizes).get(id).unwrap() += 1;

let (mut i, j) = (&mut u32s, &usizes).get(id).unwrap();
*i += *j as u32;

u32s[id] += 1;

When using a single view, if you are certain an entity has the desired component, you can access it via index.

Fast Get

Using get with &mut ViewMut<T> will return Mut<T>. This struct helps fine track component modification.
FastGet::fast_get can be used to opt out of this fine tracking and get back &mut T.

Iterators

Iteration is one of the most important features of an ECS.

In Shipyard this is achieved using IntoIter::iter on views.

let world = World::new();

let (mut u32s, usizes) = world.borrow::<(ViewMut<u32>, View<usize>)>().unwrap();

for i in u32s.iter() {
    dbg!(i);
}

for (mut i, j) in (&mut u32s, &usizes).iter() {
    *i += *j as u32;
}

You can use views in any order. However, using the same combination of views in different positions might yield components in a different order.
You shouldn't expect specific ordering from Shipyard's iterators in general.

With Id

You can ask an iterator to tell you which entity owns each component by using WithId::with_id:

let world = World::new();

let u32s = world.borrow::<View<u32>>().unwrap();

for (id, i) in u32s.iter().with_id() {
    println!("{} belongs to entity {:?}", i, id);
}

Not

It's possible to filter entities that don't have a certain component using Not by adding ! in front of the view reference.

let world = World::new();

let (u32s, usizes) = world.borrow::<(View<u32>, View<usize>)>().unwrap();

for (i, _) in (&u32s, !&usizes).iter() {
    dbg!(i);
}

Uniques

Uniques (a.k.a. resources) are useful when you know there will only ever be a single instance of some component.
In that case there is no need to attach the component to an entity. It also works well as global data while still being safe.

As opposed to other storages, uniques have to be initialized with add_unique. We can then access it with UniqueView and UniqueViewMut.

let world = World::new();

world.add_unique(Camera::new()).unwrap();

world
    .run(|camera: UniqueView<Camera>| {
        // -- snip --
    })
    .unwrap();

Systems

Systems are a great way to organize code.
A function with views as arguments is all you need.

Here's an example:

fn create_ints(mut entities: EntitiesViewMut, mut u32s: ViewMut<u32>) {
    // -- snip --
}

We have a system, let's run it!

let world = World::new();

world.run(create_ints).unwrap();

It also works with closures.

Passing Data to Systems

The first argument doesn't have to be a view, you can pass any data, even references.

fn in_acid(season: Season, positions: View<Position>, mut healths: ViewMut<Health>) {
    // -- snip --
}

let world = World::new();

world.run_with_data(in_acid, Season::Spring).unwrap();

We call run_with_data instead of run when we want to pass data to a system.

If you want to pass multiple variables, you can use a tuple.

fn in_acid(
    (season, precipitation): (Season, Precipitation),
    positions: View<Position>,
    mut healths: ViewMut<Health>,
) {
    // -- snip --
}

let world = World::new();

world
    .run_with_data(in_acid, (Season::Spring, Precipitation(0.1)))
    .unwrap();

Workloads

A workload is a named group of systems.

fn create_ints(mut entities: EntitiesViewMut, mut u32s: ViewMut<u32>) {
    // -- snip --
}

fn delete_ints(mut u32s: ViewMut<u32>) {
    // -- snip --
}

let world = World::new();

Workload::builder("Int cycle")
    .with_system(&create_ints)
    .with_system(&delete_ints)
    .add_to_world(&world)
    .unwrap();

world.run_workload("Int cycle").unwrap();

Workloads are stored in the World, ready to be run again and again.
They don't take up much memory so even if you make a few with similar systems it's not a problem.

Workloads will run their systems first to last or at the same time when possible. We call this outer-parallelism, you can learn more about it in this chapter.

Workload Nesting

You can also add a workload to another and build your execution logic brick by brick.

struct Dead<T>(core::marker::PhantomData<T>);

fn increment(mut u32s: ViewMut<u32>) {
    for mut i in (&mut u32s).iter() {
        *i += 1;
    }
}

fn flag_deleted_u32s(u32s: View<u32>, mut deads: ViewMut<Dead<u32>>) {
    for (id, i) in u32s.iter().with_id() {
        if *i > 100 {
            deads.add_component_unchecked(id, Dead(core::marker::PhantomData));
        }
    }
}

fn clear_deleted_u32s(mut all_storages: AllStoragesViewMut) {
    all_storages.delete_any::<SparseSet<Dead<u32>>>();
}

let world = World::new();

Workload::builder("Filter u32")
    .with_system(&flag_deleted_u32s)
    .with_system(&clear_deleted_u32s)
    .add_to_world(&world)
    .unwrap();

Workload::builder("Loop")
    .with_system(&increment)
    .with_workload("Filter u32")
    .add_to_world(&world)
    .unwrap();

world.run_workload("Loop").unwrap();

Congratulations, you made it to the end of the fundamentals!
The next section will take you under the hood to learn how to get the most out of Shipyard.

Going Further

This section covers more advanced topics. Topics include parallelism, and how everything behaves so you can avoid surprises.

Sparse Set

SparseSet is Shipyard's default storage. This chapter explains the basics of how it works, the actual implementation is more optimized both in term of speed and memory.

Overview

SparseSet is made of three arrays:

  • sparse contains indices to the dense and data arrays
  • dense contains EntityId
  • data contains the actual components

dense and data always have the same length, the number of components present in the storage.
sparse on the other hand can be as big as the total number of entities created.

Let's look at an example:

let mut world = World::new();

let entity0 = world.add_entity((0u32,));
let entity1 = world.add_entity((10.0f32,));
let entity2 = world.add_entity((20u32,));

The World starts out empty, when we add 0u32 a SparseSet<u32> will be generated.

At the end of the example we have:

SparseSet<u32>:
    sparse: [0, dead, 1]
    dense:  [0, 2]
    data:   [0, 20]

SparseSet<f32>:
    sparse: [dead, 0]
    dense:  [1]
    data:   [10.0]

You can see that SparseSet<u32>'s sparse contains three elements but dense does not.
Note also that both sparse don't contains the same number of elements. As far as SparseSet<f32> knowns entity2 might not exist.

Removal

Removing is done by swap removing from both dense and data and updating sparse in consequence.

Continuing the previous example if we call:

world.remove::<(u32,)>(entity0);

The internal representation now looks like this:

sparse: [dead, dead, 0]
dense: [2]
data: [20]

dense and data shifted to the left, sparse's first element is now dead and the third element is now 0 to follow dense's shift.

Iteration

Iterating one or several SparseSet is different. With a single SparseSet it's as simple as iterating data.
To iterate multiple SparseSets the smallest will be chosen as "lead". We then iterate its dense array and for each entity we check all the other SparseSets to see if they also contain a component for this entity.

Parallelism

By late 90s - early 2000s, CPUs started to get too close to the physical limitation of transistors and manufacturers couldn't "just" make their product faster. The solution: more cores.

Nowadays almost all devices come with multiple cores, it would be a shame to use just one.

In ECS there's two big ways to split work across cores: running systems on separate threads or using a parallel iterator, we call these two methods "outer-parallelism" and "inner-parallelism," respectively.

Outer-parallelism

We'll start by the simplest one to use. So simple that there's nothing to do, workloads handle all the work for you. We even almost used multiple threads in the Systems chapter.

As long as the "parallel" feature is set (enabled by default) workloads will try to execute systems as much in parallel as possible. There is a set of rules that defines the "possible":

  • Systems accessing AllStorages stop all threading.
  • There can't be any other access during an exclusive access, so ViewMut<T> will block T threading.

When you make a workload, all systems in it will be checked and batches (groups of systems that don't conflict) will be created.
add_to_world returns information about these batches and why each system didn't get into the previous batch.

Inner-parallelism

While parallel iterators does require us to modify our code, it's just a matter of using par_iter instead of iter.
Don't forget to import rayon. par_iter returns a ParallelIterator.

Example:

use rayon::prelude::*;

fn many_u32s(mut u32s: ViewMut<u32>) {
    u32s.par_iter().for_each(|i| {
        // -- snip --
    });
}

Don't replace all your iter method calls just yet, however! Using a parallel iterator comes with an upfront overhead cost. It will only exceed the speed of its sequential counterpart on storages large enough to make up for the overhead cost in improved processing efficiency.

!Send and !Sync Components

World can store !Send and/or !Sync components once the thread_local feature is set but they come with limitations:

  • !Send storages can only be added in World's thread.
  • Send + !Sync components can only be accessed from one thread at a time.
  • !Send + Sync components can only be accessed immutably from other threads.
  • !Send + !Sync components can only be accessed in the thread they were added in.

These storages are accessed with NonSend, NonSync and NonSendSync, for example:

fn run(rcs_usize: NonSendSync<View<Rc<usize>>>, rc_u32: NonSendSync<UniqueView<Rc<u32>>>) {}

Performance Tips

List of small information to get the most out of Shipyard.

for_each

for ... in desugars to calling next repeatedly, the compiler can sometimes optimize it very well.
If you don't want to take any chance prefer calling for_each instead.

borrow / run in a loop

While borrowing storages is quite cheap, doing so in a loop is generally a bad idea.
Prefer moving the loop inside run and move borrow's call outside the loop.

bulk_add_entity

When creating many entities at the same time remember to call bulk_add_entity if possible.

Recipes

Cool patterns you may be interested in.

Building an Entity Hierarchy with Shipyard

Hierarchies are a very commonly used organizational structure in game development. An important example is a transform hierarchy: child entities move along with their parents.

How can we build such a hierarchy of entities in shipyard?

One method is to use a secondary data structure which represents the hierarchy.

But an ECS already has all the means to store data: components. So let's use them!

Below you won't find a ready-to-use solution, rather some hints on how to start with your own hierarchy implementation, tailored to your requirements.

Parents and Children

Think about the different roles an entity can take in a hierarchy. It can be:

  • a parent (root node),
  • a parent and a child (intermediate node),
  • a child (leaf node).

From this we can derive two simple, composable component types:

A Parent component stores the number of its children and the first child:

struct Parent {
    num_children: usize,
    first_child: EntityId,
}

A Child component links to its parent as well as neighbor siblings:

struct Child {
    parent: EntityId,
    prev: EntityId,
    next: EntityId,
}

As you can see, we simply store EntityIds to refer to other entities inside a component.

Note that Options are completely avoided by making the sibling chain circular:

  • Last child's next points to the first child.
  • First child's prev points to the last child.

Our entire hierarchy structure resides only in Parent and Child components – nice!

But it'd be a hassle to create them manually each time you want to insert an entity into the tree.

Let's make it convenient

We begin with two useful methods in a trait declaration:

trait Hierarchy {
    // Removes the child status of an entity.
    fn detach(&mut self, id: EntityId);

    // Attaches an entity as a child to a given parent entity.
    fn attach(&mut self, id: EntityId, parent: EntityId);
}

With these, you'll be able to not only insert new entities into the tree but also move a whole subtree – a child with all its descendants – to another parent.

Since we need access to EntitiesViewMut as well as our hierarchy component storages, we implement the Hierarchy trait for the type (EntitiesViewMut<'_>, ViewMut<'_, Parent>, ViewMut<'_, Child>).

fn detach(&mut self, id: EntityId) {
    let (_, parents, children) = self;
    // remove the Child component - if nonexistent, do nothing
    if let Some(child) = children.remove(id) {
        // retrieve and update Parent component from ancestor
        let parent = &mut parents[child.parent];
        parent.num_children -= 1;

        if parent.num_children == 0 {
            // if the number of children is zero, the Parent component must be removed
            parents.remove(child.parent);
        } else {
            // the ancestor still has children, and we have to change some linking
            // check if we have to change first_child
            if parent.first_child == id {
                parent.first_child = child.next;
            }
            // remove the detached child from the sibling chain
            children[child.prev].next = child.next;
            children[child.next].prev = child.prev;
        }
    }
}

Before we move on to attach, let's make some observations.

We use indexing on parents and children but if the entity doesn't have the component it'll unwrap.

We don't have to worry as long as we only use the methods in our Hierarchy trait.

If you accidentally delete hierarchy components in other places without changing the linking, things will go fatally wrong. If you want to catch these errors you might want to use get and handle the error (for example with expect).

attach looks like this:

fn attach(&mut self, id: EntityId, parent: EntityId) {
    // the entity we want to attach might already be attached to another parent
    self.detach(id);

    let (entities, parents, children) = self;

    // either the designated parent already has a Parent component – and thus one or more children
    if let Ok(p) = parents.get(parent) {
        // increase the parent's children counter
        p.num_children += 1;

        // get the ids of the new previous and next siblings of our new child
        let prev = children[p.first_child].prev;
        let next = p.first_child;

        // change the linking
        children[prev].next = id;
        children[next].prev = id;

        // add the Child component to the new entity
        entities.add_component(id, children, Child { parent, prev, next });
    } else {
        // in this case our designated parent is missing a Parent component
        // we don't need to change any links, just insert both components
        entities.add_component(
            id,
            children,
            Child {
                parent,
                prev: id,
                next: id,
            },
        );
        entities.add_component(
            parent,
            parents,
            Parent {
                num_children: 1,
                first_child: id,
            },
        );
    }
}

We can now add another handy method to our trait:

// Creates a new entity and attaches it to the given parent.
fn attach_new(&mut self, parent: EntityId) -> EntityId;`
fn attach_new(&mut self, parent: EntityId) -> EntityId {
    let id = self.0.add_entity((), ());
    self.attach(id, parent);
    id
}

And lastly a simple usage example:

let world = World::new();

let mut hierarchy = world.borrow::<(EntitiesViewMut, ViewMut<Parent>, ViewMut<Child>)>().unwrap();

let root1 = hierarchy.0.add_entity((), ());
let root2 = hierarchy.0.add_entity((), ());

let e1 = hierarchy.attach_new(root1);
let _e2 = hierarchy.attach_new(e1);
let e3 = hierarchy.attach_new(e1);
let _e4 = hierarchy.attach_new(e3);

hierarchy.attach(e3, root2);

Traversing the hierarchy

There are different ways the hierarchy can be queried.

For example, we may want to know the parent of a given entity. Doing this is simply done by inspecting its child component - if there is one.

However, sometimes you might need

  • all children,
  • all ancestors,
  • or all descendants of a given entity.

A perfect use case for iterators! An iterator has to implement the next method from the Iterator trait.

We start with a ChildrenIter, which is pretty straightforward:

struct ChildrenIter<C> {
    get_child: C,
    cursor: (EntityId, usize),
}

impl<'a, C> Iterator for ChildrenIter<C>
where
    C: Get<Out = &'a Child> + Copy,
{
    type Item = EntityId;

    fn next(&mut self) -> Option<Self::Item> {
        if self.cursor.1 > 0 {
            self.cursor.1 -= 1;
            let ret = self.cursor.0;
            self.cursor.0 = self.get_child.get(self.cursor.0).unwrap().next;
            Some(ret)
        } else {
            None
        }
    }
}

Note that we don't implement Iterator for ViewMut<Child> directly, but for a type that implements the GetComponent trait. This way, our iterator can be used with View as well as ViewMut.

The next one is the AncestorIter:

struct AncestorIter<C> {
    get_child: C,
    cursor: EntityId,
}

impl<'a, C> Iterator for AncestorIter<C>
where
    C: Get<Out = &'a Child> + Copy,
{
    type Item = EntityId;

    fn next(&mut self) -> Option<Self::Item> {
        self.get_child.get(self.cursor).ok().map(|child| {
            self.cursor = child.parent;
            child.parent
        })
    }
}

Easy.

DescendantIter will be a bit more complicated. We choose to implement a depth-first variant using recursion.

It is based on the code for the ChildrenIter but comes with an additional stack to keep track of the current level the cursor is in:

  • Push a new level to the stack if we encounter a Parent component.
  • Pop the last level from the stack whenever we run out of siblings, then carry on where we left off.
struct DescendantsIter<P, C> {
    get_parent: P,
    get_child: C,
    cursors: Vec<(EntityId, usize)>,
}

impl<'a, P, C> Iterator for DescendantsIter<P, C>
where
    P: Get<Out = &'a Parent> + Copy,
    C: Get<Out = &'a Child> + Copy,
{
    type Item = EntityId;

    fn next(&mut self) -> Option<Self::Item> {
        if let Some(cursor) = self.cursors.last_mut() {
            if cursor.1 > 0 {
                cursor.1 -= 1;
                let ret = cursor.0;
                cursor.0 = self.get_child.get(cursor.0).unwrap().next;
                if let Ok(parent) = self.get_parent.get(ret) {
                    self.cursors.push((parent.first_child, parent.num_children));
                }
                Some(ret)
            } else {
                self.cursors.pop();
                self.next()
            }
        } else {
            None
        }
    }
}

What we still need to do is to implement a simple trait with methods that return nicely initialized *Iter structs for us:

trait HierarchyIter<'a, P, C> {
    fn ancestors(&self, id: EntityId) -> AncestorIter<C>;
    fn children(&self, id: EntityId) -> ChildrenIter<C>;
    fn descendants(&self, id: EntityId) -> DescendantsIter<P, C>;
}

impl<'a, P, C> HierarchyIter<'a, P, C> for (P, C)
where
    P: Get<Out = &'a Parent> + Copy,
    C: Get<Out = &'a Child> + Copy,
{
    fn ancestors(&self, id: EntityId) -> AncestorIter<C> {
        let (_, children) = self;

        AncestorIter {
            get_child: *children,
            cursor: id,
        }
    }

    fn children(&self, id: EntityId) -> ChildrenIter<C> {
        let (parents, children) = self;

        ChildrenIter {
            get_child: *children,
            cursor: parents
                .get(id)
                .map_or((id, 0), |parent| (parent.first_child, parent.num_children)),
        }
    }

    fn descendants(&self, id: EntityId) -> DescendantsIter<P, C> {
        let (parents, children) = self;

        DescendantsIter {
            get_parent: *parents,
            get_child: *children,
            cursors: parents.get(id).map_or_else(
                |_| Vec::new(),
                |parent| vec![(parent.first_child, parent.num_children)],
            ),
        }
    }
}

Cool. Let's extend the former usage example into a little test.

#[test]
fn test_hierarchy() {
    let world = World::new();

    let mut hierarchy = world.borrow::<(EntitiesViewMut, ViewMut<Parent>, ViewMut<Child>)>().unwrap();

    let root1 = hierarchy.0.add_entity((), ());
    let root2 = hierarchy.0.add_entity((), ());

    let e1 = hierarchy.attach_new(root1);
    let e2 = hierarchy.attach_new(e1);
    let e3 = hierarchy.attach_new(e1);
    let e4 = hierarchy.attach_new(e3);

    hierarchy.attach(e3, root2);

    let e5 = hierarchy.attach_new(e3);

    assert!((&hierarchy.1, &hierarchy.2)
        .children(e3)
        .eq([e4, e5].iter().cloned()));

    assert!((&hierarchy.1, &hierarchy.2)
        .ancestors(e4)
        .eq([e3, root2].iter().cloned()));

    assert!((&hierarchy.1, &hierarchy.2)
        .descendants(root1)
        .eq([e1, e2].iter().cloned()));

    assert!((&hierarchy.1, &hierarchy.2)
        .descendants(root2)
        .eq([e3, e4, e5].iter().cloned()));
}

Removing entities from the hierarchy

Removing an entity from the hierarchy means removing its Parent and Child components.

To remove an entity's Child component, we can simply reuse detach. Removing its Parent component must be done with caution. This entity's children now become orphans – we have to detach them as well.

Both methods can be added to our Hierarchy trait:

fn remove(&mut self, id: EntityId) {
    self.detach(id);

    let children = (&self.1, &self.2).children(id).collect::<Vec<_>>();
    for child_id in children {
        self.detach(child_id);
    }
    self.1.remove(id);
}

A method that removes a whole subtree is easy to write by making use of recursion again:

fn remove_all(&mut self, id: EntityId) {
    let (_, parents, children) = self;

    for child_id in (&*parents, &*children).children(id).collect::<Vec<_>>() {
        self.remove_all(child_id);
    }
    self.remove(id);
}

That's it! We can now add the following code to the end of our test from the last chapter:

hierarchy.detach(e1);

assert!((&hierarchy.1, &hierarchy.2).descendants(root1).eq(None));
assert!((&hierarchy.1, &hierarchy.2).ancestors(e1).eq(None));
assert!((&hierarchy.1, &hierarchy.2).children(e1).eq([e2].iter().cloned()));

hierarchy.remove(e1);

assert!((&hierarchy.1, &hierarchy.2).children(e1).eq(None));

hierarchy.remove_all(root2);

assert!((&hierarchy.1, &hierarchy.2).descendants(root2).eq(None));
assert!((&hierarchy.1, &hierarchy.2).descendants(e3).eq(None));
assert!((&hierarchy.1, &hierarchy.2).ancestors(e5).eq(None));

Sorting

The order between siblings may or may not play a role in your project.

However, a simple sorting for children can be done in two steps:

  • Collect all children into a Vec and sort it.
  • Adjust the linking in the Child components according to the sorted list.

We can add this method to the Hierarchy trait:

fn sort_children_by<F>(&mut self, id: EntityId, compare: F)
where
    F: FnMut(&EntityId, &EntityId) -> std::cmp::Ordering,
{
    let (_, parents, children_storage) = self;

    let mut children = (&*parents, &*children_storage)
        .children(id)
        .collect::<Vec<EntityId>>();
    if children.len() > 1 {
        children.sort_by(compare);
        // set first_child in Parent component
        parents[id].first_child = children[0];
        // loop through children and relink them
        for i in 0..children.len() - 1 {
            children_storage[children[i]].next = children[i + 1];
            children_storage[children[i + 1]].prev = children[i];
        }
        children_storage[children[0]].prev = *children.last().unwrap();
        children_storage[*children.last().unwrap()].next = children[0];
    }
}

Again a small test demonstrates the usage:

#[test]
fn test_sorting() {
    let world = World::new();

    let (mut hierarchy, mut usizes) = world.borrow::<(
        (EntitiesViewMut, ViewMut<Parent>, ViewMut<Child>),
        ViewMut<usize>,
    )>().unwrap();

    let root = hierarchy.0.add_entity((), ());

    let e0 = hierarchy.attach_new(root);
    let e1 = hierarchy.attach_new(root);
    let e2 = hierarchy.attach_new(root);
    let e3 = hierarchy.attach_new(root);
    let e4 = hierarchy.attach_new(root);

    hierarchy.0.add_component(e0, &mut usizes, 7);
    hierarchy.0.add_component(e1, &mut usizes, 5);
    hierarchy.0.add_component(e2, &mut usizes, 6);
    hierarchy.0.add_component(e3, &mut usizes, 1);
    hierarchy.0.add_component(e4, &mut usizes, 3);

    assert!((&hierarchy.1, &hierarchy.2)
        .children(root)
        .eq([e0, e1, e2, e3, e4].iter().cloned()));

    hierarchy.sort_children_by(root, |a, b| usizes[*a].cmp(&usizes[*b]));

    assert!((&hierarchy.1, &hierarchy.2)
        .children(root)
        .eq([e3, e4, e1, e2, e0].iter().cloned()));
}

Do it yourself!

We recommend that you build your own hierarchy system fitted to your specific needs. In deviation of the above code examples you may want:

  • a single hierarchy component instead of two,
  • breadth-first instead of depth-first traversal,
  • different sorting methods,
  • etc.

Further reading

These notes are based on ideas presented in a highly recommended article by skypjack: ECS back and forth.

If you're working with Seed, @MartinKavik ported the bunny demo to it. You can find the source here.

0.4 comes with a few big changes, this chapter aims to facilitate the transition.

Imports

Let's start small, prelude and internal no longer exist, you just have to replace all shipyard::prelude and shipyard::internal by shipyard.

Systems

Following an issue opened by @cart, systems will become functions instead of an instance of the System trait.

To make this work, borrowing is now done with the actual types you get when you borrow a storage instead of using references.

In 0.3:

struct MySystem;
impl<'sys> System<'sys> for MySystem {
    type Data = (
        EntitiesMut,
        &mut usize,
    );
    fn run((mut entities, mut usizes): <Self::Data as SystemData<'sys>>::View) {}
}

// or with the macro

#[system(MySystem)]
fn run(mut entities: &mut Entities, mut usizes: &mut usize) {}

In 0.4:

fn my_system(mut entities: EntitiesViewMut, mut usizes: ViewMut<usize>) {}

This change also affects run and borrow.
World::run_system is no longer needed and you can run systems with run directly.

world.run(my_system);

// and closures still work

world.run(|mut entities: EntitiesViewMut, mut usizes: ViewMut<usize>| {});

run has the same return type as the system or closure and it doesn't require any tuple most of the time.

Here's the complete list:

0.30.4
AllStorages / &mut AllStoragesAllStoragesViewMut
Entities / &EntitiesEntitiesView
EntitiesMut / &mut EntitiesEntitiesViewMut
&TView<T>
&mut TViewMut<T>
ThreadPool / &ThreadPoolThreadPoolView
Unique<&T>UniqueView<T>
Unique<&mut T>UniqueViewMut<T>
NonSend<&T>NonSend<View<T>>
NonSend<&mut T>NonSend<ViewMut<T>>
Unique<NonSend<&T>>NonSend<UniqueView<T>>
Unique<NonSend<&mut T>>NonSend<UniqueViewMut<T>>
FakeBorrow<T>FakeBorrow<T>

NonSync and NonSendSync follow the same pattern as NonSend.

Macro

The system proc macro doesn't exist anymore. With the new system design the advantage it provides are not great enough to justify it.

Workloads

The ugly

Workloads are the only one suffering a downgrade. You'll have to give all systems twice to the function plus a few things.

In 0.3:

world.add_workload<(Sys1, Sys2), _>("Workload1");

In 0.4:

world
    .add_workload("Workload1")
    .with_system((
        |world: &World| world.try_run(sys1),
        sys1
    ))
    .with_system((
        |world: &World| world.try_run(sys2),
        sys2
    ))
    .build();

// with a macro

world
    .add_workload("Workload1")
    .with_system(system!(sys1))
    .with_system(system!(sys2))
    .build();

⚠️ The two arguments are wrapped in a tuple.

This repetition will disappear someday but I don't expect it to be soon.
You don't have to use a closure, any function with &World as argument and returning Result<(), shipyard::error::Run> are valid.
It's very important to pass the same function twice, the workload might always error if this isn't the case.

The good

Workloads don't come with only a downgrade. You can now return errors from systems inside workloads.

#[derive(Debug)]
struct TerribleError;

impl Display for TerribleError {
    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), core::fmt::Error> {
        Debug::fmt(self, fmt)
    }
}
impl Error for TerribleError {}

fn my_sys(mut entities: EntitiesViewMut) -> Result<(), TerribleError> {
    Err(TerribleError)
}

fn main() {
    use shipyard::error::{Run, RunWorkload};

    let world = World::new();
    world
        .add_workload("May fail")
        .with_system((
            |world: &World| world.try_run(my_sys)?.map_err(Run::from_custom),
            my_sys,
        ))
        .build();
    match world.try_run_default().map_err(RunWorkload::custom_error) {
        Err(Some(error)) => {
            assert!(error.is::<TerribleError>());
        }
        _ => {}
    }
}

The error has to be anonymized so you'll get back a Box<dyn Error + Send> with std and a Box<dyn Any + Send> with no_std.
Workloads stop at the first error encountered, just like 0.3.
You can also use the try_system! macro the same way as system!.

world
    .add_workload("May fail")
    .with_system(try_system!(my_sys))
    .build();

It'll generate the same code as above.

Iterator

You can now use std::iter::Iterator and for loop with views without having to call into_iter.
All iteration code from 0.3 will still work.

fn my_sys((mut usizes, u32s): (ViewMut<usize>, View<u32>)) {
    for (i, &j) in (&mut usizes, &u32s).iter() {
        *i += j as usize;
    }
}

Get

The GetComponent trait has been renamed Get.

What follows is only true for 0.4. 0.5 went back to get returning a Result.
Get::get has been renamed try_get and a new get method has been added to unwrap errors.

If you used get followed by unwrap, you can simply remove the unwrap.
If you used another error handling method you'll have to replace get by try_get.

Pilgrimage

Links and information not directly related to shipyard.

More Resources

Packs, the whole series is a good read
Timothy Ford's GDC talk on ECS usage in Overwatch
Catherine West's closing keynote on using the ECS pattern in Rust
Sander Mertens's ECS FAQ
FSM in ECS
Todo MVC using ECS

Shipyard Related Crates

List of crate built on top of Shipyard.
Don't hesitate to contact me if your crate uses Shipyard (it doesn't have to be prefixed with "shipyard").

Projects using Shipyard

List of project using Shipyard.
Don't hesitate to contact me to get your project listed.

  • Almetica is a server for the MMORPG TERA.
  • Guacamole Runner by @BoxyUwU is a small game where the player is constantly falling and must jump off planes to stay in the air. When they go over the top of the dirt tiles they plant flowers which gives them points.

Guide Contributors

dakom - David Komer
eldyer