Shipyard is an Entity Component System crate. ECS is a pattern mostly used in games but not only. It fits really well with Rust, allowing easy composition and lifetime management.
Most notable changes:
-
Remove
system!
macro
This macro was used to hide what information workloads need. For example usingsystem!(check)
expended to(|world: &World| world.try_run(check), check))
.
The scheduler would use the function on the right to define what the function on the left borrows. If the two functions didn't match you could run into runtime errors.The new api (
.with_system(&check)
) does not have this problem. -
Remove packs
Packs are removed temporarily, this implementation had too many limitations. The next implementation should not have the same problems but requires a fairly big amount of work so this version ships without packs and the next version should add them without modifying any existing api.remove
was the most impacted, to remove two or more components you had to writeRemove::<(usize, u32)>::remove((&mut usizes, &mut u32s), entity);
.
With 0.5 it's just(&mut usizes, &mut u32s).remove(entity)
. -
Bulk add entity
This new method is faster than adding entities one by one. -
Remove
Shiperator
0.4 used a customIterator
trait to modify some methods' behavior likefilter
.
With 0.5, iterating an exclusive storage yields aMut<T>
by default which removes the need for a custom trait. -
Accurate modification tracking by default
In 0.4 when you tracked modification on a storage it would tag all components accessed exclusively regardless if they were modified or not.
This is not true anymore, this code would tag exactly what you would expect:fn increment_even(mut u32s: ViewMut<u32>) { for mut i in (&mut u32s).iter() { if *i % 2 == 0 { *i += 1; } } }
-
No more
try_*
In 0.4 all fallible functions had two versions, one that panicked and one prefixed withtry_
that returned aResult
.
Now all functions that can fail because of storage access return aResult
while almost all others panic. -
Workload building is more flexible
In 0.4 you could only create workloads directly withWorld
and it was borrowed for the entire building time.world .add_workload("Add & Check") .with_system(system!(add)) .with_system(system!(check)) .build();
In 0.5 you only need to borrow
World
to actually add the workload to it.Workload::builder("Add & Check") .with_system(&add) .with_system(&check) .add_to_world(&world) .unwrap();
You can also "prepare" systems in advance, merge
WorkloadBuilder
s, extend aWorkloadBuilder
with a system iterator or even nest multiple workloads. -
Workload debug
When you create a workload you can print information about how it will be scheduled and why two systems can't run at the same time. -
Custom view
A lot of internal traits, types and functions are exposed in 0.5. One of them isBorrow
, it allows user defined views. Custom views are very important to build clean apis on top of Shipyard.
For example this custom view (shipyard-scenegraph/views.rs at master · dakom/shipyard-scenegraph · GitHub) borrows 11 storages. Users won't have to borrow 11 views though, just a single one.
Creating a custom view is currently fairly verbose, I want to add a proc macro that would automatically derive the necessary traits. -
Custom storage
Shipyard comes with two default storages:SparseSet
andUnique
. 0.5 allows you to define your own storages. You can then build a view for them and access them in systems for example.
The advantage compared to simply storing your type inUnique
is to be able to define what your storage should do when an entity is deleted. -
Performance improvements
0.4 0.5 insert 10k entities with 4 components 3.15ms 1.00ms same as above but using bulk_add_entity
X 333.29 us iterate 2 storages of 10k components 106.24us 37.24us same as above but the components are shuffled 91.49us 34.92us iterate a single storage of 520 components 162.79ns 103.25ns access a component on 10k entities 70.76us 19.14us delete 520 entities in a 27 storages World
1.95us 212.99ns add and remove a component to a 600 bytes entity 10k times 306.02us 139.12us
Small example:
use shipyard::{IntoIter, View, ViewMut, World};
struct Health(u32);
struct Position {
x: f32,
y: f32,
}
fn in_acid(positions: View<Position>, mut healths: ViewMut<Health>) {
for (_, mut 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).unwrap();
}