by leudz

leudz / shipyard

Entity Component System focused on usability and speed.

209 Stars 16 Forks Last release: 5 months ago (0.4.1) Other 569 Commits 4 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

Shipyard <!-- omit in toc -->

Shipyard is an Entity Component System focused on usability and speed.

LICENSE Documentation Chat

If you have any question or want to follow the development more closely join the Zulip.

There's two big learning resources: - The Tutorial (WIP) for people new to ECS or who prefer to learn by making a project. - The Guide for people that already know how to use an ECS and mostly want to learn Shipyard's syntax.
It also goes into greater depth and provides useful recipes.

Simple Example <!-- omit in toc -->

use shipyard::*;

struct Health(f32); struct Position { _x: f32, _y: f32, }

fn in_acid(positions: View, mut healths: ViewMut) { for (_, mut health) in (&positions, &mut healths) .iter() .filter(|(pos, _)| is_in_acid(pos)) { health.0 -= 1.0; } }

fn is_in_acid(_: &Position) -> bool { // it's wet season true }

fn main() { let world = World::new();
    |mut entities: EntitiesViewMut,
     mut positions: ViewMut<position>,
     mut healths: ViewMut<health>| {
            (&amp;mut positions, &amp;mut healths),
            (Position { _x: 0.0, _y: 0.0 }, Health(1000.0)),


Table of Contents <!-- omit in toc -->

Let there be SparseSets

I initially started to make an ECS to learn how it works. After a failed attempt and some research, I started to work on Shipyard.

Specs was already well established as the go-to Rust ECS but I thought I could do better and went with EnTT core data-structure:


It's extremely flexible and is the core data structure behind Shipyard.
I wouldn't say Shipyard is better or worse than Specs, it's just different.


Systems make it very easy to split your logic in manageable chunks. Shipyard takes the concept quite far.

You always start with a function or closure and almost always take a few views (reference to storage) as arguments.
The basic example shown above does just that:

fn in_acid(positions: View, mut healths: ViewMut) {
    // -- snip --
A function with two views as argument.

Not just storage

The first argument doesn't have to be a view, you can pass any data to a system. You don't even have to own it.

fn in_acid(season: &Season, positions: View, mut healths: ViewMut) {
    // -- snip --

world.run_with_data(in_acid, &season);

You have to provide the data when running the system of course.


Systems can also have a return type, if run directly with

you'll get the returned value right away.
For workloads you can only get back errors.
fn lowest_hp(healths: View) -> EntityId {
    // -- snip --

let entity =;


Just like any function you can add some generics. You'll have to specify them when running the system.

fn in_acid(positions: View>, mut healths: ViewMut) {
    // -- snip --

All at once

You can of course use all of them at the same time.

fn debug(fmt: &mut Formatter, view: View) -> Result {
    // -- snip --

world.run_with_data(debug::, fmt)?;

Unique Storage (Resource)

Unique storages are used to store data you only have once in the

and aren't related to any entity.
fn render(renderer: UniqueView) {
    // -- snip --


!Send and !Sync Components

components can be stored directly in the
and accessed almost just like any other component.
Make sure to add the cargo feature to have access to this functionality.
fn run(rcs: NonSendSync>>) {
    // -- snip --


Workloads make it easy to run multiple systems again and again. They also automatically schedule systems so you don't have borrow error when trying to use multiple threads.

fn in_acid(positions: View, mut healths: ViewMut) {
    // -- snip --

fn tag_dead(entities: EntitiesView, healths: View, mut deads: ViewMut) { for (id, health) in healths.iter().with_id() { if health.0 == 0.0 { entities.add_component(&mut deads, Dead, id); } } }

fn remove_dead(mut all_storages: AllStoragesViewMut) { all_storages.remove_any::(); }

world .add_workload("Rain") .with_system(system!(in_acid)) .with_system(system!(tag_dead)) .with_system(system!(remove_dead)) .build();

world.run_workload("Rain"); world.run_workload("Rain");

The system macro acts as duck tape while waiting for some features in the language, it will disappear as soon as possible.
You can make workloads without it but I strongly recommended to use it.

Cargo Features

  • panic (default) adds panicking functions
  • parallel (default) — adds parallel iterators and dispatch
  • serde1 — adds (de)serialization support with serde
  • non_send — adds methods and types required to work with
  • non_sync — adds methods and types required to work with
  • std (default) — lets shipyard use the standard library


This crate uses

both because sometimes there's no way around it, and for performance gain.
Releases should have all invocation of
If you find places where a safe alternative is possible without repercussion (small ones are sometimes acceptable) please open an issue or a PR.


Licensed under either of

  • Apache License, Version 2.0 (LICENSE-APACHE or
  • MIT license (LICENSE-MIT or

at your option.

module is a fork of
. The original code is licensed under MIT or APACHE-2.0.
The modifications are licensed the same way as the rest of Shipyard's code.


Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.