You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
148 lines
5.1 KiB
148 lines
5.1 KiB
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<ButtonInput<MouseButton>>, |
|
key_input: Res<ButtonInput<KeyCode>>, |
|
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<f32>, |
|
|
|
/// 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<AccumulatedMouseMotion>, |
|
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<AssetServer>) { |
|
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<CursorOptions>>, |
|
crosshair: Single<&mut Visibility, With<Crosshair>>, |
|
) { |
|
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(); |
|
}
|
|
|