Change Detection
Flax tracks when a component is added, mutably accessed, or removed.
A query allows filtering the entities based on a change event since it last ran.
modified
filter mutated or new componentsadded
only new componentsremoved
filter recently removed components.
The modified filter is best used for queries which calculate or update a value based on one or more components, or in other ways react to a changed value.
A change filter can be added to a single component, or to a tuple of components. Applying a .modified()
transform on a tuple will create a query which yields if any of the constituents were modified.
The following example creates a system which prints the updated health values for each entity.
#![allow(unused)] fn main() { let query = Query::new((name(), health().modified())); let health_changes = System::builder() .with_query(query) .build(|mut query: QueryBorrow<_>| { info_span!("health_changes"); for (name, health) in &mut query { tracing::info!("{name:?}: is now at {health} health"); } }); }
Combining filters
Change filters can be combined with other filters, which leads to queries needing to perform even even less work.
The following example creates a query which removes despawns entities when their
health becomes 0
. Noteworthy in particular, is that this system can run in
parallel with the previously discussed system, since they do not overlap in
mutable access.
#![allow(unused)] fn main() { let query = Query::new((name().opt(), entity_ids(), player().satisfied())) .with_filter(health().le(0.0).modified()); let cleanup = System::builder() .with_name("cleanup") .with_query(query) .with_cmd_mut() .build(|mut q: QueryBorrow<_, _>, cmd: &mut CommandBuffer| { for (name, id, is_player) in &mut q { if is_player { tracing::info!("Player died"); } tracing::info!(name, is_player, "Despawning {id}"); cmd.despawn(id); } }); }
Bringing it all together
In order for the health monitoring and cleanup systems to be effective, there needs to be something to modify the health of entities.
Such as a random damage system, and a poison status effect.
#![allow(unused)] fn main() { let player_id = Entity::builder() .set(name(), "player".into()) .set(health(), 100.0) .set_default(player()) .spawn(&mut world); let enemies = (0..10) .map(|i| { Entity::builder() .set(name(), format!("enemy.{i}")) .set(health(), 50.0) .spawn(&mut world) }) .collect_vec(); let all = enemies.iter().copied().chain([player_id]).collect_vec(); let mut rng = StdRng::from_entropy(); all.choose_multiple(&mut rng, all.len() / 5) .for_each(|&id| { world.set(id, poison(), 10.0).unwrap(); }); let damage_random = System::builder() .with_world_mut() .build(move |world: &mut World| { let count = rng.gen_range(0..enemies.len()); let targets = all.choose_multiple(&mut rng, count); for &enemy in targets { if let Ok(mut health) = world.get_mut(enemy, health()) { *health -= 1.0; } } }); let update_poison = System::builder() .with_query(Query::new((name().opt(), health().as_mut(), poison()))) .for_each(|(name, health, poison)| { *health -= poison; tracing::info!("{name:?} suffered {poison} in poison damage"); }); }
Using a schedule allows for easy parallelization and execution of the systems, but is not a requirement for change detection.
#![allow(unused)] fn main() { let mut schedule = Schedule::new() .with_system(damage_random) .with_system(update_poison) .with_system(health_changes) .flush() .with_system(cleanup) .flush(); while world.is_alive(player_id) { schedule .execute_par(&mut world) .expect("Failed to run schedule"); sleep(Duration::from_millis(1000)); } }
See the full example here
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.