use std::f32::consts::TAU; use bevy::prelude::*; use bevy::input::mouse::AccumulatedMouseMotion; use bevy::window::{CursorGrabMode, CursorOptions}; pub fn cursor_grab( mut mouse_button_input: ResMut>, key_input: Res>, window: Single<(&mut Window, &mut CursorOptions)>, ) { let (mut window, mut cursor) = window.into_inner(); let is_grabbed = cursor.grab_mode != CursorGrabMode::None; 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 { cursor.grab_mode = CursorGrabMode::Locked; // 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_ungrab && !request_grab { cursor.grab_mode = CursorGrabMode::None; } if is_grabbed && !request_ungrab { // Set the cursor position to the center of the window, so that when // it is ungrabbed, it will reappear there. Because `is_grabbed` is // not updated on grab, this block is delayed by one frame. // // On Wayland, since the cursor is locked into place, this only needs // to be done once. Unfortunately, for some reason this doesn't work // in the same frame as setting `grab_mode`, and would log an error. // // On X11, the cursor can't be locked into place, only confined to the // window bounds, so we repeatedly move the cursor back to the center // while it's grabbed. // // On the web, the cursor can be locked, but setting its position is // not supported at all, so this would instead log a bunch of errors. let center = window.resolution.size() / 2.; #[cfg(not(target_family = "wasm"))] // skip on web window.set_cursor_position(Some(center)); } // Keep cursor visbility in sync with `grab_mode`. cursor.visible = cursor.grab_mode == CursorGrabMode::None; } pub fn is_cursor_grabbed(cursor: Single<&CursorOptions>) -> bool { cursor.grab_mode != CursorGrabMode::None } #[derive(Component, Debug)] pub struct CameraFreeLook { /// The mouse sensitivity, in radians per pixel. pub sensitivity: Vec2, /// How far the camera can be tilted up and down. pub pitch_limit: std::ops::RangeInclusive, /// Upon initialization, `pitch` and `yaw` will /// be set from the camera transform's rotation. initialized: bool, /// The current yaw (right/left) of the camera, in radians. pub yaw: f32, /// The current pitch (tilt) of the camera, in radians. pub pitch: f32, } impl Default for CameraFreeLook { fn default() -> Self { Self { sensitivity: Vec2::splat(0.2).map(f32::to_radians), pitch_limit: -(TAU / 4.0)..=(TAU / 4.0), initialized: false, yaw: 0.0, pitch: 0.0, } } } pub fn camera_look( accumulated_mouse_motion: Res, camera: Single<(&mut Transform, &mut CameraFreeLook)>, ) { let (mut transform, mut look) = camera.into_inner(); // 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); // Override the camera transform's rotation. transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, look.yaw, look.pitch); } #[derive(Component)] pub struct Crosshair; pub fn setup_crosshair(mut commands: Commands, assets: Res) { commands.spawn(( Node { width: percent(100), height: percent(100), align_items: AlignItems::Center, justify_content: JustifyContent::Center, ..default() }, children![( Crosshair, Node { width: px(64), height: px(64), ..default() }, ImageNode { image: assets.load("crosshair.png"), ..default() }, // Hidden by default, because cursor shouldn't be grabbed at startup either. Visibility::Hidden, )], )); } pub fn update_crosshair_visibility( cursor: Single<&CursorOptions, Changed>, crosshair: Single<&mut Visibility, With>, ) { let is_grabbed = cursor.grab_mode != CursorGrabMode::None; let mut crosshair_visibility = crosshair.into_inner(); *crosshair_visibility = (!is_grabbed || cursor.visible) .then_some(Visibility::Hidden) .unwrap_or_default(); }