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

[Merged by Bors] - Make Transform propagation correct in the presence of updated children #4608

Closed
wants to merge 6 commits into from
Closed
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 90 additions & 37 deletions crates/bevy_transform/src/systems.rs
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::{Changed, Entity, Query, With, Without};
use bevy_hierarchy::{Children, Parent};

/// Update [`GlobalTransform`] component of entities based on entity hierarchy and
Expand All @@ -13,6 +8,7 @@ pub fn transform_propagate_system(
mut root_query: Query<
(
Option<&Children>,
Option<Changed<Children>>,
Copy link
Member

Choose a reason for hiding this comment

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

Instead of Option<Changed<Children>>, ChangeTrackers<Children> should be used.

Copy link
Member Author

Choose a reason for hiding this comment

The 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 Option<ChangeTrackers<Children>> of course if you prefer, although I would have thought that getting the changed only would be easier.

Copy link
Member

Choose a reason for hiding this comment

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

Hm, then i suppose using Option<ChangeTrackers<Children>> would be better. I'd rather not mix filters in with stuff meant to be queried for, given the effect mixing other filters like With can have.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure I understand the concern

Changed is merely a query which returns true if the component value has changed since the last time the system was run.

Copy link
Member

Choose a reason for hiding this comment

The 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 With do very different things.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, so adding a FilterMatches WorldQuery for the cases when you need the information the filter gives dynamically.

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,
Expand All @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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()
},
);
}
}
}