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

Ensure GlobalTransform consistency when Parent is spawned without Children #3340

Closed
wants to merge 3 commits into from

Conversation

yilinwei
Copy link
Contributor

Objective

Closes #3329

Solution

parent_update and transform_propagate run in the same stage but parent_update can spawn Children. This means that the system can enter an inconsistent state where the GlobalTransform has not been updated on Children when spawning an Entity where the Parent does not have an existing Children component.

Introduce a marker trait, DirtyParent, so that the system will revert to the correct state the next time the systems run.

@github-actions github-actions bot added the S-Needs-Triage This issue needs to be labelled label Dec 15, 2021
`parent_update` and `transform_propagate` run in the same stage but `parent_update` can spawn `Children`. This means that the `system` can enter an inconsistent state where the `GlobalTransform` has not been updated on `Children` when spawning an `Entity` where the `Parent` does not have an existing `Children` component.

Introduce a marker trait, `DirtyParent`, so that the system will revert to the correct state the next time the systems run.
@alice-i-cecile alice-i-cecile added A-Transform Translations, rotations and scales C-Bug An unexpected or incorrect behavior S-Needs-Review and removed S-Needs-Triage This issue needs to be labelled labels Dec 15, 2021
@alice-i-cecile
Copy link
Member

Thanks for the fix :) I'll dig into the details now. Could you swap the title of this PR over to something more descriptive, like "Ensure GlobalTransform consistency when spawning children"?

