Advanced query
A query is a combination of a
Fetch
and a
Filter
in unison with a state.
The most common kind of Fetch
is the component and the tuple fetch. The tuple
combines multiple fetches into a single one.
A tuple of components, or any other Fetch
will only yield entities which match
the full tuple, I.e;
Optional query
#![allow(unused)] fn main() { // Entities with mass (0..10).for_each(|i| { builder .set(name(), format!("Entity.{i}")) .set(position(), rng.gen::<Vec3>() * 10.0) .set(velocity(), rng.gen()) .set(mass(), rng.gen_range(10..30) as f32) .spawn(&mut world); }); // Entities without mass (0..100).for_each(|i| { builder .set(name(), format!("Entity.{i}")) .set(position(), rng.gen::<Vec3>() * 0.5) .set(velocity(), rng.gen()) .spawn(&mut world); }); // Since this query accessed `position`, `velocity` **and** `mass` only the // first group of entities will be matched for (pos, vel, mass) in &mut Query::new((position(), velocity(), mass())).borrow(&world) { tracing::debug!("pos: {pos}, vel: {vel}, mass: {mass}"); } }
Will only yield entities which have both position and velocity.
By adding .opt()
to any fetch, you convert it to an Option<T>
where T
is
the value yielded by the underlying fetch
#![allow(unused)] fn main() { // Use an optional fetch to yield an `Option<T>`, works for any query for (pos, vel, mass) in &mut Query::new((position(), velocity(), mass().opt())).borrow(&world) { if mass.is_some() { tracing::debug!("Has mass"); } tracing::debug!("pos: {pos}, vel: {vel}, mass: {mass:?}"); } }
However, it can still be cumbersome to deal with the option, especially since
components yield references, which can not be combined with
Option::unwrap_or_default
The opt_or_default
combinator can be used, or opt_or
to provide your custom value if the entity does not have the specified component.
There only work for fetches which return a shared reference, as the provided
default is also returned by reference. For when there is an owned value, consider using the builting Option::unwrap_or_default
.
This combinator is useful when writing systems which may need to operate on entities which lack some components, such as physics where the entity may not have a rotation.
Change detection
Every time a component is modified, either through World::get_mut
, or a query, a Modified
event is added to the affected entities.
Similarly, set
when the component did not previously exist, and new entities will create an Added
event.
Removal
events are created by
World::remove
.
The following example makes use of optional combinators and change detection to handle a 3d world.
#![allow(unused)] fn main() { component! { rotation: Quat, scale: Vec3, world_matrix: Mat4 => [Debuggable], } let create_world_matrix = System::builder() .with_name("add_world_matrix") .with_query( Query::new(entity_ids()) .with(position()) .without(world_matrix()), ) .with_cmd_mut() .build( |mut query: QueryBorrow<EntityIds, _>, cmd: &mut CommandBuffer| { for id in &mut query { tracing::info!("Adding world matrix to {id}"); cmd.set(id, world_matrix(), Mat4::IDENTITY); } }, ); let update_world_matrix = System::builder() .with_name("update_world_matrix") .with_query( Query::new(( entity_ids(), world_matrix().as_mut(), position(), rotation().opt_or_default(), scale().opt_or(Vec3::ONE), )) .with_filter(position().modified() | rotation().modified() | scale().modified()), ) .for_each(|(id, world_matrix, pos, rot, scale)| { tracing::info!("Updating world matrix for: {id} {pos} {rot} {scale}"); *world_matrix = Mat4::from_scale_rotation_translation(*scale, *rot, *pos); }); let mut schedule = Schedule::builder() .with_system(create_world_matrix) .flush() .with_system(update_world_matrix) .build(); let all_ids = Query::new(entity_ids()).borrow(&world).iter().collect_vec(); tracing::info!("Schedule: {schedule:#?}"); for _ in 0..10 { schedule .execute_par(&mut world) .expect("Failed to execute schedule"); for _ in 0..32 { let id = *all_ids.choose(&mut rng).expect("no ids"); let mut pos = world.get_mut(id, position())?; // Move a bit away from origin let dir = pos.normalize(); *pos += dir * rng.gen::<f32>(); drop(pos); let mut scale = world.entry(id, scale())?.or_insert(Vec3::ONE); *scale *= 1.1; } sleep(Duration::from_secs(1)) } tracing::info!("World: {world:#?}"); }
Implementation details
Each `ChangeEvent` consists of a subslice of adjacent entities in the same
archetype, the change type, and when the change occurred.
Two change events where the entities are adjacent will be joined into a single
one will be joined. This means the change list is always rather small compared
to the number of changing entities (especially compared to using a `HashSet`).
The following example combines optional queries with change detection to create
a small physic calculation.