diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 0831f316ef4f8..71dd485add7d4 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -31,6 +31,13 @@ pub const MAX_CHANGE_AGE: u32 = u32::MAX - (2 * CHECK_TICK_THRESHOLD - 1); /// Normally change detecting is triggered by either [`DerefMut`] or [`AsMut`], however /// it can be manually triggered via [`DetectChanges::set_changed`]. /// +/// To ensure that changes are only triggered when the value actually differs, +/// check if the value would change before assignment, such as by checking that `new != old`. +/// You must be *sure* that you are not mutably derefencing in this process. +/// +/// [`set_if_neq`](DetectChanges::set_if_neq) is a helper +/// method for this common functionality. +/// /// ``` /// use bevy_ecs::prelude::*; /// @@ -75,6 +82,24 @@ pub trait DetectChanges { /// [`SystemParam`](crate::system::SystemParam). fn last_changed(&self) -> u32; + /// Sets `self` to `value`, if and only if `*self != *value` + /// + /// `T` is the type stored within the smart pointer (e.g. [`Mut`] or [`ResMut`]). + /// + /// This is useful to ensure change detection is only triggered when the underlying value + /// changes, instead of every time [`DerefMut`] is used. + #[inline] + fn set_if_neq(&mut self, value: T) + where + Self: Deref + DerefMut, + T: PartialEq, + { + // This dereference is immutable, so does not trigger change detection + if **self != value { + **self = value; + } + } + /// Manually sets the change tick recording the previous time this data was mutated. /// /// # Warning @@ -458,11 +483,13 @@ mod tests { world::World, }; - #[derive(Component)] + use super::DetectChanges; + + #[derive(Component, PartialEq)] struct C; - #[derive(Resource)] - struct R; + #[derive(PartialEq, Resource)] + struct R(u8); #[test] fn change_expiration() { @@ -551,6 +578,32 @@ mod tests { } } + #[test] + fn set_if_neq() { + let mut world = World::new(); + + world.insert_resource(R(0)); + // Resources are Changed when first added + world.increment_change_tick(); + // This is required to update world::last_change_tick + world.clear_trackers(); + + let mut r = world.resource_mut::(); + assert!(!r.is_changed(), "Resource must begin unchanged."); + + r.set_if_neq(R(0)); + assert!( + !r.is_changed(), + "Resource must not be changed after setting to the same value." + ); + + r.set_if_neq(R(3)); + assert!( + r.is_changed(), + "Resource must be changed after setting to a different value." + ); + } + #[test] fn mut_from_res_mut() { let mut component_ticks = ComponentTicks { @@ -563,7 +616,7 @@ mod tests { last_change_tick: 3, change_tick: 4, }; - let mut res = R {}; + let mut res = R(0); let res_mut = ResMut { value: &mut res, ticks, @@ -588,7 +641,7 @@ mod tests { last_change_tick: 3, change_tick: 4, }; - let mut res = R {}; + let mut res = R(0); let non_send_mut = NonSendMut { value: &mut res, ticks, diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index f3e63bf3e2b95..c27c6fe041159 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,5 +1,6 @@ use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStack}; use bevy_ecs::{ + change_detection::DetectChanges, entity::Entity, prelude::Component, query::WorldQuery, @@ -179,10 +180,8 @@ pub fn ui_focus_system( Some(*entity) } else { if let Some(mut interaction) = node.interaction { - if *interaction == Interaction::Hovered - || (cursor_position.is_none() && *interaction != Interaction::None) - { - *interaction = Interaction::None; + if *interaction == Interaction::Hovered || (cursor_position.is_none()) { + interaction.set_if_neq(Interaction::None); } } None @@ -227,8 +226,8 @@ pub fn ui_focus_system( while let Some(node) = iter.fetch_next() { if let Some(mut interaction) = node.interaction { // don't reset clicked nodes because they're handled separately - if *interaction != Interaction::Clicked && *interaction != Interaction::None { - *interaction = Interaction::None; + if *interaction != Interaction::Clicked { + interaction.set_if_neq(Interaction::None); } } } diff --git a/examples/ecs/component_change_detection.rs b/examples/ecs/component_change_detection.rs index 56823144d945f..ee32a41fb0ba5 100644 --- a/examples/ecs/component_change_detection.rs +++ b/examples/ecs/component_change_detection.rs @@ -13,7 +13,7 @@ fn main() { .run(); } -#[derive(Component, Debug)] +#[derive(Component, PartialEq, Debug)] struct MyComponent(f32); fn setup(mut commands: Commands) { @@ -25,7 +25,12 @@ fn change_component(time: Res