Bevyでマウス位置を取得する

## はじめに

先日Bevyのバージョンが0.7になり、仕様変更に戸惑っていたRemint20です。今回はマウスカーソルの位置（ワールド座標系）を受け取り、スプライトをマウス方向に向けるプログラムを紹介します。

**完成品**

<iframe width="560" height="315" src="https://www.youtube.com/embed/lKyPBW1ZV1o" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

## セットアップ
マウスを取得するための簡単なセットアップをおこないましょう。ここではマウス位置を受け取る構造体と後程利用する関数を定義しておきます。

```rs
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .insert_resource(MousePos(Vec2::ZERO))
        .add_startup_system(setup)
        .add_system(cursor_system)
        .run();
}

struct MousePos(Vec2);

fn setup(mut commands: Commands) {}

fn cursor_system(){}
```

## カメラを設定
カーソルの位置を取得するにはカメラを設定しないといけません。今回は後でスプライトを表示するので`OrthographicCameraBundle::new_2d()`を指定します。カメラはプログラムが始まったときに設定してほしいので`setup()`内で設定します。`MainCamera`コンポーネントは`cursor_system()`でカメラを取得するために使います。

```rs
#[derive(Component)]
struct MainCamera;

fn setup(mut commands: Commands, assert_server: Res<AssetServer>) {
    commands
        .spawn()
        .insert_bundle(OrthographicCameraBundle::new_2d())
        .insert(MainCamera);
}
```

## マウスカーソル位置を取得

それではマウスカーソルを取得しましょう。最初に用意した`cursor_system()`を以下のコードに変換します。

```rs
fn cursor_system(
    wnds: Res<Windows>,
    mut mouse_pos: ResMut<MousePos>,
    camera_query: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
) {
    if let Ok((camera, camera_transform)) = camera_query.get_single() {
        if let Some(wnd) = wnds.get_primary() {
            // カメラがウィンドウ内にあるか確認して、その位置を取得する
            if let Some(screen_pos) = wnd.cursor_position() {
                // ウィンドウサイズの取得
                let window_size = Vec2::new(wnd.width() as f32, wnd.height() as f32);

                // 画面位置を[0..window_size] -> [-1..1]（gpu座標）に変換します
                let ndc = (screen_pos / window_size) * 2.0 - Vec2::ONE;

                // 投影とカメラ変換を元に戻すための行列
                let ndc_to_world =
                    camera_transform.compute_matrix() * camera.projection_matrix.inverse();

                // ワールド座標に変換
                let world_pos = ndc_to_world.project_point3(ndc.extend(-1.0));

                // Vec3 -> Vec2に変換
                let world_pos: Vec2 = world_pos.truncate();

                eprintln!("World Position: x:{} y:{}", world_pos.x, world_pos.y);
                
                // MousePos内に格納
                mouse_pos.0 = world_pos;
            }
        }
    }
}
```


### カメラとウィンドウの取得

`camera_query`では`Camera`と`GlobalTransform`を受け取っています。また、`With<MainCamera>`で`MainCamera`コンポーネントを持つオブジェクトだけを指定しています。With<>はフィルターの意味もありますが、今回の場合は`GlobalTransform`を受け取るためにも必要となります。
次にウィンドウの取得です。ウィンドウは基本的に一つしか使用しないと思われるので`get_primary()`で取得しています。もしほかのウィンドウを取得したい場合は`get()`を使用するといいでしょう。その代わりに`WindowId`を取得する必要が出てきますが...。
最後に`screen_pos`を受け取っています。取得したウィンドウの座標になります。

```rs
    if let Ok((camera, camera_transform)) = camera_query.get_single() {
        if let Some(wnd) = wnds.get_primary() {
            // カメラがウィンドウ内にあるか確認して、その位置を取得する
            if let Some(screen_pos) = wnd.cursor_position() {
            ...
```
### ウィンドウサイズを所得

これは特に説明はいりませんね。ウィンドウの横幅と縦幅を`Vec2`に変換しているだけです。

```rs
let window_size = Vec2::new(wnd.width() as f32, wnd.height() as f32);
```

### ウィンドウ座標 -> ワールド座標に変換
```rs
// [0..window_size]（画面位置） -> [-1..1]（gpu座標）に変換します
let ndc = (screen_pos / window_size) * 2.0 - Vec2::ONE;

// 投影とカメラ変換を元に戻すための行列
let ndc_to_world = camera_transform.compute_matrix() * camera.projection_matrix.inverse();

// ワールド座標に変換
let world_pos = ndc_to_world.project_point3(ndc.extend(-1.0));
```
はじめにウィンドウ座標をGPU座標に変換します。それぞれの座標は以下の図になります。

![ウィンドウ座標とGPU座標.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/414005/80fc7024-efd0-ab78-0732-b241e6d42b95.png)

:::note warn
他のゲームエンジンやGPUライブラリでは**左上が原点**になっていることが多いですがBevyでは**左下が原点**になります。
:::

最後にワールド座標に変換するための行列を用意して、それにGPU座標を入れて行列計算してもらいます。

これでマウスカーソルの位置をワールド座標系として取得することができます。

## スプライトをカーソル方向に向ける

では取得したカーソル位置を利用してオブジェクトを回転させてみましょう。初めにスプライトを用意します。用意するスプライトはなんでも構いません。私は[DOTOWN](https://dotown.maeda-design-room.net/category/town/)から取得しました。
`setup`を以下のコードに変換して、`Player`コンポーネントを追加しましょう。

```diff_rust
#[derive(Component)]
struct Player;

fn setup(mut commands: Commands, assert_server: Res<AssetServer>) {
    ...

    commands
        .spawn_bundle(SpriteBundle {
            transform: Transform {
                translation: Vec3::new(0.0, 0.0, 0.0),
                scale: Vec3::new(0.0625, 0.0625, 1.0),
                ..Default::default()
            },
            texture: assert_server.load("textures/うんちハニワ.png"),
            ..Default::default()
        })
        .insert(Player);
}
```

次に`look_at`関数を作成。ここではPlayerコンポーネントを持つオブジェクトをマウスカーソル方向に回転させる機能を持たせます。
これには`Transform`の`look_at`を使用しましょう。`look_at(&mut self, target: Vec3, up: Vec3)`で宣言されているので`from`に向けたい方向、`up`に回転したい軸を入れましょう。（実際はupはXでもYでも問題ない、`from`と`up`を逆にしても動きます。2次元の場合だけだと思うけど...）

```rs
fn look_at(mut query: Query<&mut Transform, With<Player>>, mouse_pos: Res<MousePos>) {
    if let Ok(mut tf) = query.get_single_mut() {
        tf.look_at(Vec3::Z, Vec3::from((mouse_pos.0, 0.)));
    }
}
```

最後に`app`に`add_system()`として読み込みます。
```diff_rust
App::new()
        .add_plugins(DefaultPlugins)
        .insert_resource(MousePos(Vec2::ZERO))
        .add_startup_system(setup)
        .add_system(cursor_system)
+        .add_system(look_at)
        .run();
```

実行して確かめてみましょう。


今回使用したコードは[Github](https://github.com/Remint20/bevy_examples)でも公開しています。
Bevyはまだまだ安定版が出ていません。なので**大きな仕様変更**があることに注意してください。