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] - Add an extension trait to EntityCommands to update hierarchy while preserving GlobalTransform #7024

Closed
Closed
Show file tree
Hide file tree
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
3 changes: 2 additions & 1 deletion crates/bevy_hierarchy/src/child_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ impl Command for RemoveChildren {

/// Command that removes the parent of an entity, and removes that entity from the parent's [`Children`].
pub struct RemoveParent {
child: Entity,
/// `Entity` whose parent must be removed.
pub child: Entity,
}

impl Command for RemoveParent {
Expand Down
99 changes: 99 additions & 0 deletions crates/bevy_transform/src/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//! Extension to [`EntityCommands`] to modify [`bevy_hierarchy`] hierarchies
//! while preserving [`GlobalTransform`].

use bevy_ecs::{prelude::Entity, system::Command, system::EntityCommands, world::World};
use bevy_hierarchy::{AddChild, RemoveParent};

#[cfg(doc)]
use bevy_hierarchy::BuildChildren;

use crate::{GlobalTransform, Transform};

/// Command similar to [`AddChild`], but updating the child transform to keep
/// it at the same [`GlobalTransform`].
///
/// You most likely want to use [`BuildChildrenTransformExt::set_parent_in_place`]
/// method on [`EntityCommands`] instead.
pub struct AddChildInPlace {
/// Parent entity to add the child to.
pub parent: Entity,
/// Child entity to add.
pub child: Entity,
}
impl Command for AddChildInPlace {
fn write(self, world: &mut World) {
let hierarchy_command = AddChild {
child: self.child,
parent: self.parent,
};
hierarchy_command.write(world);
let mut update_transform = || {
nicopap marked this conversation as resolved.
Show resolved Hide resolved
let parent = *world.get_entity(self.parent)?.get::<GlobalTransform>()?;
let child_global = *world.get_entity(self.child)?.get::<GlobalTransform>()?;
let mut child_entity = world.get_entity_mut(self.child)?;
let mut child = child_entity.get_mut::<Transform>()?;
*child = child_global.reparented_to(&parent);
Some(())
};
update_transform();
}
}
/// Command similar to [`RemoveParent`], but updating the child transform to keep
/// it at the same [`GlobalTransform`].
///
/// You most likely want to use [`BuildChildrenTransformExt::remove_parent_in_place`]
/// method on [`EntityCommands`] instead.
pub struct RemoveParentInPlace {
/// `Entity` whose parent must be removed.
pub child: Entity,
}
impl Command for RemoveParentInPlace {
fn write(self, world: &mut World) {
let hierarchy_command = RemoveParent { child: self.child };
hierarchy_command.write(world);
let mut update_transform = || {
nicopap marked this conversation as resolved.
Show resolved Hide resolved
let child_global = *world.get_entity(self.child)?.get::<GlobalTransform>()?;
let mut child_entity = world.get_entity_mut(self.child)?;
let mut child = child_entity.get_mut::<Transform>()?;
*child = child_global.compute_transform();
Some(())
};
update_transform();
}
}
/// Collection of methods similar to [`BuildChildren`], but preserving each
/// entity's [`GlobalTransform`].
pub trait BuildChildrenTransformExt {
/// Change this entity's parent while preserving this entity's [`GlobalTransform`]
/// by updating its [`Transform`].
///
/// See [`BuildChildren::set_parent`] for a method that doesn't update the
/// [`Transform`].
///
/// Note that both the hierarchy and transform updates will only execute
/// at the end of the current stage.
fn set_parent_in_place(&mut self, parent: Entity) -> &mut Self;

/// Make this entity parentless while preserving this entity's [`GlobalTransform`]
/// by updating its [`Transform`] to be equal to its current [`GlobalTransform`].
///
/// See [`BuildChildren::remove_parent`] for a method that doesn't update the
/// [`Transform`].
///
/// Note that both the hierarchy and transform updates will only execute
/// at the end of the current stage.
fn remove_parent_in_place(&mut self) -> &mut Self;
}
impl<'w, 's, 'a> BuildChildrenTransformExt for EntityCommands<'w, 's, 'a> {
fn remove_parent_in_place(&mut self) -> &mut Self {
let child = self.id();
self.commands().add(RemoveParentInPlace { child });
self
}

fn set_parent_in_place(&mut self, parent: Entity) -> &mut Self {
let child = self.id();
self.commands().add(AddChildInPlace { child, parent });
self
}
}
5 changes: 4 additions & 1 deletion crates/bevy_transform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
#![warn(clippy::undocumented_unsafe_blocks)]
#![doc = include_str!("../README.md")]

pub mod commands;
/// The basic components of the transform crate
pub mod components;
mod systems;

#[doc(hidden)]
pub mod prelude {
#[doc(hidden)]
pub use crate::{components::*, TransformBundle, TransformPlugin};
pub use crate::{
commands::BuildChildrenTransformExt, components::*, TransformBundle, TransformPlugin,
};
}

use bevy_app::prelude::*;
Expand Down
235 changes: 235 additions & 0 deletions examples/transforms/global_vs_local_translation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
//! Illustrates the difference between direction of a translation in respect to local object or
//! global object Transform.
nicopap marked this conversation as resolved.
Show resolved Hide resolved

use bevy::{math::Vec3A, prelude::*};

// Define a marker for entities that should be changed via their global transform.
#[derive(Component)]
struct ChangeGlobal;

// Define a marker for entities that should be changed via their local transform.
#[derive(Component)]
struct ChangeLocal;

// Define a marker for entities that should move.
#[derive(Component)]
struct Move;

// Define a resource for the current movement direction;
#[derive(Resource, Default)]
struct Direction(Vec3);

// Define component to decide when an entity should be ignored by the movement systems.
#[derive(Component)]
struct ToggledBy(KeyCode);

#[derive(Resource)]
struct DynamicParent {
green: Entity,
yellow: Entity,
has_hierarchy: bool,
}

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_startup_system(setup)
.init_resource::<Direction>()
.add_system(move_cubes_according_to_global_transform)
.add_system(move_cubes_according_to_local_transform)
.add_system(update_directional_input)
.add_system(toggle_movement)
.run();
}

// Startup system to setup the scene and spawn all relevant entities.
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
asset_server: Res<AssetServer>,
) {
let mut green = None;
// To show the difference between a local transform (rotation, scale and
// position in respect to a given entity) and global transform (rotation,
// scale and position in respect to the base coordinate system of the visible scene)
// it's helpful to add multiple entities that are attached to each other.
// This way we'll see that the transform in respect to an entity's parent is different to the
// global transform within the visible scene.
// This example focuses on translation only to clearly demonstrate the differences.

// Spawn a basic cube to have an entity as reference.
let yellow = commands
.spawn((
PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
material: materials.add(StandardMaterial {
base_color: Color::YELLOW,
alpha_mode: AlphaMode::Blend,
..Default::default()
}),
..default()
},
ChangeLocal,
Move,
ToggledBy(KeyCode::Key1),
))
// Spawn two entities as children above the original main entity.
// The red entity spawned here will be changed via its global transform
// where the green one will be changed via its local transform.
.with_children(|child_builder| {
// also see parenting example
child_builder.spawn((
PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 0.5 })),
material: materials.add(StandardMaterial {
base_color: Color::RED,
alpha_mode: AlphaMode::Blend,
..Default::default()
}),
transform: Transform::from_translation(Vec3::Y - Vec3::Z),
..default()
},
ChangeGlobal,
Move,
ToggledBy(KeyCode::Key2),
));
let green_content = child_builder
.spawn((
PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 0.5 })),
material: materials.add(StandardMaterial {
base_color: Color::GREEN,
alpha_mode: AlphaMode::Blend,
..Default::default()
}),
transform: Transform::from_translation(Vec3::Y + Vec3::Z),
..default()
},
ChangeLocal,
Move,
ToggledBy(KeyCode::Key3),
))
.id();
green = Some(green_content);
})
.id();
commands.insert_resource(DynamicParent {
green: green.unwrap(),
yellow,
has_hierarchy: true,
});

