3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🦀ひとりRustとBevyでゲーム開発🕊️Advent Calendar 2024

Day 22

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

Last updated at Posted at 2024-12-22

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

マウス入力

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

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()を呼べば、そのウィンドウでのマウスポインタの座標を取得できます。以下はチートブックから拝借したサンプルコードです。

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()から取得して計算しています。

ワールド座標への変換

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

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

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

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

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,
    ) {
        ...
    }
}

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

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

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

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

参考文献

詳しくはチートブックを見よう!(他力本願)

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?