bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
36.04k stars 3.56k forks source link

Allow specifying which transform components are relative to the parent, and which are global #1780

Open vultix opened 3 years ago

vultix commented 3 years ago

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:

TransformDescriptor {
    translation: Local,
    rotation: Global,
    scale: Local
}

The transform_propagate_system could then look for this component and update the GlobalTransform 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

alice-i-cecile commented 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)

mockersf commented 3 years ago

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.

alice-i-cecile commented 3 years ago

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,
}
guccialex commented 3 years ago

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;
    }

}
JonahPlusPlus commented 3 years ago

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.

atlv24 commented 2 years ago

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

vultix commented 2 years ago

I should’ve been more clear - this was a 2D tank game

MatrixDev commented 1 year ago

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

wilcooo commented 1 year ago

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.

PraxTube commented 1 year ago
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.

RJ commented 1 year ago

@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)
         )
PraxTube commented 1 year ago

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.

musjj commented 1 year ago

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

PraxTube commented 1 year ago

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.

musjj commented 1 year ago

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

PraxTube commented 1 year ago

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);
        }
    }
}
honungsburk commented 1 year ago

@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();
        }
    }
}
Testare commented 10 months ago

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