🦀
chip8. タッチ操作でカメラを動かす(追加)
はじめに
2024/10/12時点の内容です。
- rustc 1.81.0
- bevy 0.14.2
bevyは開発初期段階のOSSで、まだまだ破壊的なアップデートが入ります。
でも、面白いですよ。
前回
タッチ操作を追加
入力の4つ目としてタッチ操作を追加しました。カメラの動きとの対応は下表のとおりです。
カメラの動き | キー | マウス | タッチ | ゲームパッド |
---|---|---|---|---|
仰角アップ | 上矢印 | 右ボタン&下へ移動 | 一本指を下へ | 十字ボタン上 / 左スティック上 |
仰角ダウン | 下矢印 | 右ボタン&上へ移動 | 一本指を上へ | 十字ボタン下 / 左スティック下 |
右回り込み | 右矢印 | 右ボタン&左へ移動 | 一本指を左へ | 十字ボタン右 / 左スティック右 |
左回り込み | 左矢印 | 右ボタン&右へ移動 | 一本指を右へ | 十字ボタン左 / 左スティック左 |
ズームイン | Z | ホイール下回転 | 二本指を開く | 左ショルダーボタン / 左トリガー |
ズームアウト | X | ホイール上回転 | 二本指を閉じる | 右ショルダーボタン / 右トリガー |
キーとゲームパッドはカメラを動かす感覚、マウスとタッチは対象を動かす感覚。
個人の主観により操作の向きが逆になっています (^_^;) 。
タッチのピンチイン/ピンチアウト
以下を組み合わせれば、指2本の間の距離を計ることができ、その距離が伸び縮みする変化量deltaを知ることができるので、ピンチイン/ピンチアウトを実現できます。
- タッチで指2本の場合、
Res<Touches>
の中にはIDが違う2つのTouch
が含まれています。 -
Touch
には、指の位置を取得するメソッドtouch.position()
とtouch.previous_position()
があって、それぞれ現在の位置と前回の位置をVec2型で返します。 - Vec2型には2点間の距離を求めるメソッド
Vec2_a.distance(Vec2_b)
があります。
4つのデバイス入力でカメラを動かす
※ブラウザで操作できるWASM版はこちら
一本指で上下左右、二本指でピンチイン/ピンチアウト
main.rsは前回とほぼ同じ(utils::HashMap
のuseが増えただけ)。
move_camera.rsにタッチ操作のコードを追加しています。
spawn_chest3d.rsは前回・前々回と全く同じ。
main.rs
//external crates
use bevy::
{ prelude::*,
log::LogPlugin,
color::palettes::*,
input::mouse::{ MouseMotion, MouseWheel },
utils::HashMap,
};
//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>,
//Touch
touches: Res<Touches>,
//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 ); } );
//--------------------------------------------------------------------------
//タッチ操作による変化量
let mut map = HashMap::new();
for touch in touches.iter()
{ let id = touch.id();
let mut val = ( touch.delta(), touch.position(), touch.previous_position() );
if let Some ( ( delta, _, _ ) ) = map.get( &id ) { val.0 += *delta; }
map.insert( id, val );
}
match map.len()
{ 1 => //一本指
{ let ( delta, _, _ ) = map.values().next().unwrap();
dy.push( delta.y * 0.01 ); //上下
dx.push( delta.x * -0.01 ); //左右
}
2 => //二本指(ピンチ)
{ let mut vals = map.values();
let ( _, first_now , first_old ) = vals.next().unwrap();
let ( _, second_now, second_old ) = vals.next().unwrap();
let distance_now = first_now.distance( *second_now );
let distance_old = first_old.distance( *second_old );
let delta = distance_old - distance_now;
dz.push( delta * 0.01 ); //ズーム(ピンチイン/ピンチアウト)
}
_ => ()
}
//--------------------------------------------------------------------------
//ゲームパッドのボタン(十字&ショルダー)による変化量
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 (これは前回・前々回のソースコードと同じ)
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