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 4 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::*;
Copy link
Member

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

1. Prefer granular imports over glob imports of `bevy::prelude::*` and `bevy::sub_crate::*`.

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

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 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 from bevy_ecs::prelude should be encouraged.

I think similarly for bevy_app::prelude could be argued, since bevy_app and bevy_ecs should be merged anyway, although the set used from bevy_app is more likely to be static.

Copy link
Member

@mockersf mockersf May 1, 2022

Choose a reason for hiding this comment

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

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.

Yeah, the example in the style guide is wrong, you never import bevy when working in bevy

I don't like that rule for bevy_ecs::prelude - granular imports from other crates are more reasonable (bevy_app is marginal)

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

Copy link
Member

Choose a reason for hiding this comment

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

#4644 discusses this change.

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()
},
);
}
}
}