-
-
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
[Merged by Bors] - Make Transform
propagation correct in the presence of updated children
#4608
Changes from 4 commits
0977aba
471cc92
bed4091
ef08753
a5509de
9591e78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,5 @@ | ||
use crate::components::{GlobalTransform, Transform}; | ||
use bevy_ecs::{ | ||
entity::Entity, | ||
prelude::Changed, | ||
query::{With, Without}, | ||
system::Query, | ||
}; | ||
use bevy_ecs::prelude::*; | ||
use bevy_hierarchy::{Children, Parent}; | ||
|
||
/// Update [`GlobalTransform`] component of entities based on entity hierarchy and | ||
|
@@ -13,6 +8,7 @@ pub fn transform_propagate_system( | |
mut root_query: Query< | ||
( | ||
Option<&Children>, | ||
Option<Changed<Children>>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? I thought we'd want to support roots without children - this would be quite a serious regression. I could change it to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, then i suppose using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand the concern
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe, but it's introduced and used as a filter most of the time. I feel like it's bad practice to mix filters with regular queries, given filters like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would also like to forbid the use of filters as ordinary query parameters down the line if we can; and this will be one more case that we'd have to revert. That said, I don't feel strongly, and won't block on it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, so adding a I don't hate it, although it seems quite a bit less ergonomic, especially when you need to know about multiple filters. |
||
&Transform, | ||
Changed<Transform>, | ||
&mut GlobalTransform, | ||
|
@@ -23,18 +19,21 @@ pub fn transform_propagate_system( | |
(&Transform, Changed<Transform>, &mut GlobalTransform), | ||
With<Parent>, | ||
>, | ||
children_query: Query<Option<&Children>, (With<Parent>, With<GlobalTransform>)>, | ||
children_query: Query<(&Children, Changed<Children>), (With<Parent>, With<GlobalTransform>)>, | ||
) { | ||
for (children, transform, transform_changed, mut global_transform) in root_query.iter_mut() { | ||
let mut changed = false; | ||
for (children, maybe_changed_children, transform, transform_changed, mut global_transform) in | ||
root_query.iter_mut() | ||
{ | ||
// TODO: Use `Option::contains` when stable | ||
let mut changed = maybe_changed_children == Some(true); | ||
if transform_changed { | ||
*global_transform = GlobalTransform::from(*transform); | ||
changed = true; | ||
} | ||
|
||
if let Some(children) = children { | ||
for child in children.iter() { | ||
propagate_recursive( | ||
let _ = propagate_recursive( | ||
&global_transform, | ||
&mut transform_query, | ||
&children_query, | ||
|
@@ -52,44 +51,42 @@ fn propagate_recursive( | |
(&Transform, Changed<Transform>, &mut GlobalTransform), | ||
With<Parent>, | ||
>, | ||
children_query: &Query<Option<&Children>, (With<Parent>, With<GlobalTransform>)>, | ||
children_query: &Query<(&Children, Changed<Children>), (With<Parent>, With<GlobalTransform>)>, | ||
entity: Entity, | ||
mut changed: bool, | ||
) { | ||
// We use a result here to use the `?` operator. Ideally we'd use a try block instead | ||
) -> Result<(), ()> { | ||
let global_matrix = { | ||
if let Ok((transform, transform_changed, mut global_transform)) = | ||
transform_query.get_mut(entity) | ||
{ | ||
changed |= transform_changed; | ||
if changed { | ||
*global_transform = parent.mul_transform(*transform); | ||
} | ||
*global_transform | ||
} else { | ||
return; | ||
let (transform, transform_changed, mut global_transform) = | ||
transform_query.get_mut(entity).map_err(drop)?; | ||
|
||
changed |= transform_changed; | ||
if changed { | ||
*global_transform = parent.mul_transform(*transform); | ||
} | ||
*global_transform | ||
}; | ||
|
||
if let Ok(Some(children)) = children_query.get(entity) { | ||
for child in children.iter() { | ||
propagate_recursive( | ||
&global_matrix, | ||
transform_query, | ||
children_query, | ||
*child, | ||
changed, | ||
); | ||
} | ||
let (children, changed_children) = children_query.get(entity).map_err(drop)?; | ||
changed |= changed_children; | ||
for child in children.iter() { | ||
let _ = propagate_recursive( | ||
&global_matrix, | ||
transform_query, | ||
children_query, | ||
*child, | ||
changed, | ||
); | ||
} | ||
Ok(()) | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use bevy_ecs::{ | ||
schedule::{Schedule, Stage, SystemStage}, | ||
system::{CommandQueue, Commands}, | ||
world::World, | ||
}; | ||
use bevy_app::prelude::*; | ||
use bevy_ecs::prelude::*; | ||
use bevy_ecs::system::CommandQueue; | ||
use bevy_math::vec3; | ||
|
||
use crate::components::{GlobalTransform, Transform}; | ||
use crate::systems::transform_propagate_system; | ||
|
@@ -271,4 +268,60 @@ mod test { | |
vec![children[1]] | ||
); | ||
} | ||
|
||
#[test] | ||
fn correct_transforms_when_no_children() { | ||
let mut app = App::new(); | ||
|
||
app.add_system(parent_update_system); | ||
app.add_system(transform_propagate_system); | ||
|
||
let translation = vec3(1.0, 0.0, 0.0); | ||
|
||
let parent = app | ||
.world | ||
.spawn() | ||
.insert(Transform::from_translation(translation)) | ||
.insert(GlobalTransform::default()) | ||
.id(); | ||
|
||
let child = app | ||
.world | ||
.spawn() | ||
.insert_bundle(( | ||
Transform::identity(), | ||
GlobalTransform::default(), | ||
Parent(parent), | ||
)) | ||
.id(); | ||
|
||
let grandchild = app | ||
.world | ||
.spawn() | ||
.insert_bundle(( | ||
Transform::identity(), | ||
GlobalTransform::default(), | ||
Parent(child), | ||
)) | ||
.id(); | ||
|
||
app.update(); | ||
|
||
// check the `Children` structure is spawned | ||
assert_eq!(&**app.world.get::<Children>(parent).unwrap(), &[child]); | ||
assert_eq!(&**app.world.get::<Children>(child).unwrap(), &[grandchild]); | ||
// Note that at this point, the `GlobalTransform`s will not have updated yet, due to `Commands` delay | ||
app.update(); | ||
|
||
let mut state = app.world.query::<&GlobalTransform>(); | ||
for global in state.iter(&app.world) { | ||
assert_eq!( | ||
global, | ||
&GlobalTransform { | ||
translation, | ||
..Default::default() | ||
}, | ||
); | ||
} | ||
} | ||
} |
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.
Why this change?
The style guide mention preferring granular imports
bevy/.github/contributing/engine_style_guide.md
Line 9 in 7d21ca3
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 wasn't aware of that guideline. I made this change here because I think I needed something else in the import, and importing the prelude is significantly less prone to merge conflicts overall.
As a note, that style guide seems to only care about cases where you have a dependency on the root
bevy
facade, not when depending directly on the sub crates.I'm not sure that I really like that guideline - merge conflicts in the imports create a ton of busywork, since the tooling requires a lot of manual handholding.
I can change it back though.
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 like that rule for
bevy_ecs::prelude
- granular imports from other crates are more reasonable (bevy_app
is marginal)bevy_ecs
's types are the standard vocabulary of bevy, and there should be very few source files which don't use any of them (and where the set used is static). That is, I think glob importing frombevy_ecs::prelude
should be encouraged.I think similarly for
bevy_app::prelude
could be argued, sincebevy_app
andbevy_ecs
should be merged anyway, although the set used frombevy_app
is more likely to be static.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.
Yeah, the example in the style guide is wrong, you never import bevy when working in bevy
Maybe, but I don't think this PR is the place to change that and I would rather have consistency until we decide to change the rule
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.
#4644 discusses this change.