// Spawn a camera looking at the entities to show what's happening in this example.
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(0.0, 10.0, 20.0).looking_at(Vec3::ZERO, Vec3::Y),
..default()
});

// Add a light source for better 3d visibility.
commands.spawn(PointLightBundle {
transform: Transform::from_translation(Vec3::splat(3.0)),
..default()
});

// Add text to explain inputs and what is happening.
commands.spawn(TextBundle::from_section(
"Press the arrow keys to move the cubes. \
Toggle movement for yellow (1), red (2) and green (3) cubes via number keys.\n\n\
Notice how the green cube will translate further in respect to the \
yellow in contrast to the red cube.\n\
This is due to the use of its LocalTransform that is relative to the \
yellow cubes transform instead of the GlobalTransform as in the case of the red cube.\n\
The red cube is moved through its GlobalTransform and thus is \
unaffected by the yellows transform.\n\
You can toggle the parent relationship between the green and yellow cubes using (4)",
nicopap marked this conversation as resolved.
Show resolved Hide resolved
TextStyle {
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
font_size: 22.0,
color: Color::WHITE,
},
));
}

// This system will move all cubes that are marked as ChangeGlobal according to their global transform.
fn move_cubes_according_to_global_transform(
mut cubes: Query<&mut GlobalTransform, (With<ChangeGlobal>, With<Move>)>,
direction: Res<Direction>,
time: Res<Time>,
) {
for mut global_transform in &mut cubes {
*global_transform.translation_mut() += Vec3A::from(direction.0) * time.delta_seconds();
}
}

