-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Conversation
crates/bevy_transform/src/hierarchy/hierarchy_maintenance_system.rs
Outdated
Show resolved
Hide resolved
`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.
1a4fcdc
to
6732003
Compare
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"? |
GlobalTransform
consistency when Parent
is spawned without Children
crates/bevy_transform/src/hierarchy/hierarchy_maintenance_system.rs
Outdated
Show resolved
Hide resolved
crates/bevy_transform/src/hierarchy/hierarchy_maintenance_system.rs
Outdated
Show resolved
Hide resolved
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() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
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. |
There was a problem hiding this 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.
Not a usual reviewer, but I have reviewed the changes and approves them. |
// 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Could it be done by using |
@Davier I don't think so, but I'm not too familiar with the semantics of
|
You're right, I had misunderstood the issue |
There was a problem hiding this 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. 👍
There was a problem hiding this 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>)>>, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- Add a marker component
- Remove the automatic insertion of the
Children
resource, so that it fails in a way which is obvious to the user (howTransform
andGlobalTransform
behave) - 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
- Go to the root nodes
- Walk down each node checking if the
Transform
has changed - If the
Transform
has changed, then update theGlobalTransform
and theGlobalTransform
of theChildren
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:
- 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. - Will also evaluate to
true
when mutatingChildren
later as well.
I cannot see how a single Changed<Children>
would be able to sidestep both the issues?
I think there's another method without the marker component. We could do a dummy update of the |
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. |
Closing in favor of #4608. This was really valuable foundation, but I think that the approach without the temporary component is substantially more useful. |
…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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
…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 <[email protected]>
Objective
Closes #3329
Solution
parent_update
andtransform_propagate
run in the same stage butparent_update
can spawnChildren
. This means that thesystem
can enter an inconsistent state where theGlobalTransform
has not been updated onChildren
when spawning anEntity
where theParent
does not have an existingChildren
component.Introduce a marker trait,
DirtyParent
, so that the system will revert to the correct state the next time the systems run.