Skip to content

Commit 1c5e850

Browse files
alice-i-cecileDessix
authored andcommitted
Add set_if_neq method to DetectChanges trait (Rebased) (bevyengine#6853)
# 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. Adopted from bevyengine#5373, but untangled and rebased to current `bevy/main`. ## 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 bevyengine#2363 as it avoids triggering change detection, but not a complete solution (as it still requires triggering it when real changes are made). Co-authored-by: Zoey <[email protected]>
1 parent 492358c commit 1c5e850

File tree

3 files changed

+88
-9
lines changed

3 files changed

+88
-9
lines changed

crates/bevy_ecs/src/change_detection.rs

Lines changed: 76 additions & 1 deletion
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
///
@@ -90,6 +97,17 @@ pub trait DetectChanges {
9097
/// However, it can be an essential escape hatch when, for example,
9198
/// you are trying to synchronize representations using change detection and need to avoid infinite recursion.
9299
fn bypass_change_detection(&mut self) -> &mut Self::Inner;
100+
101+
/// Sets `self` to `value`, if and only if `*self != *value`
102+
///
103+
/// `T` is the type stored within the smart pointer (e.g. [`Mut`] or [`ResMut`]).
104+
///
105+
/// This is useful to ensure change detection is only triggered when the underlying value
106+
/// changes, instead of every time [`DerefMut`] is used.
107+
fn set_if_neq<Target>(&mut self, value: Target)
108+
where
109+
Self: Deref<Target = Target> + DerefMut<Target = Target>,
110+
Target: PartialEq;
93111
}
94112

95113
macro_rules! change_detection_impl {
@@ -132,6 +150,19 @@ macro_rules! change_detection_impl {
132150
fn bypass_change_detection(&mut self) -> &mut Self::Inner {
133151
self.value
134152
}
153+
154+
#[inline]
155+
fn set_if_neq<Target>(&mut self, value: Target)
156+
where
157+
Self: Deref<Target = Target> + DerefMut<Target = Target>,
158+
Target: PartialEq,
159+
{
160+
// This dereference is immutable, so does not trigger change detection
161+
if *<Self as Deref>::deref(self) != value {
162+
// `DerefMut` usage triggers change detection
163+
*<Self as DerefMut>::deref_mut(self) = value;
164+
}
165+
}
135166
}
136167

137168
impl<$($generics),*: ?Sized $(+ $traits)?> Deref for $name<$($generics),*> {
@@ -435,6 +466,19 @@ impl<'a> DetectChanges for MutUntyped<'a> {
435466
fn bypass_change_detection(&mut self) -> &mut Self::Inner {
436467
&mut self.value
437468
}
469+
470+
#[inline]
471+
fn set_if_neq<Target>(&mut self, value: Target)
472+
where
473+
Self: Deref<Target = Target> + DerefMut<Target = Target>,
474+
Target: PartialEq,
475+
{
476+
// This dereference is immutable, so does not trigger change detection
477+
if *<Self as Deref>::deref(self) != value {
478+
// `DerefMut` usage triggers change detection
479+
*<Self as DerefMut>::deref_mut(self) = value;
480+
}
481+
}
438482
}
439483

440484
impl std::fmt::Debug for MutUntyped<'_> {
@@ -458,12 +502,17 @@ mod tests {
458502
world::World,
459503
};
460504

461-
#[derive(Component)]
505+
use super::DetectChanges;
506+
507+
#[derive(Component, PartialEq)]
462508
struct C;
463509

464510
#[derive(Resource)]
465511
struct R;
466512

513+
#[derive(Resource, PartialEq)]
514+
struct R2(u8);
515+
467516
#[test]
468517
fn change_expiration() {
469518
fn change_detected(query: Query<ChangeTrackers<C>>) -> bool {
@@ -635,4 +684,30 @@ mod tests {
635684
// Modifying one field of a component should flag a change for the entire component.
636685
assert!(component_ticks.is_changed(last_change_tick, change_tick));
637686
}
687+
688+
#[test]
689+
fn set_if_neq() {
690+
let mut world = World::new();
691+
692+
world.insert_resource(R2(0));
693+
// Resources are Changed when first added
694+
world.increment_change_tick();
695+
// This is required to update world::last_change_tick
696+
world.clear_trackers();
697+
698+
let mut r = world.resource_mut::<R2>();
699+
assert!(!r.is_changed(), "Resource must begin unchanged.");
700+
701+
r.set_if_neq(R2(0));
702+
assert!(
703+
!r.is_changed(),
704+
"Resource must not be changed after setting to the same value."
705+
);
706+
707+
r.set_if_neq(R2(3));
708+
assert!(
709+
r.is_changed(),
710+
"Resource must be changed after setting to a different value."
711+
);
712+
}
638713
}

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.elapsed_seconds().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)