Document not found (404)
+This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +diff --git a/bunny_demo/index.html b/bunny_demo/index.html new file mode 100644 index 00000000..14f19101 --- /dev/null +++ b/bunny_demo/index.html @@ -0,0 +1,22 @@ + + + +
+ + + +This URL is invalid, sorry. Please use the navigation bar or search to continue.
+ +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!
+ +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.
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.
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);
+
+ When an entity is created you will receive a unique handle to it: an EntityId
.
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.
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));
+
+ Deleting a component will erase it from the storage but will not return it.
+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.
let mut world = World::new();
+
+let id = world.add_entity((0u32, 1usize));
+
+world.strip(id);
+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);
+let world = World::new();
+
+let mut all_storages = world.borrow::<AllStoragesViewMut>().unwrap();
+
+let id = all_storages.add_entity((0u32, 1usize));
+
+all_storages.strip(id);
+
+ Deleting an entity deletes it from the entities storage, while also deleting all its components.
+let mut world = World::new();
+
+let id = world.add_entity((0u32,));
+
+world.delete_entity(id);
+let world = World::new();
+
+let mut all_storages = world.borrow::<AllStoragesViewMut>().unwrap();
+
+let id = all_storages.add_entity((0u32,));
+
+all_storages.delete_entity(id);
+
+ 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.
+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
.
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.
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);
+}
+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);
+}
+
+ Removing a component will take it out of the storage and return it.
+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.
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);
+
+ 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.
+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();
+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.
+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.
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();
+
+ 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
.
let world = World::default();
+let world = World::new();
+There is no need to register components, storages are created on first access.
+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();
+
+ This section covers more advanced topics. Topics include parallelism, and how everything behaves so you can avoid surprises.
+ +!Send
and !Sync
ComponentsWorld
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>>>) {}
+
+ 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.
+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":
+AllStorages
stop all threading.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.
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.
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 loopWhile 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.
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.
SparseSet
is made of three arrays:
sparse
contains indices to the dense
and data
arraysdense
contains EntityId
data
contains the actual componentsdense
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.
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.
Iterating one or several SparseSet
is different. With a single SparseSet
it's as simple as iterating data
.
+To iterate multiple SparseSet
s the smallest will be chosen as "lead". We then iterate its dense
array and for each entity we check all the other SparseSet
s to see if they also contain a component for this entity.
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:
+However, programming with an ECS requires thinking about data and logic in a different way than you might be used to.
+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.
+ +