Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add set_if_neq method to DetectChanges trait #5373

Closed
wants to merge 13 commits into from

Conversation

alice-i-cecile
Copy link
Member

@alice-i-cecile alice-i-cecile commented Jul 19, 2022

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:

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

@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use labels Jul 19, 2022
@alice-i-cecile
Copy link
Member Author

@aevyrie IIRC you've wanted this functionality :)

@aevyrie
Copy link
Member

aevyrie commented Jul 19, 2022

Maybe out of scope, but I think the real problem is that mutable dereferences of Mut are implicit. In fact, I think this current implementation might suffer from the exact footgun I'm thinking of here, derefing the Mut for comparison will trigger change detection, negating the purpose of this comparison. I would need to do some testing, but off the top of my head I recall needing to use *resource.as_ref() to prevent a deref of the Mut during comparison.

At minimum, I think you need to add a test here that verifies set_if_differs does not trigger a change detection.

@alice-i-cecile
Copy link
Member Author

Agreed, working on a test here :)

@alice-i-cecile
Copy link
Member Author

alice-i-cecile commented Jul 19, 2022

@maniwani, do you have any insight into how to manually update the change tick correctly? The test is currently failing because the resource is being set as Changed upon spawn and I can't seem to get it to update the change tick manually.

World::increment_change_ticks didn't work 😢

Obviously I can do something more realistic like use a schedule, but I'd like to understand what's going on.

EDIT: world.increment_change_ticks is updating the values correctly. I'm nervous that manually fetched resources do not have the correct change tick values.

@alice-i-cecile alice-i-cecile marked this pull request as draft July 19, 2022 01:57
@alice-i-cecile alice-i-cecile marked this pull request as ready for review July 19, 2022 03:28
@alice-i-cecile
Copy link
Member Author

@aevyrie tests are passing now and the UX is as expected :) Some real fancy bounds to get that working; thanks @BoxyUwU.

Copy link
Member

@aevyrie aevyrie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really useful. Just have some non-blocking suggestions for the comments.

crates/bevy_ecs/src/change_detection.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/change_detection.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/change_detection.rs Outdated Show resolved Hide resolved
Copy link
Member

@DJMcNab DJMcNab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure I'd seen this in another PR, although can't locate it at the moment.

This seems fine. Possibly it should come as part of a wider conversation, but I don't have any suggestions for a real alternative.

crates/bevy_ecs/src/change_detection.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/change_detection.rs Outdated Show resolved Hide resolved
@alice-i-cecile
Copy link
Member Author

Possibly it should come as part of a wider conversation, but I don't have any suggestions for a real alternative.

Following #5007 we could consider doing fake-specialization with associated types, and then allow users to choose whether to use naive DerefMut, DerefMut + PartialEq or none for change detection.

That's a much bigger and more controversial design (even if I agree with it); this is just intended to be a nice little helper for now.

@aevyrie
Copy link
Member

aevyrie commented Jul 19, 2022

🚲 🏠 time. Would set_if_neq perhaps be a bit more precise, especially considering it's literally a neq test?

@alice-i-cecile
Copy link
Member Author

It's more precise, but also more jargon-y. Does get bonus points for brevity though.

I'm not sure which one I prefer. I would have gone with set_if_not_equal, but that's super verbose for a convenience method.

@aevyrie
Copy link
Member

aevyrie commented Jul 19, 2022

It's more precise, but also more jargon-y. Does get bonus points for brevity though.

Normally I'd agree, I always attempt to err on the side of explicitness. However, eq, neq, and their cousins are already used everywhere in std. I'll leave it at that. It really isn't that important, and the current name makes sense.

@alice-i-cecile alice-i-cecile requested a review from aevyrie July 19, 2022 23:48
@aevyrie
Copy link
Member

aevyrie commented Jul 20, 2022

@alice-i-cecile remember to update the title and original post with the changed method name. :)

@alice-i-cecile alice-i-cecile changed the title Add set_if_differs method to DetectChanges trait Add set_if_neq method to DetectChanges trait Jul 20, 2022
@alice-i-cecile alice-i-cecile requested a review from maniwani July 20, 2022 13:58
Copy link
Member

@DJMcNab DJMcNab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we not use the encapsulated pattern anywhere in the engine?

Should this PR replace those instances if so - at the very least the choice not to do so needs to be mentioned somewhere.

Self: Deref<Target = T> + DerefMut<Target = T>,
T: PartialEq,
{
// This dereference is immutable, so does not trigger change detection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first reading, I wasn't a huge fan of the term immutable here. However, it seems to be the standard terminology, so it's fine.

Copy link
Member

@mockersf mockersf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes looks OK, but it seems very hard to discover. Is there an example where it could be used?

@alice-i-cecile
Copy link
Member Author

Yeah, I should do a pass through and see both a) are there any internal usages and b) is there a good example to put this in?

@aevyrie
Copy link
Member

aevyrie commented Jul 24, 2022

The change detection example might be a good place for this: https://github.com/bevyengine/bevy/blob/main/examples/ecs/component_change_detection.rs

@alice-i-cecile
Copy link
Member Author

I found this pattern used in a handful of places internally, but could only find a couple of places where I could use this method directly. There are two other patterns that aren't covered:

  1. Side effects accompany the change, and we need to guard the field.
  2. We're only mutating a single field of a component. We could use struct update syntax and then set_if_neq, but this will be somewhat wasteful. This typically occurs with Transform.

@JoJoJet
Copy link
Member

JoJoJet commented Nov 26, 2022

  • Side effects accompany the change, and we need to guard the field.

Can you give an example of this pattern?

  • We're only mutating a single field of a component. We could use struct update syntax and then set_if_neq, but this will be somewhat wasteful. This typically occurs with Transform.

We should be able to use Mut::map_unchanged, which might be nicer in some cases. It can be saved for a follow-up PR, though.

@alice-i-cecile alice-i-cecile added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Nov 26, 2022
@alice-i-cecile
Copy link
Member Author

bors r+

bors bot pushed a commit that referenced this pull request Nov 28, 2022
# 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).
@bors
Copy link
Contributor

bors bot commented Nov 28, 2022

Build failed:

@alice-i-cecile
Copy link
Member Author

This super isn't compiling ATM: I'm going to fix up the merge locally.

Comment on lines +98 to +100
if **self != value {
**self = value;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps explicit <Self as Deref<Target = T>>::deref(*self, ...) and <Self as DerefMut<Target = T>>::deref_mut(*self, ...) would be beneficial for clarity?

This pattern may be common, but it's rather opaque if you aren't aware that Rust will not cache one DerefMut implicitly to share across both references to **self.
It's also a bit clearer than trying to explain that one is "immutable" as a prior commenter mentioned, as it becomes explicit; then you can just comment "triggers change tracking" or similar on the second one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to weigh in, I think it's fine as-is.

@alice-i-cecile
Copy link
Member Author

Closing in favor of #6853, which fixed the rebase for me <3

bors bot pushed a commit that referenced this pull request Dec 11, 2022
# 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>
alradish pushed a commit to alradish/bevy that referenced this pull request Jan 22, 2023
…e#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 <Dessix@Dessix.net>
ItsDoot pushed a commit to ItsDoot/bevy that referenced this pull request Feb 1, 2023
…e#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 <Dessix@Dessix.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants