Ensure GlobalTransform consistency when Parent is spawned without Children#3340
Ensure GlobalTransform consistency when Parent is spawned without Children#3340yilinwei wants to merge 3 commits intobevyengine:mainfrom
GlobalTransform consistency when Parent is spawned without Children#3340Conversation
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
| 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.
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.
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. |
alice-i-cecile
left a comment
There was a problem hiding this comment.
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.
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.
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 |
mirkoRainer
left a comment
There was a problem hiding this comment.
LGTM. Test passes as expected and change is fairly straightforward. 👍
DJMcNab
left a comment
There was a problem hiding this comment.
I like the test, but I agree that the actual mechanism of using a temporary component for this is unneccessary.
| >, | ||
| 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.
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.
I did have a discussion about this when implementing it. The options were:
- Add a marker component
- Remove the automatic insertion of the
Childrenresource, so that it fails in a way which is obvious to the user (howTransformandGlobalTransformbehave) - Recalculate when the
Childrenchange.
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.
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.
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.
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.
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
Transformhas changed - If the
Transformhas changed, then update theGlobalTransformand theGlobalTransformof 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
truewhen mutatingChildrenlater 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 <36049421+DJMcNab@users.noreply.github.com>
…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>
…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>
…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>
…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>
Objective
Closes #3329
Solution
parent_updateandtransform_propagaterun in the same stage butparent_updatecan spawnChildren. This means that thesystemcan enter an inconsistent state where theGlobalTransformhas not been updated onChildrenwhen spawning anEntitywhere theParentdoes not have an existingChildrencomponent.Introduce a marker trait,
DirtyParent, so that the system will revert to the correct state the next time the systems run.