// This system will move all cubes that are marked as ChangeLocal according to their local transform.
fn move_cubes_according_to_local_transform(
mut cubes: Query<&mut Transform, (With<ChangeLocal>, With<Move>)>,
direction: Res<Direction>,
time: Res<Time>,
) {
for mut transform in &mut cubes {
transform.translation += direction.0 * time.delta_seconds();
}
}

// This system updates a resource that defines in which direction the cubes should move.
// The direction is defined by the input of arrow keys and is only in left/right and up/down direction.
fn update_directional_input(mut direction: ResMut<Direction>, keyboard_input: Res<Input<KeyCode>>) {
let horizontal_movement = Vec3::X
* (keyboard_input.pressed(KeyCode::Right) as i32
- keyboard_input.pressed(KeyCode::Left) as i32) as f32;
let vertical_movement = Vec3::Y
* (keyboard_input.pressed(KeyCode::Up) as i32
- keyboard_input.pressed(KeyCode::Down) as i32) as f32;
direction.0 = horizontal_movement + vertical_movement;
}

// This system enables and disables the movement for each entity if their assigned key is pressed.
fn toggle_movement(
mut commands: Commands,
movable_entities: Query<(Entity, &Handle<StandardMaterial>, &ToggledBy), With<Move>>,
static_entities: Query<(Entity, &Handle<StandardMaterial>, &ToggledBy), Without<Move>>,
mut materials: ResMut<Assets<StandardMaterial>>,
keyboard_input: Res<Input<KeyCode>>,
mut dynamic_parent: ResMut<DynamicParent>,
) {
if keyboard_input.just_pressed(KeyCode::Key4) {
if dynamic_parent.has_hierarchy {
commands
.entity(dynamic_parent.green)
.remove_parent_in_place();
} else {
commands
.entity(dynamic_parent.green)
.set_parent_in_place(dynamic_parent.yellow);
}
dynamic_parent.has_hierarchy = !dynamic_parent.has_hierarchy;
}
// Update the currently movable entities and remove their Move component if
// the assigned key was pressed to disable their movement. This will also
// make them transparent so they can be identified as 'disabled' in the scene.
for (entity, material_handle, toggled_by) in &movable_entities {
if keyboard_input.just_pressed(toggled_by.0) {
materials
.get_mut(material_handle)
.unwrap()
.base_color
.set_a(0.5);
commands.entity(entity).remove::<Move>();
}
}
// Update the currently non-movable entities and add a Move component if
// the assigned key was pressed to enable their movement. This will also
// make them opaque so they can be identified as 'enabled' in the scene.
for (entity, material_handle, toggled_by) in &static_entities {
if keyboard_input.just_pressed(toggled_by.0) {
materials
.get_mut(material_handle)
.unwrap()
.base_color
.set_a(1.0);
commands.entity(entity).insert(Move);
}
}
}