マウス入力は少し説明することが増えます。というのも、ゲームにおいてはスクリーン座標系とワールド座標系という二つの座標系があり、しばしばそれらを相互に変換する必要があるからです。
マウス入力
マウス入力を読み取ること自体は簡単です。以下は私の作っているゲームで、マウスボタンを押したときに魔法を発射するシステムのコードです(わかりやすいように少し改変して平坦にしています)。キーボード入力と同様にマウス入力もリソースになっているので、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()
から取得して計算しています。
昨日作っていた、インベントリのアイテム情報ポップアップ表示です
— Cubbit (@cubbit2) December 22, 2024
これはひとりアドベントカレンダーの記事用動画ですわよ
投稿遅刻して泣きながら3日分まとめて記事書いてます#ゲーム制作 https://t.co/zLkDQttaDI pic.twitter.com/GG2XBimWrQ
ワールド座標への変換
さて、ここまではまあ通常のGUIプログラミングと大差ないのですが、ゲームプログラミング特有の操作として、スクリーン座標系とワールド座標系の相互変換があります。
Bevyには大きくわけで、スクリーン座標系とワールド座標系があります。画面上のボタンやヒットポイント表示などのHUDは、ウィンドウ左上隅を原点としたスクリーン座標系に従って配置されています。それに対し、プレイヤーキャラクターのスプライトなどはワールド座標系に従って配置されており、その座標系にカメラの座標系変換が施されたものが最終的な画面上のスプライトの位置になります。カメラはプレイヤーキャラクターが画面の中央に来るように設定されていますので、ワールド座標系の原点は通常はウィンドウの左上隅を遥かに超えたとこrにあります。
また、スクリーン座標系においては長さ1
は画面上の1
ピクセルと一致しますが、ワールド座標系では長さ1
は1
ピクセルとは限らず、実際には私のゲームではデフォルトで長さ1
が2
ピクセルになるように拡大表示されています。ぜんぜん異なる種類の座標系が重なって表示されているわけです。
たとえば、私のゲームではスクリーン上をクリックすると、ワールド内のプレイヤーキャラクターがその座標を狙って魔法を発射するという仕組みになっています。このとき、先述の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,
) {
...
}
}
これで、あとはプレイヤーキャラクターのワールド座標からマウスポインタのワールド座標を減算すれば、魔法が発射される方向のベクトルが手に入るわけです。そういえば現在は高校数学から線形代数が外れちゃったみたいですね。このあたりのベクトル計算はゲームプログラミングでは必須なので、ゲームを作りたい高校生の人はぜひ線形代数というやつを勉強してみてください。
ワールド座標系からスクリーン座標系への変換
逆に、ワールド座標系からスクリーン座標系へ変換したい場合もあります。私の作っているゲームでは、店員キャラクターに話しかけると頭の上にフキダシが表示されるのですが、キャラクターはワールド座標系上で配置されているので、その座標をスクリーン座標系へと変換してその位置にフキダシを表示しています。
お店でのお買い物を実装しました
— Cubbit (@cubbit2) December 17, 2024
商品を持ち逃げしようとしたら大変なことになるアレもやろうかな……?#ゲーム制作 pic.twitter.com/vGAwCK7T67
あー、今この動画を改めてみてみたら、キャラの位置とフキダシの位置が盛大にズレてますね。これはあとで直しました。
参考文献
詳しくはチートブックを見よう!(他力本願)