Open-source project
leudz/shipyard avatar
leudz/shipyard

Shipyard: a Rust ECS that borrows its data layout from EnTT

Entity Component System focused on usability and flexibility.

892 stars61 forksRustNOASSERTION

At a glance

What is it?
Shipyard is an entity component system for Rust built on the SparseSet data structure and grouping model taken from EnTT, with parallel iterators, a workload threading scheduler, a derive macro for components, and optional support for serde, tracing and non-Send components. The name comes from assembly lines, and the design goal is parallel workloads that do not cross paths.
Who is it for?
Shipyard suits a Rust game developer who wants an ECS with a flexible component storage model and real parallelism, and who is comfortable with the trade-offs that a 0.x library with a moving API implies. It does not suit a project that needs a stable published API today, since the releases are still 0.x and the guide has separate master and 0.11 versions, and it does not suit a project that cannot use the standard library.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 18 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The basic example is one system and one world

The smallest complete program shows the shape of the library. You derive Component for your data types, write a system as a function taking views over components, and run it against a World:

rust
use shipyard::{Component, IntoIter, View, ViewMut, World};

#[derive(Component)]
struct Health(u32);
#[derive(Component)]
struct Position {
    x: f32,
    y: f32,
}

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

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

fn main() {
    let mut world = World::new();

    world.add_entity((Position { x: 0.0, y: 0.0 }, Health(1000)));

    world.run(in_acid);
}

The important detail is in the function signature. A system takes View for the components it reads and ViewMut for the ones it writes, and the borrows are declared by the types rather than by the caller, which is what lets the framework know the access pattern and schedule accordingly. The two views are then zipped, so the iteration walks the entities that have both components, and a filter narrows it further. The comment in the example is the joke that explains the setup: the acid is everywhere, so every entity in the world loses health.

A borrowed view, and the iterators that come with it

The View and ViewMut types are the mechanism that makes the whole thing work, and their distinction is not a formality. A View is a read borrow of the storage for one component type, and a ViewMut is a write borrow, and a system asking for both is stating that it will read one and write the other. The pairing in the example uses a two-view iterator, and the crate exports an IntoIter trait as part of that machinery, since iterating over a combination of views is where the interesting work happens. When two systems want conflicting access, for example both wanting to write the same component, the borrow system has to arbitrate, and that is exactly the information the framework needs in order to decide what can run in parallel. This is the design where the type signature is the schedule, rather than a separate declaration of which systems conflict.

Workload threading and parallel iterators, on by default

The parallel feature is enabled by default and it is what the name is pointing at. The feature enables workload threading and adds parallel iterators, and the parallel feature in the package manifest pulls in rayon for the work, the parallel support of the proc macro crate, and the rayon support of hashbrown. The name explanation in the README makes the intent concrete: assembly lines take input, process it at each step, and output a result, and you can have multiple lines working in parallel as long as they don't cross paths. The Venetian Arsenal is the historical example, and it could output a fully finished ship every day. A shipyard, then, is a library for building highly parallel software, and the constraint that lines do not cross paths is exactly the constraint the borrow system enforces. The alternative Cargo feature for the same goal is thread_local, which adds the methods and types needed to work with components that are neither Send nor Sync, which is what you need when a component holds something thread-bound.

Eight Cargo features, three of them on by default

The feature list is where the practical decisions live. The defaults are parallel, proc and std, and std is what lets Shipyard use the standard library at all, so a no_std target means turning that off, which is also visible in the manifest categories, which list no-std among the crate's categories. The proc feature re-exports the macros from the separate shipyard_proc crate, mainly so you can derive Component, and the crate is fetched from git rather than crates.io, which is worth knowing if you vendor dependencies. extended_tuple extends the implementations from the default 10 to 32 in tuple size at the cost of four times the build time, which is the trade you make when a system needs to touch more than ten component types at once. serde1 adds serialization support, tracing reports workload and system execution, and there is a memory_constrained feature that is not in the README list but appears in the manifest: it lowers the SparseArray page size from 4KiB to 256B, with a negative impact on iteration of around 5 to 10 percent. The full list of features is also what the docs.rs configuration builds, since it is set to all-features with a cfg for the docsrs build.

The data model is EnTT's, and that was a deliberate choice

The motivation section is short and unusually candid about where the design came from. The author wanted to make an ECS to learn how they work, and after a failed attempt and some research started on Shipyard. Specs was already well established as the go-to Rust ECS, but the author thought they could do better and went with EnTT's core data structure, the SparseSet, and its grouping model, calling it a very flexible combo. That decision has consequences for anyone porting code or choosing between libraries. A SparseSet gives you dense storage of the components that exist plus a sparse mapping from entity id to index, which is what makes iteration over the present components cache friendly, and the grouping model is what lets you ask for the components of a set of entities at once. The practical trade is that you are adopting EnTT's mental model along with the crate, so a project that later wants to move to EnTT itself has less to unlearn.

