🦀

chip7. キー、マウス、ゲームパッドでカメラを動かす

2023/08/11に公開

はじめに

2024/10/06時点の内容です。

  • rustc 1.81.0
  • bevy 0.14.2
    bevyは開発初期段階のOSSで、まだまだ破壊的なアップデートが入ります。
    でも、面白いですよ。

前回

chip6. 3Dオブジェクト「宝箱」を表示する

入力と動きの対応表

カメラの動きと入力(キー、マウス、ゲームパッド)の対応は下表のとおりです。
アナログスティック/トリガーでは押し込み具合が反映されます。

カメラの動き キー マウス ゲームパッド
仰角アップ 上矢印 右ボタン&下へ移動 十字ボタン上/左スティック上
仰角ダウン 下矢印 右ボタン&上へ移動 十字ボタン下/左スティック下
右回り込み 右矢印 右ボタン&左へ移動 十字ボタン右/左スティック右
左回り込み 左矢印 右ボタン&右へ移動 十字ボタン左/左スティック左
ズームイン ホイール下回転 左ショルダーボタン/左トリガー
ズームアウト ホイール上回転 右ショルダーボタン/右トリガー

マウスだけ他の操作と向きが逆になっています。
他はカメラを移動している感覚なんですけど、マウスだけ対象を摘まんで動かす感覚が強いので‥‥。

入力でカメラを動かす


入力に従って動きます
ソースコードは3つに分けました。
main.rsは小さくなっています。move_camera.rsが今回の本丸。spawn_chest3d.rsは前回(chip6.)と同じです。

main.rs
//external crates
use bevy::
{   prelude::*,
    log::LogPlugin,
    color::palettes::*,
    input::mouse::{ MouseMotion, MouseWheel },
};

//standard library
use std::f32::consts::{ TAU, PI }; //360°、180°

//internal sub-modules
mod spawn_chest3d;  //3Dオブジェクトのspawn
use spawn_chest3d::*;
mod move_camera;    //極座標カメラの移動
use move_camera::*;

//メイン関数
fn main() -> AppExit
{   App::new()
        .add_plugins
        (   DefaultPlugins //アプリの基本的な面倒を見てもらう
                .set( LogPlugin { filter: "error".into(), ..default() } ),
        )
        .add_systems( Startup, spawn_chest3d )    //3Dオブジェクトを作る
        .add_systems( Update, move_orbit_camera ) //極座標カメラを動かす
        .run()
}

複数のデバイスから入力があった場合 全部加算すると変化量が大きくなってしまうので少し悩んだんですが、平均を計算して加えるようにしました。

move_camera.rs
use super::*;

//極座標の型
#[derive( Clone, Copy )]
pub struct Orbit
{   r    : f32, //中心とカメラの距離
    theta: f32, //中心から見たカメラの垂直角度θ
    phi  : f32, //中心から見たカメラの水平角度Φ
}

impl Orbit
{   //極座標から直交座標へ変換
    pub fn into_vec3( self ) -> Vec3
    {   let x = self.r * self.theta.sin() * self.phi.sin();
        let y = self.r * self.theta.cos() * -1.0;
        let z = self.r * self.theta.sin() * self.phi.cos();
        Vec3::new( x, y, z )
    }
}

//極座標カメラのComponent
#[derive( Component )]
pub struct OrbitCamera { pub orbit: Orbit }

impl Default for OrbitCamera
{   fn default() -> Self
    {   Self { orbit: Orbit { r: 2.5, theta: PI * 0.75, phi: 0.0 } }
    }
}