@yilinwei yilinwei changed the title Closes #3329 Ensure GlobalTransform consistency when Parent is spawned without Children Dec 15, 2021
mut transform_query: Query<(&Transform, &mut GlobalTransform), With<Parent>>,
changed_transform_query: Query<Entity, Changed<Transform>>,
children_query: Query<Option<&Children>, (With<Parent>, With<GlobalTransform>)>,
) {
for (entity, children, transform, mut global_transform) in root_query.iter_mut() {
let mut changed = false;
if changed_transform_query.get(entity).is_ok() {
if changed_transform_query.get(entity).is_ok() || dirty_parent_query.get(entity).is_ok() {
Copy link
Member

Choose a reason for hiding this comment

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

Like you suggested on Discord, I think this is going to be cleaner with an Option<&DirtyParent> query parameter, rather than creating a new query entirely.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've switched this to use an Or and the same changed_transform_query, wdyt?

@alice-i-cecile
Copy link
Member

Looks good! Thanks for the fix; the code quality, tests and debugging are excellent for a new contributor. I left a couple of nits; once those are cleaned up this will have my approval.

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Looks good to me :) The Or filter is nicer, I'm glad you suggested that idea.

@BorisBoutillier
Copy link
Contributor

Not a usual reviewer, but I have reviewed the changes and approves them.

@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 Jan 1, 2022
Comment on lines +70 to +71
// Mark the entity with a `DirtyParent` component so that systems which run
// in the same stage have a way to query for a missed update.
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like this comment is more appropriate on the struct declaration. Right now the only explanation as to what DirtyParent means is here but the struct itself has no documentation.

Copy link
Contributor Author

@yilinwei yilinwei Jan 2, 2022

Choose a reason for hiding this comment

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

I'm not certain the struct declaration is a great place for this particular bit of documentation - DirtyParent is a quirk of both the parent_update_system and how the stages are currently run; the comment is there because it's not obvious why you'd add it in that particular code branch.

As to the wider point, I don't know what the documentation on the declaration should achieve. The problem is quite a niche one - you need to spawn your components in a particular way and even then, you'll only touch DirtyParent in the specific case that you're running a system in the same stage. If you're looking at DirtyParent directly, it's likely that you'll want to read through the code itself.

Arguably it should be part of the documentation on the whole hierarchy system, since that would be where the "entrypoint" to this behaviour would be.

@Davier
Copy link
Contributor

Davier commented Jan 1, 2022

Could it be done by using set_changed() on the dirty parent's transform instead of inserting a new component?

@yilinwei
Copy link
Contributor Author

yilinwei commented Jan 2, 2022

@Davier I don't think so, but I'm not too familiar with the semantics of set_changed.

  • The parent_update_system runs before the transform_propagate_system (which is where we add the new Children and mark it with DirtyParent).
  • The Transform is already changed for that particular frame.
  • The Children component is not visible on that frame.

@Davier
Copy link
Contributor

Davier commented Jan 2, 2022

@Davier I don't think so, but I'm not too familiar with the semantics of set_changed.

You're right, I had misunderstood the issue

Copy link
Contributor

@mirkoRainer mirkoRainer left a comment

Choose a reason for hiding this comment

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

LGTM. Test passes as expected and change is fairly straightforward. 👍

@alice-i-cecile alice-i-cecile added this to the Bevy 0.6.1 milestone Jan 15, 2022
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 like the test, but I agree that the actual mechanism of using a temporary component for this is unneccessary.

@@ -13,7 +14,7 @@ pub fn transform_propagate_system(
Without<Parent>,
>,
mut transform_query: Query<(&Transform, &mut GlobalTransform), With<Parent>>,
changed_transform_query: Query<Entity, Changed<Transform>>,
changed_transform_query: Query<Entity, Or<(Changed<Transform>, With<DirtyParent>)>>,
Copy link
Member

Choose a reason for hiding this comment

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

I think Changed<Children> would be a better filter here; in that case we could avoid the entire DirtyParent bookkeeping.

I'm not actually sure that (without doing that) any changes to Children which didn't also change the child's parent would result in transforms being updated; unless there is a system similar to update_parents but the other way around.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did have a discussion about this when implementing it. The options were:

  1. Add a marker component
  2. Remove the automatic insertion of the Children resource, so that it fails in a way which is obvious to the user (how Transform and GlobalTransform behave)
  3. Recalculate when the Children change.

My preference was 1. or 2. The reasoning was that Children feels like a general Component since it's first-class in the API and using it when there are Parent/Child relationships is encouraged.

IMO, it shouldn't really trigger a Transform update; not merely for optimization purposes, but I don't like the idea of the user receiving the GlobalTransform update events each time a child is added or removed. I think that would be confusing.

Copy link
Member

Choose a reason for hiding this comment

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

I suppose what actually needs to happen is that Changed<Children> needs to be reacted to by re-calculating the children, but 'self' doesn't need to be recalculated (unless it already needed to be). The tree has changed, so the transformations of the subtree need updating.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't quite follow; could you please elaborate?

I think in general Changed<Children> is too coarse a filter; under normal operation if a child is changed, there shouldn't be a reason to recalculate the GlobalTransform of it's siblings. It's only in this specific case, where we "lose" the insertion since we're in the same stage that we need to react to it.

Copy link
Member

Choose a reason for hiding this comment

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

Well my point is: if you add a chilld, how does the child's transform get updated? Neither the parent or child's transforms have changed, so that filter won't activate; so changed will be false, so it won't activate.

If you don't want to update 'self''s transform then, the way to implement this would be to add a changed_children query.
If this solution is implemented, it cleanly sidesteps the need for the messy DirtyParent component.

Copy link
Contributor Author

@yilinwei yilinwei Jan 18, 2022

Choose a reason for hiding this comment

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

OK I don't understand the solution at all; at least I can't think of a way of doing it without recalculating the siblings which haven't changed.

This is how I think that the algorithm currently works.

  1. Go to the root nodes
  2. Walk down each node checking if the Transform has changed
  3. If the Transform has changed, then update the GlobalTransform and the GlobalTransform of the Children

In the problematic case, the Children has been created in this stage so it doesn't see the child when walking down the tree. In the frame afterwards, neither the parent nor child have an updated Transform but has a Changed<Children> which would evaluate to true.

The Changed<Children> query:

  1. Can't distinguish which child has changed, which means we'd have to update all the siblings since we need to mark the parent. Note, this is also true of DirtyParent, but since it only lasts for a single frame, that doesn't matter since any changes during that single frame needs to be calculated anyway.
  2. Will also evaluate to true when mutating Children later as well.

I cannot see how a single Changed<Children> would be able to sidestep both the issues?

@yilinwei
Copy link
Contributor Author

I think there's another method without the marker component. We could do a dummy update of the Transform using commands.insert which would apply on the same frame as the children insertion; but that would overwrite any user mutations for that frame.

@alice-i-cecile alice-i-cecile removed this from the Bevy 0.6.1 milestone Feb 10, 2022
@alice-i-cecile alice-i-cecile removed 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 Apr 26, 2022
@alice-i-cecile
Copy link
Member

I'm going to block this on seeing if we can get @DJMcNab's suggestion to do this without a temporary component to work.

Reviewers: if you can submit a PR to this branch to fix that, it would be greatly appreciated.

@alice-i-cecile
Copy link
Member

alice-i-cecile commented Apr 26, 2022

Closing in favor of #4608. This was really valuable foundation, but I think that the approach without the temporary component is substantially more useful.

bors bot pushed a commit that referenced this pull request May 2, 2022
…ren (#4608)

Supercedes #3340, and absorbs the test from there.

# Objective

- Fixes #3329

## Solution

- If the `Children` component has changed, we currently do not have a way to know how it has changed.
- Therefore, we must update the hierarchy downwards  from that point to be correct.

Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
bors bot pushed a commit that referenced this pull request May 2, 2022
…ren (#4608)

Supercedes #3340, and absorbs the test from there.

# Objective

- Fixes #3329

## Solution

- If the `Children` component has changed, we currently do not have a way to know how it has changed.
- Therefore, we must update the hierarchy downwards  from that point to be correct.

Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
bors bot pushed a commit that referenced this pull request May 2, 2022
…ren (#4608)

Supercedes #3340, and absorbs the test from there.

# Objective

- Fixes #3329

## Solution

- If the `Children` component has changed, we currently do not have a way to know how it has changed.
- Therefore, we must update the hierarchy downwards  from that point to be correct.

Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
exjam pushed a commit to exjam/bevy that referenced this pull request May 22, 2022
…ren (bevyengine#4608)

Supercedes bevyengine#3340, and absorbs the test from there.

# Objective

- Fixes bevyengine#3329

## Solution

- If the `Children` component has changed, we currently do not have a way to know how it has changed.
- Therefore, we must update the hierarchy downwards  from that point to be correct.

Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
ItsDoot pushed a commit to ItsDoot/bevy that referenced this pull request Feb 1, 2023
…ren (bevyengine#4608)

Supercedes bevyengine#3340, and absorbs the test from there.

# Objective

- Fixes bevyengine#3329

## Solution

- If the `Children` component has changed, we currently do not have a way to know how it has changed.
- Therefore, we must update the hierarchy downwards  from that point to be correct.

Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Transform Translations, rotations and scales C-Bug An unexpected or incorrect behavior
Projects
None yet
Development

Successfully merging this pull request may close these issues.

GlobalTransform not being updated
8 participants