Two demos, a guide and a visualizer in the same repository

The repository carries more than the library. There is a guide directory, built with mdbook, with a master version and a versioned guide for 0.11, so you can read the documentation for the release you actually depend on rather than only the moving target. There are two runnable demos. The bunny demo is the one referred to in the README as the Bunny Mark, and the square eater is a small game inspired by Erik Hazzard's Rectangle Eater, published as a Play Online link with the source in square_eater/src/main.rs. The square eater builds to WebAssembly, targeting wasm32-unknown-unknown, and the justfile has a target that moves the resulting .wasm file into the square_eater public directory before the game is served, so the demo runs in a browser rather than on your machine. There is also a visualizer, served with trunk, which suggests the crate has a web-based inspection tool for the world state. The workspace members list the demos, the proc crate, and the visualizer as separate crates, with the library itself at the root.

The justfile shows how the project is actually tested

The justfile is the clearest statement of what the maintainer considers done, and it is a demanding list. The test target chains eight things: fmt, check-no-default, test-all, doc, working_miri, clippy, clean, and a check that the no-default-features build compiles. test-all runs cargo test with all features, check-no-default runs cargo check with no default features so the no_std path stays alive, and doc runs cargo doc with all features and no dependencies, with warnings promoted to errors through the environment variable. The clippy target does the same with cargo clippy. The Miri target is the interesting one, because it runs the test suite under the undefined behaviour detector, and the file carries a note explaining a workaround: nightly Rust is making big changes and currently cannot compile shipyard, so a working version is pinned to a specific nightly date, with a link to the upstream Rust issue. Running the tests under Miri with the no-default-features build and only the std feature is a level of care you do not see in most library repositories.

Dual licensed, on a moving 0.x version line

The licence is the standard dual arrangement: Apache License version 2.0 or the MIT license, at your option, with the two licence files in the repository root and the manifest recording the same choice as MIT OR Apache-2.0. The contributing section states the same thing for contributions, that unless you explicitly state otherwise any contribution is dual licensed under the same terms with no additional conditions. The repository also carries a release-check-list.md file, a justfile publish target that shells out to cargo release with a version argument, and a release-check-list that a maintainer works through before tagging. The versioning tells you where the project is. The crate version is 0.11.0, the tagged releases are Shipyard 0.11, 0.10 and 0.8, and the guide is published per version, with the master guide on one URL and the 0.11 guide on another. The last push was on 2026-10-04, so the work is current, but a 0.x library with a versioned guide is a library whose API you should expect to move, and whose changelog you should read before upgrading.

Editorial conclusion

Shipyard suits a Rust game developer who wants an ECS with a flexible component storage model and real parallelism, and who is comfortable with the trade-offs that a 0.x library with a moving API implies. It does not suit a project that needs a stable published API today, since the releases are still 0.x and the guide has separate master and 0.11 versions, and it does not suit a project that cannot use the standard library. Before adopting it, read the Cargo features, because the defaults enable parallelism, the derive macro and std, and the extended_tuple and thread_local features are what you switch on when your component count or your thread requirements demand it.

Frequently asked questions

What is Shipyard and what data model does it use?

It is an entity component system for Rust, focused on usability and speed. The author went with EnTT's core data structure, the SparseSet, and its grouping model, and describes the combination as very flexible.

How do I write a system in Shipyard?

A system is a function that takes a View for the components it reads and a ViewMut for the ones it writes. The two views are zipped and iterated, and the borrows declared in the signature are what let the framework schedule systems in parallel without conflicting access.

Which Cargo features does Shipyard have and which are on by default?

The defaults are parallel, which enables workload threading and parallel iterators, proc, which re-exports the Component derive macros, and std. The others are extended_tuple, which extends tuple implementations from 10 to 32 at four times the build time, serde1, tracing, thread_local for components that are neither Send nor Sync, and memory_constrained, which lowers the SparseArray page size.

How does the Shipyard project test itself?

The justfile chains formatting, a no-default-features check, a full test run with all features, documentation build, Miri under a pinned nightly, clippy and a clean. The Miri target runs the test suite under the undefined behaviour detector, which is why a specific working nightly is pinned in the file.

Official sources

  1. Issues
  2. leudz/shipyard on GitHub
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/leudz-shipyard.svg)](https://hysenlabs.com/projects/leudz-shipyard)