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
@ -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
|
/// Normally change detecting is triggered by either [`DerefMut`] or [`AsMut`], however
|
||||||
/// it can be manually triggered via [`DetectChanges::set_changed`].
|
/// 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::*;
|
/// use bevy_ecs::prelude::*;
|
||||||
///
|
///
|
||||||
@ -90,6 +97,17 @@ pub trait DetectChanges {
|
|||||||
/// However, it can be an essential escape hatch when, for example,
|
/// 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.
|
/// you are trying to synchronize representations using change detection and need to avoid infinite recursion.
|
||||||
fn bypass_change_detection(&mut self) -> &mut Self::Inner;
|
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 {
|
macro_rules! change_detection_impl {
|
||||||
@ -132,6 +150,19 @@ macro_rules! change_detection_impl {
|
|||||||
fn bypass_change_detection(&mut self) -> &mut Self::Inner {
|
fn bypass_change_detection(&mut self) -> &mut Self::Inner {
|
||||||
self.value
|
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),*> {
|
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 {
|
fn bypass_change_detection(&mut self) -> &mut Self::Inner {
|
||||||
&mut self.value
|
&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<'_> {
|
impl std::fmt::Debug for MutUntyped<'_> {
|
||||||
@ -458,12 +502,17 @@ mod tests {
|
|||||||
world::World,
|
world::World,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Component)]
|
use super::DetectChanges;
|
||||||
|
|
||||||
|
#[derive(Component, PartialEq)]
|
||||||
struct C;
|
struct C;
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
struct R;
|
struct R;
|
||||||
|
|
||||||
|
#[derive(Resource, PartialEq)]
|
||||||
|
struct R2(u8);
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn change_expiration() {
|
fn change_expiration() {
|
||||||
fn change_detected(query: Query<ChangeTrackers<C>>) -> bool {
|
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.
|
// Modifying one field of a component should flag a change for the entire component.
|
||||||
assert!(component_ticks.is_changed(last_change_tick, change_tick));
|
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 crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiStack};
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
|
change_detection::DetectChanges,
|
||||||
entity::Entity,
|
entity::Entity,
|
||||||
prelude::Component,
|
prelude::Component,
|
||||||
query::WorldQuery,
|
query::WorldQuery,
|
||||||
@ -179,10 +180,8 @@ pub fn ui_focus_system(
|
|||||||
Some(*entity)
|
Some(*entity)
|
||||||
} else {
|
} else {
|
||||||
if let Some(mut interaction) = node.interaction {
|
if let Some(mut interaction) = node.interaction {
|
||||||
if *interaction == Interaction::Hovered
|
if *interaction == Interaction::Hovered || (cursor_position.is_none()) {
|
||||||
|| (cursor_position.is_none() && *interaction != Interaction::None)
|
interaction.set_if_neq(Interaction::None);
|
||||||
{
|
|
||||||
*interaction = Interaction::None;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@ -227,8 +226,8 @@ pub fn ui_focus_system(
|
|||||||
while let Some(node) = iter.fetch_next() {
|
while let Some(node) = iter.fetch_next() {
|
||||||
if let Some(mut interaction) = node.interaction {
|
if let Some(mut interaction) = node.interaction {
|
||||||
// don't reset clicked nodes because they're handled separately
|
// don't reset clicked nodes because they're handled separately
|
||||||
if *interaction != Interaction::Clicked && *interaction != Interaction::None {
|
if *interaction != Interaction::Clicked {
|
||||||
*interaction = Interaction::None;
|
interaction.set_if_neq(Interaction::None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ fn main() {
|
|||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component, Debug)]
|
#[derive(Component, PartialEq, Debug)]
|
||||||
struct MyComponent(f32);
|
struct MyComponent(f32);
|
||||||
|
|
||||||
fn setup(mut commands: Commands) {
|
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 {
|
for (entity, mut component) in &mut query {
|
||||||
if rand::thread_rng().gen_bool(0.1) {
|
if rand::thread_rng().gen_bool(0.1) {
|
||||||
info!("changing component {:?}", entity);
|
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 New Issue
Block a user