Systems

Maintaining queries and their associated data and logic can be verbose and error prone.

Systems provide the perfect aid and allows bundling arguments, such as queries, world access, and much more along with the logic to execute with it.

A system can be run manually or in a Schedule, which will automatically parallelize the system execution on multiple threads.

For example, to update distance from each position you could do:

#![allow(unused)]
fn main() {
        let update_distance = System::builder()
            .with_name("update_distance")
            .with_query(Query::new((entity_ids(), position(), distance().as_mut())))
            .build(
                |mut query: QueryBorrow<(_, Component<Vec2>, Mutable<f32>), _>| {
                    for (id, pos, dist) in &mut query {
                        println!("Updating distance for {id} with position: {pos:?}");
                        *dist = pos.length();
                    }
                },
            );
}

The system construction can subsequently be extracted to a function

#![allow(unused)]
fn main() {
        fn update_distance_system() -> BoxedSystem {
            System::builder()
                .with_name("update_distance")
                .with_query(Query::new((entity_ids(), position(), distance().as_mut())))
                .build(
                    |mut query: QueryBorrow<(_, Component<Vec2>, Mutable<f32>), _>| {
                        for (id, pos, dist) in &mut query {
                            println!("Updating distance for {id} with position: {pos:?}");
                            *dist = pos.length();
                        }
                    },
                )
                .boxed()
        }
    }
    let mut update_distance = update_distance_system();

}

However, the query won't yield entities since none have the distance component to modify. No to fear, we could add another system which ensures the component is present.

#![allow(unused)]
fn main() {
    fn add_distance_system() -> BoxedSystem {
        let query = Query::new(entity_ids())
            .with(position())
            .without(distance());

        System::builder()
            .with_cmd_mut()
            .with_query(query)
            .build(
                |cmd: &mut CommandBuffer, mut query: QueryBorrow<'_, flax::EntityIds, _>| {
                    for id in &mut query {
                        cmd.set(id, distance(), 0.0);
                    }
                },
            )
            .boxed()
    }
}

For each

Most systems iterate each item of a single query, without any other system arguments.

The shorthand for_each is provided for this purpose

#![allow(unused)]
fn main() {
    fn update_distance_system() -> BoxedSystem {
        System::builder()
            .with_name("update_distance")
            .with_query(Query::new((entity_ids(), position(), distance().as_mut())))
            .for_each(|(id, pos, dist)| {
                println!("Updating distance for {id} with position: {pos:?}");
                *dist = pos.length();
            })
            .boxed()
    }

}

Schedule

Lets add some more systems and add them to a Schedule

#![allow(unused)]
fn main() {
    /// Despawn all entities with a distance > 50
    fn despawn_system() -> BoxedSystem {
        System::builder()
            .with_name("delete_outside_world")
            .with_query(Query::new((entity_ids(), distance())).filter(distance().gt(50.0)))
            .with_cmd_mut()
            .build(|mut q: QueryBorrow<_, _>, cmd: &mut CommandBuffer| {
                for (id, &dist) in &mut q {
                    println!("Despawning {id} at: {dist}");
                    cmd.despawn(id);
                }
            })
            .boxed()
    }

    fn inspect_system() -> BoxedSystem {
        System::builder()
            .with_name("debug_world")
            .with_world()
            .build(|_world: &_| {
                // println!("World: {_world:#?}");
            })
            .boxed()
    }

    component! {
        /// Entities with this component will not be moved
        is_static: () => [ flax::Debuggable ],
    }

    fn move_system() -> BoxedSystem {
        System::builder()
            .with_name("move_out")
            .with_query(Query::new(position().as_mut()).filter(is_static().without()))
            .for_each(|pos| {
                let dir = pos.normalize_or_zero();

                *pos += dir;
            })
            .boxed()
    }

    // Since this system will move non static entities out from the origin, they will
    // eventually be despawned

    // Spawn new entities with a random position each frame
    fn spawn_system(mut rng: StdRng, count: usize) -> BoxedSystem {
        System::builder()
            .with_name("spawn")
            .with_cmd_mut()
            .build(move |cmd: &mut CommandBuffer| {
                for _ in 0..count {
                    let pos = vec2(rng.gen_range(-10.0..10.0), rng.gen_range(-10.0..10.0));
                    println!("Spawning new entity at: {pos:?}");
                    Entity::builder()
                        .set(position(), pos)
                        .set_default(distance())
                        .spawn_into(cmd);
                }
            })
            .boxed()
    }

    let rng = StdRng::seed_from_u64(42);

    // Assemble the schedule, takes care of dependency management
    let mut schedule = Schedule::builder()
        .with_system(add_distance_system())
        .flush()
        .with_system(update_distance_system())
        .with_system(despawn_system())
        .with_system(spawn_system(rng, 1))
        .with_system(move_system())
        .with_system(inspect_system())
        .build();

    for i in 0..20 {
        println!("Frame: {i}");
        schedule.execute_par(&mut world)?;
    }


}

System access

One of the abilities of a system is to automatically extract what they need for their arguments from the schedule context.

This both increases their ease of use, but further assures that only the declared pieces of data will be accessed, which allows seamless fine-grained parallelization capabilities

Therefore, it is advised to keep your system arguments as specific as possible, such as using queries whenever possible, rather than the whole world.

Note: Systems which access the world will not paralellize with other systems as it may access anything.

#![allow(unused)]
fn main() {
    // Despawn all entities with a distance > 50
    let despawn = System::builder()
        .with_name("delete_outside_world")
        .with_query(Query::new((entity_ids(), distance())).filter(distance().gt(50.0)))
        .with_cmd_mut()
        .build(|mut q: QueryBorrow<_, _>, cmd: &mut CommandBuffer| {
            for (id, &dist) in &mut q {
                println!("Despawning {id} at: {dist}");
                cmd.despawn(id);
            }
        });

    let debug_world = System::builder()
        .with_name("debug_world")
        .with_world()
        .build(|world: &_| {
            tracing::debug!("World: {world:?}");
        });

}