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.