Change Detection

Relevant official examples: change_detection.


Bevy allows you to easily detect when data is changed. You can use this to perform actions in response to changes.

Components

Use query filters:

  • Added<T>: detect new component instances
    • if the component was added to an existing entity
    • if a new entity with the component was spawned
  • Changed<T>: detect component instances that have been changed
    • triggers when the component is accessed mutably
    • also triggers if the component is newly-added (as per Added)

(If you want to react to removals, see the page on removal detection. It works differently and is much trickier to use.)

/// Print the stats of friendly players when they change
fn debug_stats_change(
    query: Query<
        // components
        (&Health, &PlayerXp),
        // filters
        (Without<Enemy>, Or<(Changed<Health>, Changed<PlayerXp>)>), 
    >,
) {
    for (health, xp) in query.iter() {
        eprintln!(
            "hp: {}+{}, xp: {}",
            health.hp, health.extra, xp.0
        );
    }
}

/// detect new enemies and print their health
fn debug_new_hostiles(
    query: Query<(Entity, &Health), Added<Enemy>>,
) {
    for (entity, health) in query.iter() {
        eprintln!("Entity {:?} is now an enemy! HP: {}", entity, health.hp);
    }
}

Changed detection is triggered by DerefMut. Simply accessing components via a mutable query, without actually performing a &mut access, will not trigger it.

This makes change detection quite accurate. You can rely on it to optimize your game's performance, or to otherwise trigger things to happen.

Also note that when you mutate a component, Bevy does not track if the new value is actually different from the old value. It will always trigger the change detection. If you want to avoid that, simply check it yourself:

fn update_player_xp(
    mut query: Query<&mut PlayerXp>,
) {
    for mut xp in query.iter_mut() {
        let new_xp = maybe_lvl_up(&xp);

        // avoid triggering change detection if the value is the same
        if new_xp != *xp {
            *xp = new_xp;
        }
    }
}

Change detection is reliable -- it will detect any changes that have occured since the last time your detecting system ran. If your system only runs sometimes (such as with states or run criteria), you do not have to worry about missing changes.

Resources

For resources, change detection is provided via methods on the Res/ResMut system parameters.

fn check_res_changed(
    my_res: Res<MyResource>,
) {
    if my_res.is_changed() {
        // do something
    }
}

fn check_res_added(
    // use Option, not to panic if the resource doesn't exist yet
    my_res: Option<Res<MyResource>>,
) {
    if let Some(my_res) = my_res {
        // the resource exists

        if my_res.is_added() {
            // it was just added
            // do something
        }
    }
}

This works the same way as change detection for components.

Possible Pitfalls

Beware of frame delay / 1-frame-lag. This can occur if Bevy runs the detecting system before the changing system. The detecting system will see the change the next time it runs, typically on the next frame update.

If you need to ensure that changes are handled immediately / during the same frame, you can use explicit system ordering.

However, when detecting component additions with Added<T> (which are typically done using Commands), this is not enough; you need stages.