//極座標カメラを動かす
pub fn move_orbit_camera
(   mut qry_camera: Query<( &mut OrbitCamera, &mut Transform )>,
    time: Res<Time>,
    //Key
    input_key: Res<ButtonInput<KeyCode>>,
    //Mouse
    input_mouse: Res<ButtonInput<MouseButton>>,
    mut evt_mouse_motion: EventReader<MouseMotion>,
    mut evt_mouse_wheel : EventReader<MouseWheel>,
    //Gamepad
    input_button: Res<ButtonInput<GamepadButton>>,
    gamepads    : Res<Gamepads>,
    axis_stick  : Res<Axis<GamepadAxis>>,
    axis_button : Res<Axis<GamepadButton>>,
)
{   //準備
    let Ok ( ( mut camera, mut transform ) ) = qry_camera.get_single_mut() else { return };
    let orbit = &mut camera.orbit;
    let time_delta = time.delta().as_secs_f32() * 2.0;
    let ( mut dy, mut dx, mut dz ) = ( vec![], vec![], vec![] );

    //キー入力による変化量
    input_key.get_pressed().for_each
    (   | &keycode |
        match keycode
        {   KeyCode::ArrowUp    => dy.push(  time_delta ), //上
            KeyCode::ArrowDown  => dy.push( -time_delta ), //下
            KeyCode::ArrowRight => dx.push(  time_delta ), //右
            KeyCode::ArrowLeft  => dx.push( -time_delta ), //左
            KeyCode::KeyX       => dz.push(  time_delta ), //ズームイン
            KeyCode::KeyZ       => dz.push( -time_delta ), //ズームアウト
            _ => (),
        }
    );

    //マウスの右ボタンが押されている場合の変化量
    if input_mouse.pressed( MouseButton::Left )
    {   evt_mouse_motion.read().for_each
        (   | mouse_motion |
            {   dy.push( mouse_motion.delta.y *  0.01 ); //上下
                dx.push( mouse_motion.delta.x * -0.01 ); //左右
            }
        );
    }

    //マウスホイールによる変化量
    evt_mouse_wheel.read().for_each( | wheel | { dz.push( wheel.y * 0.2 ); } );

    //ゲームパッドのボタン(十字&ショルダー)による変化量
    input_button.get_pressed().for_each
    (   | &button |
        match button
        {   GamepadButton { gamepad: _, button_type: GamepadButtonType::DPadUp       } => dy.push(  time_delta ), //上
            GamepadButton { gamepad: _, button_type: GamepadButtonType::DPadDown     } => dy.push( -time_delta ), //下
            GamepadButton { gamepad: _, button_type: GamepadButtonType::DPadRight    } => dx.push(  time_delta ), //右
            GamepadButton { gamepad: _, button_type: GamepadButtonType::DPadLeft     } => dx.push( -time_delta ), //左
            GamepadButton { gamepad: _, button_type: GamepadButtonType::RightTrigger } => dz.push(  time_delta ), //ズームイン
            GamepadButton { gamepad: _, button_type: GamepadButtonType::LeftTrigger  } => dz.push( -time_delta ), //ズームアウト
            _ => (),
        }
    );

    //ゲームパッドのスティック&トリガー(アナログ入力)による変化量
    gamepads.iter().for_each
    (   | gamepad |
        {   let stick_y = GamepadAxis { gamepad, axis_type: GamepadAxisType::LeftStickY };
            let stick_x = GamepadAxis { gamepad, axis_type: GamepadAxisType::LeftStickX };
            if let Some ( value ) = axis_stick.get( stick_y ) { dy.push( value * time_delta ); } //上下
            if let Some ( value ) = axis_stick.get( stick_x ) { dx.push( value * time_delta ); } //左右

            let button_r = GamepadButton { gamepad, button_type: GamepadButtonType::RightTrigger2 };
            let button_l = GamepadButton { gamepad, button_type: GamepadButtonType::LeftTrigger2  };
            if let Some ( value ) = axis_button.get( button_r ) { dz.push(  value * time_delta ); } //ズームイン
            if let Some ( value ) = axis_button.get( button_l ) { dz.push( -value * time_delta ); } //ズームアウト
        }
    );

    //変化量の平均値を極座標に加える
    dy.retain( |&y| y != 0.0 );
    dx.retain( |&x| x != 0.0 );
    dz.retain( |&z| z != 0.0 );
    if ! dy.is_empty() { orbit.theta += dy.iter().sum::<f32>() / dy.len() as f32; }
    if ! dx.is_empty() { orbit.phi   += dx.iter().sum::<f32>() / dx.len() as f32; }
    if ! dz.is_empty() { orbit.r     += dz.iter().sum::<f32>() / dz.len() as f32; }

    //オーバーフローを修正する
    orbit.r = orbit.r.clamp( 1.0, 3.0 );
    orbit.theta = orbit.theta.clamp( PI * 0.55, PI * 0.95 );
    orbit.phi = orbit.phi.rem_euclid( TAU ); //0.0 <= phi < TAU.abs()

    //カメラの位置と姿勢を更新する
    *transform = Transform::from_translation( orbit.into_vec3() ).looking_at( Vec3::ZERO, Vec3::Y );
}
spawn_chest3d.rs (これは前回(chip6)のソースコードと同じ)
spawn_chest3d.rs
use super::*;

