diff --git a/src/free_camera.rs b/src/camera.rs similarity index 69% rename from src/free_camera.rs rename to src/camera.rs index eb26137..9611eab 100644 --- a/src/free_camera.rs +++ b/src/camera.rs @@ -4,6 +4,53 @@ use bevy::input::mouse::AccumulatedMouseMotion; use bevy::prelude::*; use bevy::window::{CursorGrabMode, CursorOptions}; +pub fn cursor_grab( + mut mouse_button_input: ResMut>, + key_input: Res>, + window: Single<(&mut Window, &mut CursorOptions)>, + crosshair: Single<&mut Visibility, With>, + mut request_center_cursor: Local, +) { + let (mut window, mut cursor) = window.into_inner(); + let mut crosshair_visibility = crosshair.into_inner(); + + let is_grabbed = cursor.grab_mode == CursorGrabMode::Locked; + let request_grab = mouse_button_input.any_just_pressed([MouseButton::Left, MouseButton::Right]); + let request_ungrab = !window.focused || key_input.just_pressed(KeyCode::Escape); + + if !is_grabbed && request_grab && !request_ungrab { + *crosshair_visibility = Visibility::Inherited; + cursor.grab_mode = CursorGrabMode::Locked; + cursor.visible = false; + + // HACK: It appears setting the cursor position in the same frame we're + // locking it is not possible, so we're delaying it by a frame. + *request_center_cursor = true; + + // To prevent other systems (such as `place_break_blocks`) + // from seeing the mouse button inputs, clear the state here. + mouse_button_input.clear(); + } + + if is_grabbed && *request_center_cursor { + // Set the cursor position to the middle of the window, + // so when it is ungrabbed again it'll reappear there. + let center = window.resolution.size() / 2.; + window.set_cursor_position(Some(center)); + *request_center_cursor = false; // Only do this once. + } + + if is_grabbed && request_ungrab && !request_grab { + *crosshair_visibility = Visibility::Hidden; + cursor.grab_mode = CursorGrabMode::None; + cursor.visible = true; + } +} + +pub fn is_cursor_grabbed(cursor: Single<&CursorOptions>) -> bool { + cursor.grab_mode == CursorGrabMode::Locked +} + #[derive(Component, Debug)] pub struct CameraFreeLook { /// The mouse sensitivity, in radians per pixel. @@ -32,49 +79,27 @@ impl Default for CameraFreeLook { } } -pub fn camera_free_look( +pub fn camera_look( accumulated_mouse_motion: Res, - mut mouse_button_input: ResMut>, - key_input: Res>, - window: Single<(&Window, &mut CursorOptions)>, camera: Single<(&mut Transform, &mut CameraFreeLook)>, - crosshair: Single<&mut Visibility, With>, ) { - let (window, mut cursor) = window.into_inner(); let (mut transform, mut look) = camera.into_inner(); - let mut crosshair_visibility = crosshair.into_inner(); - if !window.focused || key_input.just_pressed(KeyCode::Escape) { - *crosshair_visibility = Visibility::Hidden; - cursor.grab_mode = CursorGrabMode::None; - cursor.visible = true; - } - if mouse_button_input.any_just_pressed([MouseButton::Left, MouseButton::Right]) { - *crosshair_visibility = Visibility::Inherited; - cursor.grab_mode = CursorGrabMode::Locked; - cursor.visible = false; - // To prevent other systems (such as placement) from seeing - // the mouse buttons being pressed, clear the state here. - mouse_button_input.clear(); + // Ensure the yaw and pitch are initialized once + // from the camera transform's current rotation. + if !look.initialized { + (look.yaw, look.pitch, _) = transform.rotation.to_euler(EulerRot::YXZ); + look.initialized = true; } - if cursor.grab_mode == CursorGrabMode::Locked { - // Ensure the yaw and pitch are initialized once - // from the camera transform's current rotation. - if !look.initialized { - (look.yaw, look.pitch, _) = transform.rotation.to_euler(EulerRot::YXZ); - look.initialized = true; - } - - // Update the current camera state's internal yaw and pitch. - let motion = accumulated_mouse_motion.delta * look.sensitivity; - let (min, max) = look.pitch_limit.clone().into_inner(); - look.yaw = (look.yaw - motion.x).rem_euclid(TAU); // keep within 0°..360° - look.pitch = (look.pitch - motion.y).clamp(min, max); + // Update the current camera state's internal yaw and pitch. + let motion = accumulated_mouse_motion.delta * look.sensitivity; + let (min, max) = look.pitch_limit.clone().into_inner(); + look.yaw = (look.yaw - motion.x).rem_euclid(TAU); // keep within 0°..360° + look.pitch = (look.pitch - motion.y).clamp(min, max); - // Override the camera transform's rotation. - transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, look.yaw, look.pitch); - } + // Override the camera transform's rotation. + transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, look.yaw, look.pitch); } // TODO: Make it possible to attach this to any entity, such as the player, @@ -116,14 +141,10 @@ impl Default for CameraNoClip { pub fn noclip_controller( time: Res>, - cursor: Single<&mut CursorOptions>, key_input: Res>, - camera: Single<(&mut Transform, &mut CameraNoClip)>, + camera: Single<(&mut Transform, &CameraNoClip)>, ) { let (mut transform, noclip) = camera.into_inner(); - if cursor.grab_mode != CursorGrabMode::Locked { - return; - } #[rustfmt::skip] let movement = { diff --git a/src/main.rs b/src/main.rs index 1695520..f05b8cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,27 +1,30 @@ use bevy::prelude::*; mod block; -mod free_camera; +mod camera; mod placement; use block::*; -use free_camera::*; +use camera::*; use placement::*; fn main() { + #[rustfmt::skip] App::new() .add_plugins(DefaultPlugins) - .add_systems( - Startup, - (setup_blocks, setup_crosshair, setup_scene).chain(), - ) - .add_systems(Update, (camera_free_look, noclip_controller).chain()) - .add_systems( - PostUpdate, - // This system requires the camera's `GlobalTransform`, so for - // a most up-to-date value, run it right after it's been updated. - place_break_blocks.after(TransformSystems::Propagate), - ) + + .add_systems(Startup, setup_crosshair) + .add_systems(Startup, setup_blocks) + .add_systems(Startup, setup_scene.after(setup_blocks)) + + .add_systems(Update, cursor_grab) + .add_systems(Update, camera_look.after(cursor_grab).run_if(is_cursor_grabbed)) + .add_systems(Update, noclip_controller.after(camera_look).run_if(is_cursor_grabbed)) + + // This system requires the camera's `GlobalTransform`, so for + // a most up-to-date value, run it right after it's been updated. + .add_systems(PostUpdate, place_break_blocks.after(TransformSystems::Propagate)) + .run(); } diff --git a/src/placement.rs b/src/placement.rs index ecb8123..6ad4eb0 100644 --- a/src/placement.rs +++ b/src/placement.rs @@ -12,10 +12,6 @@ pub fn place_break_blocks( camera: Single<(&GlobalTransform, &Camera)>, block_lookup: Query<&Transform, With>, ) { - if !mouse_button_input.any_just_pressed([MouseButton::Left, MouseButton::Right]) { - return; // only run this system when left or right mouse button is pressed - } - let (window, cursor) = window.into_inner(); let (cam_transform, camera) = camera.into_inner();