Bevy Version: | 0.13 | (outdated!) |
---|
As this page is outdated, please refer to Bevy's official migration guides while reading, to cover the differences: 0.13 to 0.14. 0.14 to 0.15.
I apologize for the inconvenience. I will update the page as soon as I find the time.
Change Detection
Relevant official examples:
component_change_detection
.
Bevy allows you to easily detect when data is changed. You can use this to perform actions in response to changes.
One of the main use cases is optimization – avoiding unnecessary work by only doing it if the relevant data has changed. Another use case is triggering special actions to occur on changes, like configuring something or sending the data somewhere.
Components
Filtering
You can make a query that only yields entities if specific components on them have been modified.
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 mutated
- also triggers if the component is newly-added (as per
Added
)
(If you want to react to removals, see removal detection. It works differently.)
/// 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);
}
}
Checking
If you want to access all the entities, as normal, regardless of if they have
been modified, but you just want to know if a component has been changed,
you can use special [Ref<T>
] query parameters instead of &
for immutable access.
For mutable access, the change detection methods are always available (because
Bevy queries actually return a special Mut<T>
type whenever you have &mut
in the query).
/// Make sprites flash red on frames when the Health changes
fn debug_damage(
mut query: Query<(&mut Sprite, Ref<Health>)>,
) {
for (mut sprite, health) in query.iter_mut() {
// detect if the Health changed this frame
if health.is_changed() {
eprintln!("HP is: {}", health.hp);
// we can also check if the sprite has been changed
if !sprite.is_changed() {
sprite.color = Color::RED;
}
}
}
}
Resources
For resources, change detection is provided via methods on the
Res<T>
/ResMut<T>
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
}
}
}
What gets detected?
Changed
detection is triggered by DerefMut
. Simply accessing
components via a mutable query, or
resources via ResMut
, without actually performing a &mut
access, will not trigger it. This makes change detection quite accurate.
Note: if you call a Rust function that takes a &mut T
(mutable borrow),
that counts! It will trigger change detection even if the function does
not actually do any mutation. Be careful with helper functions!
Also, when you mutate a component, Bevy does not check 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 works on a per-system granularity, and is reliable. A system will detect changes only if it has not seen them before (the changes happened since the last time it ran).
Unlike events, you do not have to worry about missing changes If your system only runs sometimes (such as when using states or run conditions).
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.
Removal Detection
Relevant official examples:
removal_detection
.
Removal detection is special. This is because, unlike with change detection, the data does not exist in the ECS anymore (obviously), so Bevy cannot keep tracking metadata for it.
Nevertheless, being able to respond to removals is important for some applications, so Bevy offers a limited form of it.
Components
You can check for components that have been removed during the current frame. The data is cleared at the end of every frame update. You must make sure your detecting system is ordered after (or is in another schedule that runs after) the system that does the removing.
Note: removal detection also includes despawned entities!
Use the RemovedComponents<T>
special system parameter type. Internally, it
is implemented using events and behaves like an EventReader
,
but it gives you the Entity
IDs of entities whose component T
was removed.
fn detect_removals(
mut removals: RemovedComponents<EnemyIsTrackingPlayer>,
// ... (maybe Commands or a Query ?) ...
) {
for entity in removals.read() {
// do something with the entity
eprintln!("Entity {:?} had the component removed.", entity);
}
}
(To do things with these entities, you can just use the Entity
IDs with
Commands::entity()
or Query::get()
.)
Resources
Bevy does not provide any API for detecting when resources are removed.
You can work around this using Option
and a separate Local
system parameter, effectively implementing your own detection.
fn detect_removed_res(
my_res: Option<Res<MyResource>>,
mut my_res_existed: Local<bool>,
) {
if let Some(my_res) = my_res {
// the resource exists!
// remember that!
*my_res_existed = true;
// (... you can do something with the resource here if you want ...)
} else if *my_res_existed {
// the resource does not exist, but we remember it existed!
// (it was removed)
// forget about it!
*my_res_existed = false;
// ... do something now that it is gone ...
}
}
Note that, since this detection is local to your system, it does not have to happen during the same frame update.