//3Dオブジェクトを作る
pub fn spawn_chest3d
(   mut cmds: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
)
{   //3Dカメラとライトをspawnする
    let camera = OrbitCamera::default();
    let camera_vec3 = camera.orbit.into_vec3();
    cmds.spawn( ( Camera3dBundle::default(), camera ) )
        .insert( Transform::from_translation( camera_vec3 ).looking_at( Vec3::ZERO, Vec3::Y ) );

    let light_vec3 = Vec3::new( 30.0, 100.0, 40.0 ); //ライトの位置
    let illuminance = 3000.0; //ライトの明るさ
    let shadows_enabled = true; //影の描画アリ
    cmds.spawn( DirectionalLightBundle::default() )
        .insert( DirectionalLight { illuminance, shadows_enabled, ..default() } )
        .insert( Transform::from_translation( light_vec3 ).looking_at( Vec3::ZERO, Vec3::Y ) );

    //地面
    cmds.spawn( PbrBundle::default() )
        .insert( meshes.add( Plane3d::new( Vec3::Y, Vec2::splat( 1.0 ) ) ) )
        .insert( Transform::from_translation( Vec3::ZERO ) )
        .insert( materials.add( Color::Srgba( css::YELLOW_GREEN ) ) );

    //宝箱
    cmds.spawn( PbrBundle::default() ) //透明な親を作る
        .insert( materials.add( Color::NONE ) )
        .insert( Transform::from_translation( Vec3::Y * 0.5 ) )
        .with_children //子の中に鍵付き宝箱を作る
        (   | cmds |
            {   //本体
                cmds.spawn( PbrBundle::default() )
                    .insert( meshes.add( Cuboid::new( 0.7, 0.3, 0.4 ) ) )
                    .insert( Transform::from_translation( Vec3::Y * -0.35 ) )
                    .insert( materials.add( Color::Srgba ( css::MAROON ) ) );

                //上蓋
                let z90 = Quat::from_rotation_z( PI * 0.5 );
                cmds.spawn( PbrBundle::default() )
                    .insert( meshes.add( Cylinder::new( 0.195, 0.695 ) ) )
                    .insert( Transform::from_translation( Vec3::Y * -0.2 ).with_rotation( z90 ) )
                    .insert( materials.add( Color::Srgba ( css::MAROON ) ) );

                //錠前
                cmds.spawn( PbrBundle::default() )
                    .insert( meshes.add( Cuboid::from_length( 0.1 ) ) )
                    .insert( Transform::from_translation( Vec3::Y * -0.2 + Vec3::Z * 0.17 ) )
                    .insert( materials.add( Color::Srgba ( css::GRAY ) ) )
                    .with_children
                    (   | cmds |
                        {   //鍵穴
                            let x90 = Quat::from_rotation_x( PI * 0.5 );
                            cmds.spawn( PbrBundle::default() )
                                .insert( meshes.add( Cylinder::new( 0.01, 0.11 ) ) )
                                .insert( Transform::from_translation( Vec3::Y * 0.02 ).with_rotation( x90 ) )
                                .insert( materials.add( Color::BLACK ) );
                            cmds.spawn( PbrBundle::default() )
                                .insert( meshes.add( Cuboid::new( 0.01, 0.04, 0.11 ) ) )
                                .insert( Transform::from_translation( Vec3::ZERO ) )
                                .insert( materials.add( Color::BLACK ) );
                        }
                    );
            }
        );
}

おまけ情報

Discussion