Bevy rotation in 2D
Asked Answered
D

3

6

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?

Discomfiture answered 19/12, 2020 at 14:32 Comment(0)
B
7

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.

Bloodstock answered 19/12, 2020 at 14:54 Comment(3)
Right, that's not what I'm trying to do, I want the player to be "looking at" the mouse, so basically just rotate to always be facing the mouse. I was using look_at with the mouse as a target, hoping that'd change the rotation to be facing the cursor's position, but instead it just makes the player disappear. I am not touching the camera yet, but I'll have it follow the player later onDiscomfiture
@Discomfiture Ah I see, I misinterpreted what you wanted. I've updated the answer :)Bloodstock
if the position is 0,0 or the target and position are the same, this will result in NaN and an error.Guffaw
K
11

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);
Korfonta answered 25/8, 2021 at 19:55 Comment(1)
Wanted to rewrite top answer with atan because it was not working, saw your and it did worked. ThanksHibernia
B
7

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.

Bloodstock answered 19/12, 2020 at 14:54 Comment(3)
Right, that's not what I'm trying to do, I want the player to be "looking at" the mouse, so basically just rotate to always be facing the mouse. I was using look_at with the mouse as a target, hoping that'd change the rotation to be facing the cursor's position, but instead it just makes the player disappear. I am not touching the camera yet, but I'll have it follow the player later onDiscomfiture
@Discomfiture Ah I see, I misinterpreted what you wanted. I've updated the answer :)Bloodstock
if the position is 0,0 or the target and position are the same, this will result in NaN and an error.Guffaw
T
0

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

Transeunt answered 26/10, 2023 at 15:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.