Open vultix opened 3 years ago
More generally, this feels like we want better tools to override the standard parent-child transform propagation behavior. Something specialized like this could work, or more comfortable tools to create analogous custom behavior (likely relations).
I think the latter is my preferred solution, at least to start with.
Query for the parent's transform and subtract that from the turret's transform
This sort of behavior is exactly what we'd like to make easy with relations.
(cc: @BoxyUwU)
An example use case is a tank with a child turret entity. The turret should inherit location and scale from the tank, but should rotate independently.
Currently, with the turret as a child of the tank:
What you want (if I understand correctly) is that the turret won't rotate when the tank does? That feels counter-(physics)-intuitive to me, it feels like the turret should rotate with the tank, and the player must apply a counter rotation to the turret if they want it to target the same angle.
Maybe helper methods taking the GlobalTransform
to place an object in the global world through its Transform
would better suit your need? Or not have the turret a children of the tank, but a sibling, with a parent taking the translation/scale, and then you can apply the rotation only to the part you want.
From @BoxyUwU, with a relations model, we could define this behavior as part of the data stored on each relation, with the current behavior as the default.
struct ChildOf {
despawn_on_clean: bool,
propagate_pos: bool,
propagate_rot: bool,
propagate_scale: bool,
}
In my case, I want my healthbar children to be 2 units above their parent entity and facing a certain direction no matter the parent's rotation.
What works for me is to set the "Transformation" of the healthbar children to the default zero values. Then add a system after the PostUpdate stage that edits their GlobalTransform.
.add_stage_after(CoreStage::PostUpdate, "globaltransformediting", SystemStage::single_threaded())
.add_system_to_stage("globaltransformediting", update_health_bar_positions.system())
fn update_health_bar_positions(
mut healthbars: Query< ( &mut GlobalTransform, &HealthBarTag)>,
) {
for (mut globaltransform, _) in healthbars.iter_mut(){
globaltransform.rotation = Quat::from_rotation_y( 0.0 );
globaltransform.translation.y = globaltransform.translation.y + 2.0;
}
}
For me, I need a child entity with a sky box component attached to the camera's position, but not rotating. Since this is a crate, I want to make it flexible so users don't have to do any complicated boilerplate; just slap the plugin and component (the component spawns a child entity and deletes itself) on and that's it. At the same time, I don't want to add code that just cancels out some value.
I think @vultix's solution looks nice, but I also think it might be good to make it even more flexible by making it affect specific axis, to allow for advanced constraints, like joints. Though, in that case, a bitmask, or a struct that has a hidden bitmask and methods to interact with it, may be better for so many fields. We could go a step further, and allow for movement in a particular direction, so something like TRANSLATION_X_FORWARD (or with methods: .enable_trans_x_forward()). Though, frankly, it would be over the top, with 33 constants. I would love to hear more about the relations idea from @alice-i-cecile. How I would imagine it would be using closures like so:
some_child_entity.map_translation(Some(|parent, child| {
child.x = parent.x * 2;
child.y = parent.z;
// child.z isn't used, it stays constant.
}
None could either mean use the default (child.x = parent.x; child.y = parent.y; child.z = parent.z;) or just doesn't do anything.
For the tank example - isnt the rotation locking behavior entirely broken as soon as the tank climbs a ramp? Seems that you would want the up axis to stay locked to the tank body, otherwise the turret will stay parallel to the ground.
For the sky box component - wouldnt it be better to use a fullscreen quad at the far clip plane + calculate ray directions at the corners and use an interpolator then cubemap sample in the frag shader? or is this not an ordinary cubemap texture that needs manual filtering for some reason
I should’ve been more clear - this was a 2D tank game
I have more or less the same problem but with the health bar. I want it to always have the same position as tank but rotation must be static (or updated separately when something happens).
Currently I'm adding a marker component on the tank and than a separate system creates/updates/destroys root health entites when markers are present based on their GlobalTransform. This is not ideal for performance but it works.
Ability for child entities to selectively ignore translate/rotate/scale would be great though...
The ability to disable transform propagation is necessary for using parent-child relations when working with the bevy_rapier2d
physics library. The physics engine is then in control of the transforms of both the parent and the children, so the transform propagation interferes.
For example, it seems logical to me that the wheels of a car are children of the car itself (connected using a joint).
commands
.spawn((CarBody, PhysicsStuffs, ...))
.with_children(|children| {
children.spawn((Wheel, PhysicsStuffs, ImpulseJoint::new(children.parent_entity(), ...), ...));
children.spawn((Wheel, PhysicsStuffs, ImpulseJoint::new(children.parent_entity(), ...), ...));
})
However, for now we have to do this:
let car_body = commands.spawn((CarBody, PhysicsStuffs, ...)).id();
children.spawn((Wheel, PhysicsStuffs, ImpulseJoint::new(car_body, ...), ...));
children.spawn((Wheel, PhysicsStuffs, ImpulseJoint::new(car_body, ...), ...));
and take extra care when deleting the car.
for (mut globaltransform, _) in healthbars.iter_mut(){ globaltransform.rotation = Quat::from_rotation_y( 0.0 ); globaltransform.translation.y = globaltransform.translation.y + 2.0; }
Seems like this doesn't work anymore. The bevy docs say
GlobalTransform is fully managed by bevy, you cannot mutate it, use Transform instead.
How would one go about resetting the rotation to zero now? I tried to use the approach were you reset the rotation of the child by subtracting the parents rotation, but that didn't work correctly.
@PraxTube here's a system from my code that fixes label rotations:
/// there is no way to inherit position but not rotation from the parent entity transform yet
/// see: https://github.com/bevyengine/bevy/issues/1780
/// so labels will rotate with ships unless we fiddle with it:
fn fix_entity_label_rotations(
mut q_text: Query<(&Parent, &mut Transform), With<FollowLabelChild>>,
q_parents: Query<(&Transform, &FollowLabel), Without<FollowLabelChild>>,
) {
for (parent, mut transform) in q_text.iter_mut() {
if let Ok((parent_transform, fl)) = q_parents.get(parent.get()) {
if !fl.inherit_rotation {
// global transform propagation system will make the rotation 0 now
transform.rotation = parent_transform.rotation.inverse();
}
}
}
}
edit: scheduled like this
app.add_systems(
PostUpdate,
fix_entity_label_rotations
.before(propagate_transforms)
)
Hey @RJ, thanks for the example, it works! So I think the reason my simple subtraction didn't work was because Quats are not real (R) but complex (C)? Therefore just subtracting the rotation doesn't work. Either way, using inverse()
works perfectly, thanks again.
@RJ This seems to work, but for some reason transformations on the children will still be applied relative to the parent's local rotation.
So, for example, the direction of positive Y translations will depend on the angle on the parent.
Yeah, say you have a local offset of the child (some translation) and you rotate your parent while resetting the child rotation, then your child will still move around the player.
You would have to recalculate the local offset based on the parent rotation.
You would have to recalculate the local offset based on the parent rotation.
Do you have any tips on how you'd approach that? Not really good with vector math :sweat_smile:.
You can use
fn reset_rotations(
mut q_transforms: Query<(&Parent, &mut Transform, &NoRotation)>,
q_parents: Query<&Transform, Without<NoRotation>>,
) {
for (parent, mut transform, no_rotation) in q_transforms.iter_mut() {
if let Ok(parent_transform) = q_parents.get(parent.get()) {
let rot_inv = parent_transform.rotation.inverse();
transform.rotation = rot_inv;
transform.translation = rot_inv.mul_vec3(no_rotation.offset);
}
}
}
@RJ Which version of bevy are you on?
The way you schedule the systems:
app.add_systems(
PostUpdate,
fix_entity_label_rotations
.before(propagate_transforms)
)
causes bevy to crash with error:
thread 'main' panicked at 'Tried to order against `fn "SystemTypeSet(propagate_transforms\")"`
in a schedule that has more than one `"SystemTypeSet(propagate_transforms\")"` instance.
`fn "SystemTypeSet(propagate_transforms\")"` is a `SystemTypeSet` and cannot be used for
ordering if ambiguous. Use a different set without this restriction.',
This is how I solved it:
use bevy::{prelude::*, transform::TransformSystem};
pub struct NoRotationPlugin;
impl Plugin for NoRotationPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PostStartup,
update_fransform_no_rotation.before(TransformSystem::TransformPropagate),
)
.add_systems(
PostUpdate,
update_fransform_no_rotation.before(TransformSystem::TransformPropagate),
);
}
}
// Placed on the child entity, this component will cause the child to have its transform
// updated to match the parent entity's transform. But it will not inherit the parent's
// rotation.
#[derive(Component)]
pub struct NoRotationChild;
// Placed on the parent entity, this component will cause the parent to have its transform
// updated to match the child entity's transform. But it will not inherit the child's
// rotation.
#[derive(Component)]
pub struct NoRotationParent;
/// there is no way to inherit position but not rotation from the parent entity transform yet
/// see: https://github.com/bevyengine/bevy/issues/1780
/// so labels will rotate with ships unless we fiddle with it:
/// TODO: remove this when the issue is fixed second
pub fn update_fransform_no_rotation(
mut q_text: Query<(&Parent, &mut Transform), With<NoRotationChild>>,
q_parents: Query<&Transform, (With<NoRotationParent>, Without<NoRotationChild>)>,
) {
for (parent, mut transform) in q_text.iter_mut() {
if let Ok(parent_transform) = q_parents.get(parent.get()) {
// global transform propagation system will make the rotation 0 now
transform.rotation = parent_transform.rotation.inverse();
}
}
}
As discussed in a help thread from somebody who wants to be able to position a child entity absolutely (link), I should point out for the discussion here that subtracting a parent's transform from your desired transform doesn't make your transform static unless the parent entity has no parents of its own. Otherwise, you're positioned relative to your grandparent.
In the case of the tank game, I'm imagining you want rotation of the turret to be independent because you went to use a mouse or joystick to aim. I could imagine a case where you have something like a rotating platform, or your tank is on an aircraft carrier. Perhaps that's a bit of a stretch, but if you wanted truly absolute rotation you'd have to iterate through all ancestors and subtract their transform, and this system had to execute after any of those transforms change in the frame but before transform propagation. You could just use your parent's GlobalTransform, but you would be stuck with an unavoidable one - frame lag behind updates, so you're only really absolutely positioned so long as your parent entity doesn't move.
Hence, I think there is a good argument to incorporate the ability to limit transform propagation instead of just tools for manually reversing it
What problem does this solve or what need does it fill?
An example use case is a tank with a child turret entity. The turret should inherit location and scale from the tank, but should rotate independently.
What solution would you like?
I believe this can be implemented by adding a new optional component to the turret:
The
transform_propagate_system
could then look for this component and update theGlobalTransform
accordingly.Note: I don't really like the name
TransformDescriptor
- can anyone think of better?What alternative(s) have you considered?
There are two alternatives:
Both of these are painful to use and don't take advantage of the ECS.
Additional context
This is a feature that UE4 has