Bevy rotation in 2D

5.2k views Asked by At

I'm trying to use Bevy 0.3, and I'm able to use the built-in transforms easily with Camera2dComponents::default(). This is top-down 2D.

The issue is trying to synchronise the player's rotation to the mouse:

for event in evreader.iter(&cursor_moved_events) {
    println!("{:?}", transform);
    transform.look_at(Vec3::new(event.position.x(), event.position.y(), 0.0), Vec3::new(0.0, 0.0, 1.0));
    println!("{:?}", transform);
}

In this transform is my player's Transform, of course. Here's what this outputs:

Transform { translation: Vec3(0.0, 0.0, 0.0), rotation: Quat(0.0, 0.0, 0.0, 1.0), scale: Vec3(1.0, 1.0, 1.0) }
Transform { translation: Vec3(0.0, 0.0, 0.0), rotation: Quat(0.5012898, -0.49870682, -0.49870682, 0.5012898), scale: Vec3(1.0, 1.0, 1.0) }

I'm a bit confused by what up is in look_at in 2D, but I tried a few different values and the result is always the same: as soon as that look_at runs, the player disappears from view.

Why is the camera not seeing the player anymore after that, and what am I doing wrong with this look_at?

3

There are 3 answers

3
vallentin On BEST ANSWER

While you're using Camera2dComponents which is intended for 2D, Transform is not. Transform is more general, and includes what's needed for 3D as well.

In your case, it sounds like look_at() might not do what you think it does. It creates a 3D look at matrix which computes a view matrix which is "at" a position (self.translation) and looks towards a target (the first argument). The up is then a 3D vector of what the "world up" is. So that it can calculate the rotation correction.


Updated Solution:

If you want to rotate the top-down 2D player, i.e. having one side of the player look towards the cursor. Then you need to calculate the angle between the position of your player and the target. You can do that easily using Vec2::angle_between().

let pos: Vec2 = ...;
let target: Vec2 = ...;

let angle = (target - pos).angle_between(pos);
transform.rotation = Quat::from_rotation_z(angle);

Depending on the orientation of the player itself, i.e. if you're using a sprite, the default orientation could be "looking" to the left or up. Then you might need to offset the angle. You can do that by e.g. adding + FRAC_PI_2 (or however much is needed.)

Assuming transform.translation is the center position of the player, then it will look something like this:

use bevy::math::{Quat, Vec2};

let pos = transform.translation.truncate();
let target = event.position;

let angle = (target - pos).angle_between(pos);
transform.rotation = Quat::from_rotation_z(angle);

Old Answer:

Since you want to position your camera over the cursor in a top-down 2D view. Then instead you have to use from_translation() or simply shift self.translation, which is the position of your camera.

transform.translation = Vec3::new(event.position.x(), event.position.y(), 0.0);

If you want the cursor position to be at the center of the screen, then you might have to subtract half the screen size.

0
Virgileo On

Posting as accepted solution wasn't completely correct. The idea is that we have positions of player and cursor, but for rotations we need direction vectors:

  • Direction of where the player is currently looking
  • Direction pointing from player to cursor

Finding angle between these two directions will give us delta to rotate the player by:

fn rotate_player_to_cursor(
    mut player: Query<&mut Transform, With<Player>>,
    window: Query<&Window, With<PrimaryWindow>>,
    camera: Query<(&Camera, &GlobalTransform), With<MainCamera>>
) {
    // Considering all these tags to have one unique entity each
    let player_transform = player.single();
    let (camera, camera_transform) = camera.single();
    let window = window.single();

    // Extract cursor position if it is inside window
    if let Some(cursor_pos) = window 
        .cursor_position()
        .and_then(|cursor| camera.viewport_to_world_2d(camera_transform, cursor))
    {
        // In 2d up is positive y, so current forward direction for the player
        // is local_y unit vector
        let player_dir = player_transform.local_y().truncate();

        // Direction to cursor (what we want local_y to become) is simply the
        // difference of target position and player position
        let cursor_dir = cursor_pos - player_transform.translation.truncate();
        
        // Then we find the angle between current forward direction and desired one
        let angle = player_dir.angle_between(cursor_dir);
        
        // And finally rotate player along z with that difference
        player_transform.rotate_z(angle);
    }
}

1
wannabe On

For future users I had a similar problem (having a 2d sprite face a mouse click location) in Bevy v0.5 and I could not get the accepted answer to work for whatever reason.

I solved it via this implementation in a Unity thread: https://answers.unity.com/questions/650460/rotating-a-2d-sprite-to-face-a-target-on-a-single.html

Code example:

let diff = targ.translation - pos.translation;
let angle = diff.y.atan2(diff.x); // Add/sub FRAC_PI here optionally
pos.rotation = Quat::from_axis_angle(Vec3::new(0., 0., 1.), angle);