【Rustのまほう2】#22 マウス入力

マウス入力は少し説明することが増えます。というのも、ゲームにおいてはスクリーン座標系とワールド座標系という二つの座標系があり、しばしばそれらを相互に変換する必要があるからです。


# マウス入力

マウス入力を読み取ること自体は簡単です。以下は私の作っているゲームで、マウスボタンを押したときに魔法を発射するシステムのコードです（わかりやすいように少し改変して平坦にしています）。キーボード入力と同様にマウス入力もリソースになっているので、`Res<ButtonInput<MouseButton>>`というシステムパラメータを定義します。そして、`pressed`を呼べば指定したマウスボタンが現在押されているかどうかを判定できます。

```rust
fn fire(
    ...
    buttons: Res<ButtonInput<MouseButton>>,
    ...
) {
    ...
    
    player.fire_state = if get_fire_trigger(&buttons) {
        ActorFireState::Fire
    } else {
        ActorFireState::Idle
    };

    ...
}
```

他にも、マウスボタンがそのフレームで押され始めたかを判定する`just_pressed`などがあります。



# マウスポインタの座標

また、マウスポインタの座標を取得するには、`Window`コンポーネントを参照します。Bevyはマルチウインドウに対応していますのでウィンドウは複数存在することがありますが、メインのウィンドウを取得するにはクエリに`With<PrimaryWindow>`を指定して絞り込みます。それから、`cursor_position()`を呼べば、そのウィンドウでのマウスポインタの座標を取得できます。以下はチートブックから拝借したサンプルコードです。

```rust
fn cursor_position(
    q_windows: Query<&Window, With<PrimaryWindow>>,
) {
    // Games typically only have one window (the primary window)
    if let Some(position) = q_windows.single().cursor_position() {
        println!("Cursor is inside the primary window, at {:?}", position);
    } else {
        println!("Cursor is not in the game window.");
    }
}
```

私のゲームにおいては、インベントリのアイテムにマウスポインタを合わせた時にアイテムの説明をポップアップで表示しています。このポップアップの座標を`cursor_position()`から取得して計算しています。



<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">昨日作っていた、インベントリのアイテム情報ポップアップ表示です<br>これはひとりアドベントカレンダーの記事用動画ですわよ<br>投稿遅刻して泣きながら3日分まとめて記事書いてます<a href="https://twitter.com/hashtag/%E3%82%B2%E3%83%BC%E3%83%A0%E5%88%B6%E4%BD%9C?src=hash&amp;ref_src=twsrc%5Etfw">#ゲーム制作</a> <a href="https://t.co/zLkDQttaDI">https://t.co/zLkDQttaDI</a> <a href="https://t.co/GG2XBimWrQ">pic.twitter.com/GG2XBimWrQ</a></p>&mdash; Cubbit (@cubbit2) <a href="https://twitter.com/cubbit2/status/1870633048204824692?ref_src=twsrc%5Etfw">December 22, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


# ワールド座標への変換

さて、ここまではまあ通常のＧＵＩプログラミングと大差ないのですが、ゲームプログラミング特有の操作として、**スクリーン座標系とワールド座標系の相互変換**があります。

Bevyには大きくわけで、スクリーン座標系とワールド座標系があります。画面上のボタンやヒットポイント表示などのHUDは、ウィンドウ左上隅を原点とした**スクリーン座標系**に従って配置されています。それに対し、プレイヤーキャラクターのスプライトなどは**ワールド座標系**に従って配置されており、その座標系にカメラの座標系変換が施されたものが最終的な画面上のスプライトの位置になります。カメラはプレイヤーキャラクターが画面の中央に来るように設定されていますので、ワールド座標系の原点は通常はウィンドウの左上隅を遥かに超えたとこｒにあります。
また、スクリーン座標系においては長さ`1`は画面上の`1`ピクセルと一致しますが、ワールド座標系では長さ`1`は`1`ピクセルとは限らず、実際には私のゲームではデフォルトで長さ`1`が`2`ピクセルになるように拡大表示されています。ぜんぜん異なる種類の座標系が重なって表示されているわけです。

たとえば、私のゲームではスクリーン上をクリックすると、ワールド内のプレイヤーキャラクターがその座標を狙って魔法を発射するという仕組みになっています。このとき、先述の`cursor_position()`で取得した座標は、ウィンドウを左上を原点としたスクリーン座標系なので、魔法を発射した方向を決定するには、マウスポインタの座標をワールド座標系に変換する必要があります。

具体的には、まず`Query<(&Camera, &GlobalTransform)>`システムパラメータで`Camera`コンポーネントとカメラの`GlobalTransform`コンポーネントを取得します。それから、`camera.viewport_to_world`関数を読んで、マウスのスクリーン座標をワールド座標系に変換します。

```rust
fn drop(
    ...
    window_query: Query<&Window, With<PrimaryWindow>>,
    camera_query: Query<(&Camera, &GlobalTransform), (With<Camera2d>, Without<Player>)>,
    ...
){
    ...

    let (camera, camera_global_transform) = camera_query.single();
    if let Ok(mouse_in_world) = camera.viewport_to_world(
        camera_global_transform,
        cursor_in_screen,
    ) {
        ...
    }
}
```

https://bevy-cheatbook.github.io/cookbook/cursor2world.html

これで、あとはプレイヤーキャラクターのワールド座標からマウスポインタのワールド座標を減算すれば、魔法が発射される方向のベクトルが手に入るわけです。そういえば現在は高校数学から線形代数が外れちゃったみたいですね。このあたりのベクトル計算はゲームプログラミングでは必須なので、ゲームを作りたい高校生の人はぜひ線形代数というやつを勉強してみてください。



# ワールド座標系からスクリーン座標系への変換

逆に、ワールド座標系からスクリーン座標系へ変換したい場合もあります。私の作っているゲームでは、店員キャラクターに話しかけると頭の上にフキダシが表示されるのですが、キャラクターはワールド座標系上で配置されているので、その座標をスクリーン座標系へと変換してその位置にフキダシを表示しています。



<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">お店でのお買い物を実装しました<br>商品を持ち逃げしようとしたら大変なことになるアレもやろうかな……？<a href="https://twitter.com/hashtag/%E3%82%B2%E3%83%BC%E3%83%A0%E5%88%B6%E4%BD%9C?src=hash&amp;ref_src=twsrc%5Etfw">#ゲーム制作</a> <a href="https://t.co/vGAwCK7T67">pic.twitter.com/vGAwCK7T67</a></p>&mdash; Cubbit (@cubbit2) <a href="https://twitter.com/cubbit2/status/1868956848260628995?ref_src=twsrc%5Etfw">December 17, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


あー、今この動画を改めてみてみたら、キャラの位置とフキダシの位置が盛大にズレてますね。これはあとで直しました。

# 参考文献

詳しくはチートブックを見よう！（他力本願）

https://bevy-cheatbook.github.io/input/mouse.html