From a78d11395250e2c5e9c2ae0fcdef077ad944c10b Mon Sep 17 00:00:00 2001 From: copygirl Date: Mon, 20 Oct 2025 19:01:29 +0200 Subject: [PATCH] Add camera look controls --- src/free_camera.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 5 ++++ 2 files changed, 76 insertions(+) create mode 100644 src/free_camera.rs diff --git a/src/free_camera.rs b/src/free_camera.rs new file mode 100644 index 0000000..78b316e --- /dev/null +++ b/src/free_camera.rs @@ -0,0 +1,71 @@ +use std::f32::consts::TAU; + +use bevy::input::mouse::AccumulatedMouseMotion; +use bevy::prelude::*; +use bevy::window::{CursorGrabMode, CursorOptions}; + +#[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_free_look( + window: Single<(&Window, &mut CursorOptions)>, + accumulated_mouse_motion: Res, + mouse_button_input: Res>, + key_input: Res>, + camera: Single<(&mut Transform, &mut CameraFreeLook)>, +) { + let (window, mut cursor) = window.into_inner(); + let (mut transform, mut camera) = camera.into_inner(); + + if !window.focused || key_input.just_pressed(KeyCode::Escape) { + cursor.grab_mode = CursorGrabMode::None; + cursor.visible = true; + } + if mouse_button_input.any_just_pressed([MouseButton::Left, MouseButton::Right]) { + cursor.grab_mode = CursorGrabMode::Locked; + cursor.visible = false; + } + + if cursor.grab_mode == CursorGrabMode::Locked { + // Ensure the yaw and pitch are initialized once + // from the camera transform's current rotation. + if !camera.initialized { + (camera.yaw, camera.pitch, _) = transform.rotation.to_euler(EulerRot::YXZ); + camera.initialized = true; + } + + // Update the current camera state's internal yaw and pitch. + let delta = accumulated_mouse_motion.delta * camera.sensitivity; + let (min, max) = camera.pitch_limit.clone().into_inner(); + camera.yaw = (camera.yaw - delta.x).rem_euclid(TAU); // keep within 0°..360° + camera.pitch = (camera.pitch - delta.y).clamp(min, max); + + // Override the camera transform's rotation. + transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, camera.yaw, camera.pitch); + } +} diff --git a/src/main.rs b/src/main.rs index 74f1b7e..a92c6a5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,13 @@ use bevy::prelude::*; +mod free_camera; +use free_camera::*; + fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) + .add_systems(Update, camera_free_look) .run(); } @@ -36,5 +40,6 @@ fn setup( commands.spawn(( Camera3d::default(), Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + CameraFreeLook::default(), )); }