mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
Add set_if_neq
method to DetectChanges
trait (Rebased) (#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 #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 #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 <Dessix@Dessix.net>
This commit is contained in:
parent
aeb2c4b917
commit
4820917af6
3 changed files with 88 additions and 9 deletions
|
@ -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::*;
|
||||
///
|
||||
|
@ -90,6 +97,17 @@ pub trait DetectChanges {
|
|||
/// However, it can be an essential escape hatch when, for example,
|
||||
/// you are trying to synchronize representations using change detection and need to avoid infinite recursion.
|
||||
fn bypass_change_detection(&mut self) -> &mut Self::Inner;
|
||||
|
||||
/// 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.
|
||||
fn set_if_neq<Target>(&mut self, value: Target)
|
||||
where
|
||||
Self: Deref<Target = Target> + DerefMut<Target = Target>,
|
||||
Target: PartialEq;
|
||||
}
|
||||
|
||||
macro_rules! change_detection_impl {
|
||||
|
@ -132,6 +150,19 @@ macro_rules! change_detection_impl {
|
|||
fn bypass_change_detection(&mut self) -> &mut Self::Inner {
|
||||
self.value
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_if_neq<Target>(&mut self, value: Target)
|
||||
where
|
||||
Self: Deref<Target = Target> + DerefMut<Target = Target>,
|
||||
Target: PartialEq,
|
||||
{
|
||||
// This dereference is immutable, so does not trigger change detection
|
||||
if *<Self as Deref>::deref(self) != value {
|
||||
// `DerefMut` usage triggers change detection
|
||||
*<Self as DerefMut>::deref_mut(self) = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<$($generics),*: ?Sized $(+ $traits)?> Deref for $name<$($generics),*> {
|
||||
|
@ -435,6 +466,19 @@ impl<'a> DetectChanges for MutUntyped<'a> {
|
|||
fn bypass_change_detection(&mut self) -> &mut Self::Inner {
|
||||
&mut self.value
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_if_neq<Target>(&mut self, value: Target)
|
||||
where
|
||||
Self: Deref<Target = Target> + DerefMut<Target = Target>,
|
||||
Target: PartialEq,
|
||||
{
|
||||
// This dereference is immutable, so does not trigger change detection
|
||||
if *<Self as Deref>::deref(self) != value {
|
||||
// `DerefMut` usage triggers change detection
|
||||
*<Self as DerefMut>::deref_mut(self) = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MutUntyped<'_> {
|
||||
|
@ -458,12 +502,17 @@ mod tests {
|
|||
world::World,
|
||||
};
|
||||
|
||||
#[derive(Component)]
|
||||
use super::DetectChanges;
|
||||
|
||||
#[derive(Component, PartialEq)]
|
||||
struct C;
|
||||
|
||||
#[derive(Resource)]
|
||||
struct R;
|
||||
|
||||
#[derive(Resource, PartialEq)]
|
||||
struct R2(u8);
|
||||
|
||||
#[test]
|
||||
fn change_expiration() {
|
||||
fn change_detected(query: Query<ChangeTrackers<C>>) -> bool {
|
||||
|
@ -635,4 +684,30 @@ mod tests {
|
|||
// Modifying one field of a component should flag a change for the entire component.
|
||||
assert!(component_ticks.is_changed(last_change_tick, change_tick));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_if_neq() {
|
||||
let mut world = World::new();
|
||||
|
||||
world.insert_resource(R2(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::<R2>();
|
||||
assert!(!r.is_changed(), "Resource must begin unchanged.");
|
||||
|
||||
r.set_if_neq(R2(0));
|
||||
assert!(
|
||||
!r.is_changed(),
|
||||
"Resource must not be changed after setting to the same value."
|
||||
);
|
||||
|
||||
r.set_if_neq(R2(3));
|
||||
assert!(
|
||||
r.is_changed(),
|
||||
"Resource must be changed after setting to a different value."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Time>, mut query: Query<(Entity, &mut MyComponent)
|
|||
for (entity, mut component) in &mut query {
|
||||
if rand::thread_rng().gen_bool(0.1) {
|
||||
info!("changing component {:?}", entity);
|
||||
component.0 = time.elapsed_seconds();
|
||||
let new_component = MyComponent(time.elapsed_seconds().round());
|
||||
// Change detection occurs on mutable derefence,
|
||||
// and does not consider whether or not a value is actually equal.
|
||||
// To avoid triggering change detection when nothing has actually changed,
|
||||
// you can use the `set_if_neq` method on any component or resource that implements PartialEq
|
||||
component.set_if_neq(new_component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue