decal/helpers/
camera_controller.rs

1//! A freecam-style camera controller plugin.
2//! To use in your own application:
3//! - Copy the code for the [`CameraControllerPlugin`] and add the plugin to your App.
4//! - Attach the [`CameraController`] component to an entity with a [`Camera3d`].
5//!
6//! Unlike other examples, which demonstrate an application, this demonstrates a plugin library.
7
8use bevy::{
9    input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseScrollUnit},
10    prelude::*,
11    window::CursorGrabMode,
12};
13use std::{f32::consts::*, fmt};
14
15/// A freecam-style camera controller plugin.
16pub struct CameraControllerPlugin;
17
18impl Plugin for CameraControllerPlugin {
19    fn build(&self, app: &mut App) {
20        app.add_systems(Update, run_camera_controller);
21    }
22}
23
24/// Based on Valorant's default sensitivity, not entirely sure why it is exactly 1.0 / 180.0,
25/// but I'm guessing it is a misunderstanding between degrees/radians and then sticking with
26/// it because it felt nice.
27pub const RADIANS_PER_DOT: f32 = 1.0 / 180.0;
28
29/// Camera controller [`Component`].
30#[derive(Component)]
31pub struct CameraController {
32    /// Enables this [`CameraController`] when `true`.
33    pub enabled: bool,
34    /// Indicates if this controller has been initialized by the [`CameraControllerPlugin`].
35    pub initialized: bool,
36    /// Multiplier for pitch and yaw rotation speed.
37    pub sensitivity: f32,
38    /// [`KeyCode`] for forward translation.
39    pub key_forward: KeyCode,
40    /// [`KeyCode`] for backward translation.
41    pub key_back: KeyCode,
42    /// [`KeyCode`] for left translation.
43    pub key_left: KeyCode,
44    /// [`KeyCode`] for right translation.
45    pub key_right: KeyCode,
46    /// [`KeyCode`] for up translation.
47    pub key_up: KeyCode,
48    /// [`KeyCode`] for down translation.
49    pub key_down: KeyCode,
50    /// [`KeyCode`] to use [`run_speed`](CameraController::run_speed) instead of
51    /// [`walk_speed`](CameraController::walk_speed) for translation.
52    pub key_run: KeyCode,
53    /// [`MouseButton`] for grabbing the mouse focus.
54    pub mouse_key_cursor_grab: MouseButton,
55    /// [`KeyCode`] for grabbing the keyboard focus.
56    pub keyboard_key_toggle_cursor_grab: KeyCode,
57    /// Multiplier for unmodified translation speed.
58    pub walk_speed: f32,
59    /// Multiplier for running translation speed.
60    pub run_speed: f32,
61    /// Multiplier for how the mouse scroll wheel modifies [`walk_speed`](CameraController::walk_speed)
62    /// and [`run_speed`](CameraController::run_speed).
63    pub scroll_factor: f32,
64    /// Friction factor used to exponentially decay [`velocity`](CameraController::velocity) over time.
65    pub friction: f32,
66    /// This [`CameraController`]'s pitch rotation.
67    pub pitch: f32,
68    /// This [`CameraController`]'s yaw rotation.
69    pub yaw: f32,
70    /// This [`CameraController`]'s translation velocity.
71    pub velocity: Vec3,
72}
73
74impl Default for CameraController {
75    fn default() -> Self {
76        Self {
77            enabled: true,
78            initialized: false,
79            sensitivity: 1.0,
80            key_forward: KeyCode::KeyW,
81            key_back: KeyCode::KeyS,
82            key_left: KeyCode::KeyA,
83            key_right: KeyCode::KeyD,
84            key_up: KeyCode::KeyE,
85            key_down: KeyCode::KeyQ,
86            key_run: KeyCode::ShiftLeft,
87            mouse_key_cursor_grab: MouseButton::Left,
88            keyboard_key_toggle_cursor_grab: KeyCode::KeyM,
89            walk_speed: 5.0,
90            run_speed: 15.0,
91            scroll_factor: 0.1,
92            friction: 0.5,
93            pitch: 0.0,
94            yaw: 0.0,
95            velocity: Vec3::ZERO,
96        }
97    }
98}
99
100impl fmt::Display for CameraController {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(
103            f,
104            "
105Freecam Controls:
106    Mouse\t- Move camera orientation
107    Scroll\t- Adjust movement speed
108    {:?}\t- Hold to grab cursor
109    {:?}\t- Toggle cursor grab
110    {:?} & {:?}\t- Fly forward & backwards
111    {:?} & {:?}\t- Fly sideways left & right
112    {:?} & {:?}\t- Fly up & down
113    {:?}\t- Fly faster while held",
114            self.mouse_key_cursor_grab,
115            self.keyboard_key_toggle_cursor_grab,
116            self.key_forward,
117            self.key_back,
118            self.key_left,
119            self.key_right,
120            self.key_up,
121            self.key_down,
122            self.key_run,
123        )
124    }
125}
126
127fn run_camera_controller(
128    time: Res<Time>,
129    mut windows: Query<&mut Window>,
130    accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
131    accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
132    mouse_button_input: Res<ButtonInput<MouseButton>>,
133    key_input: Res<ButtonInput<KeyCode>>,
134    mut toggle_cursor_grab: Local<bool>,
135    mut mouse_cursor_grab: Local<bool>,
136    mut query: Query<(&mut Transform, &mut CameraController), With<Camera>>,
137) {
138    let dt = time.delta_secs();
139
140    let Ok((mut transform, mut controller)) = query.single_mut() else {
141        return;
142    };
143
144    if !controller.initialized {
145        let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
146        controller.yaw = yaw;
147        controller.pitch = pitch;
148        controller.initialized = true;
149        info!("{}", *controller);
150    }
151    if !controller.enabled {
152        return;
153    }
154
155    let mut scroll = 0.0;
156
157    let amount = match accumulated_mouse_scroll.unit {
158        MouseScrollUnit::Line => accumulated_mouse_scroll.delta.y,
159        MouseScrollUnit::Pixel => accumulated_mouse_scroll.delta.y / 16.0,
160    };
161    scroll += amount;
162    controller.walk_speed += scroll * controller.scroll_factor * controller.walk_speed;
163    controller.run_speed = controller.walk_speed * 3.0;
164
165    // Handle key input
166    let mut axis_input = Vec3::ZERO;
167    if key_input.pressed(controller.key_forward) {
168        axis_input.z += 1.0;
169    }
170    if key_input.pressed(controller.key_back) {
171        axis_input.z -= 1.0;
172    }
173    if key_input.pressed(controller.key_right) {
174        axis_input.x += 1.0;
175    }
176    if key_input.pressed(controller.key_left) {
177        axis_input.x -= 1.0;
178    }
179    if key_input.pressed(controller.key_up) {
180        axis_input.y += 1.0;
181    }
182    if key_input.pressed(controller.key_down) {
183        axis_input.y -= 1.0;
184    }
185
186    let mut cursor_grab_change = false;
187    if key_input.just_pressed(controller.keyboard_key_toggle_cursor_grab) {
188        *toggle_cursor_grab = !*toggle_cursor_grab;
189        cursor_grab_change = true;
190    }
191    if mouse_button_input.just_pressed(controller.mouse_key_cursor_grab) {
192        *mouse_cursor_grab = true;
193        cursor_grab_change = true;
194    }
195    if mouse_button_input.just_released(controller.mouse_key_cursor_grab) {
196        *mouse_cursor_grab = false;
197        cursor_grab_change = true;
198    }
199    let cursor_grab = *mouse_cursor_grab || *toggle_cursor_grab;
200
201    // Apply movement update
202    if axis_input != Vec3::ZERO {
203        let max_speed = if key_input.pressed(controller.key_run) {
204            controller.run_speed
205        } else {
206            controller.walk_speed
207        };
208        controller.velocity = axis_input.normalize() * max_speed;
209    } else {
210        let friction = controller.friction.clamp(0.0, 1.0);
211        controller.velocity *= 1.0 - friction;
212        if controller.velocity.length_squared() < 1e-6 {
213            controller.velocity = Vec3::ZERO;
214        }
215    }
216    let forward = *transform.forward();
217    let right = *transform.right();
218    transform.translation += controller.velocity.x * dt * right
219        + controller.velocity.y * dt * Vec3::Y
220        + controller.velocity.z * dt * forward;
221
222    // Handle cursor grab
223    if cursor_grab_change {
224        if cursor_grab {
225            for mut window in &mut windows {
226                if !window.focused {
227                    continue;
228                }
229
230                window.cursor_options.grab_mode = CursorGrabMode::Locked;
231                window.cursor_options.visible = false;
232            }
233        } else {
234            for mut window in &mut windows {
235                window.cursor_options.grab_mode = CursorGrabMode::None;
236                window.cursor_options.visible = true;
237            }
238        }
239    }
240
241    // Handle mouse input
242    if accumulated_mouse_motion.delta != Vec2::ZERO && cursor_grab {
243        // Apply look update
244        controller.pitch = (controller.pitch
245            - accumulated_mouse_motion.delta.y * RADIANS_PER_DOT * controller.sensitivity)
246            .clamp(-PI / 2., PI / 2.);
247        controller.yaw -=
248            accumulated_mouse_motion.delta.x * RADIANS_PER_DOT * controller.sensitivity;
249        transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, controller.yaw, controller.pitch);
250    }
251}