Skip to content

Commit 45080da

Browse files
Add set_if_neq method to DetectChanges trait (#5373)
# Objective Change detection can be spuriously triggered by setting a field to the same value as before. As a result, a common pattern is to write: ```rust if *foo != value { *foo = value; } ``` This is confusing to read, and heavy on boilerplate. ## Solution 1. Add a method to the `DetectChanges` trait that implements this boilerplate when the appropriate trait bounds are met. 2. Document this minor footgun, and point users to it. ## Changelog - added the `set_if_neq` method to avoid triggering change detection when the new and previous values are equal. This will work on both components and resources. ## Migration Guide If you are manually checking if a component or resource's value is equal to its new value before setting it to avoid triggering change detection, migrate to the clearer and more convenient `set_if_neq` method. ## Context Related to #2363 as it avoids triggering change detection, but not a complete solution (as it still requires triggering it when real changes are made).
1 parent 5230729 commit 45080da

File tree

3 files changed

+70
-13
lines changed

3 files changed

+70
-13
lines changed

crates/bevy_ecs/src/change_detection.rs

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ pub const MAX_CHANGE_AGE: u32 = u32::MAX - (2 * CHECK_TICK_THRESHOLD - 1);
3131
/// Normally change detecting is triggered by either [`DerefMut`] or [`AsMut`], however
3232
/// it can be manually triggered via [`DetectChanges::set_changed`].
3333
///
34+
/// To ensure that changes are only triggered when the value actually differs,
35+
/// check if the value would change before assignment, such as by checking that `new != old`.
36+
/// You must be *sure* that you are not mutably derefencing in this process.
37+
///
38+
/// [`set_if_neq`](DetectChanges::set_if_neq) is a helper
39+
/// method for this common functionality.
40+
///
3441
/// ```
3542
/// use bevy_ecs::prelude::*;
3643
///
@@ -75,6 +82,24 @@ pub trait DetectChanges {
7582
/// [`SystemParam`](crate::system::SystemParam).
7683
fn last_changed(&self) -> u32;
7784

85+
/// Sets `self` to `value`, if and only if `*self != *value`
86+
///
87+
/// `T` is the type stored within the smart pointer (e.g. [`Mut`] or [`ResMut`]).
88+
///
89+
/// This is useful to ensure change detection is only triggered when the underlying value
90+
/// changes, instead of every time [`DerefMut`] is used.
91+
#[inline]
92+
fn set_if_neq<T>(&mut self, value: T)
93+
where
94+
Self: Deref<Target = T> + DerefMut<Target = T>,
95+
T: PartialEq,
96+
{
97+
// This dereference is immutable, so does not trigger change detection
98+
if **self != value {
99+
**self = value;
100+
}
101+
}
102+
78103
/// Manually sets the change tick recording the previous time this data was mutated.
79104
///
80105
/// # Warning
@@ -458,11 +483,13 @@ mod tests {
458483
world::World,
459484
};
460485

461-
#[derive(Component)]
486+
use super::DetectChanges;
487+
488+
#[derive(Component, PartialEq)]
462489
struct C;
463490

464-
#[derive(Resource)]
465-
struct R;
491+
#[derive(PartialEq, Resource)]
492+
struct R(u8);
466493

467494
#[test]
468495
fn change_expiration() {
@@ -551,6 +578,32 @@ mod tests {
551578
}
552579
}
553580

581+
#[test]
582+
fn set_if_neq() {
583+
let mut world = World::new();
584+
585+
world.insert_resource(R(0));
586+
// Resources are Changed when first added
587+
world.increment_change_tick();
588+
// This is required to update world::last_change_tick
589+
world.clear_trackers();
590+
591+
let mut r = world.resource_mut::<R>();
592+
assert!(!r.is_changed(), "Resource must begin unchanged.");
593+
594+
r.set_if_neq(R(0));
595+
assert!(
596+
!r.is_changed(),
597+
"Resource must not be changed after setting to the same value."
598+
);
599+
600+
r.set_if_neq(R(3));
601+
assert!(
602+
r.is_changed(),
603+
"Resource must be changed after setting to a different value."
604+
);
605+
}
606+
554607
#[test]
555608
fn mut_from_res_mut() {
556609
let mut component_ticks = ComponentTicks {
@@ -563,7 +616,7 @@ mod tests {
563616
last_change_tick: 3,
564617
change_tick: 4,
565618
};
566-
let mut res = R {};
619+
let mut res = R(0);
567620
let res_mut = ResMut {
568621
value: &mut res,
569622
ticks,
@@ -588,7 +641,7 @@ mod tests {
588641
last_change_tick: 3,
589642
change_tick: 4,
590643
};
591-
let mut res = R {};
644+
let mut res = R(0);
592645
let non_send_mut = NonSendMut {
593646
value: &mut res,
594647
ticks,

crates/bevy_ui/src/focus.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStack};
22
use bevy_ecs::{
3+
change_detection::DetectChanges,
34
entity::Entity,
45
prelude::Component,
56
query::WorldQuery,
@@ -179,10 +180,8 @@ pub fn ui_focus_system(
179180
Some(*entity)
180181
} else {
181182
if let Some(mut interaction) = node.interaction {
182-
if *interaction == Interaction::Hovered
183-
|| (cursor_position.is_none() && *interaction != Interaction::None)
184-
{
185-
*interaction = Interaction::None;
183+
if *interaction == Interaction::Hovered || (cursor_position.is_none()) {
184+
interaction.set_if_neq(Interaction::None);
186185
}
187186
}
188187
None
@@ -227,8 +226,8 @@ pub fn ui_focus_system(
227226
while let Some(node) = iter.fetch_next() {
228227
if let Some(mut interaction) = node.interaction {
229228
// don't reset clicked nodes because they're handled separately
230-
if *interaction != Interaction::Clicked && *interaction != Interaction::None {
231-
*interaction = Interaction::None;
229+
if *interaction != Interaction::Clicked {
230+
interaction.set_if_neq(Interaction::None);
232231
}
233232
}
234233
}

examples/ecs/component_change_detection.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ fn main() {
1313
.run();
1414
}
1515

16-
#[derive(Component, Debug)]
16+
#[derive(Component, PartialEq, Debug)]
1717
struct MyComponent(f32);
1818

1919
fn setup(mut commands: Commands) {
@@ -25,7 +25,12 @@ fn change_component(time: Res<Time>, mut query: Query<(Entity, &mut MyComponent)
2525
for (entity, mut component) in &mut query {
2626
if rand::thread_rng().gen_bool(0.1) {
2727
info!("changing component {:?}", entity);
28-
component.0 = time.elapsed_seconds();
28+
let new_component = MyComponent(time.seconds_since_startup().round());
29+
// Change detection occurs on mutable derefence,
30+
// and does not consider whether or not a value is actually equal.
31+
// To avoid triggering change detection when nothing has actually changed,
32+
// you can use the `set_if_neq` method on any component or resource that implements PartialEq
33+
component.set_if_neq(new_component);
2934
}
3035
}
3136
}

0 commit comments

Comments
 (0)