Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_camera_controller/src/free_camera.rs
7228 views
1
//! A camera controller that allows the user to move freely around the scene.
2
//!
3
//! Free cameras are helpful for exploring large scenes, level editors and for debugging.
4
//! They are rarely useful as-is for gameplay,
5
//! as they allow the user to move freely in all directions,
6
//! which can be disorienting, and they can clip through objects and terrain.
7
//!
8
//! You may have heard of a "fly camera" — a type of free camera designed for fluid "flying" movement and quickly surveying large areas.
9
//! By contrast, the default settings of this particular free camera are optimized for precise control.
10
//!
11
//! To use this controller, add [`FreeCameraPlugin`] to your app,
12
//! and attach the [`FreeCamera`] component to your camera entity.
13
//! The required [`FreeCameraState`] component will be added automatically.
14
//!
15
//! To configure the settings of this controller, modify the fields of the [`FreeCamera`] component.
16
17
use bevy_app::{App, Plugin, RunFixedMainLoop, RunFixedMainLoopSystems};
18
use bevy_camera::Camera;
19
use bevy_ecs::prelude::*;
20
use bevy_input::keyboard::KeyCode;
21
use bevy_input::mouse::{
22
AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButton, MouseScrollUnit,
23
};
24
use bevy_input::ButtonInput;
25
use bevy_log::info;
26
use bevy_math::{EulerRot, Quat, StableInterpolate, Vec2, Vec3};
27
use bevy_time::{Real, Time};
28
use bevy_transform::prelude::Transform;
29
use bevy_window::{CursorGrabMode, CursorOptions, Window};
30
31
use core::{f32::consts::*, fmt};
32
33
/// A freecam-style camera controller plugin.
34
///
35
/// Use the [`FreeCamera`] struct to add and customize the controller for a camera entity.
36
/// The camera's dynamic state is managed by the [`FreeCameraState`] struct.
37
pub struct FreeCameraPlugin;
38
39
impl Plugin for FreeCameraPlugin {
40
fn build(&self, app: &mut App) {
41
// This ordering is required so that both fixed update and update systems can see the results correctly
42
app.add_systems(
43
RunFixedMainLoop,
44
run_freecamera_controller.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop),
45
);
46
}
47
}
48
49
/// Scales mouse motion into yaw/pitch movement.
50
///
51
/// Based on Valorant's default sensitivity, not entirely sure why it is exactly 1.0 / 180.0,
52
/// but we're guessing it is a misunderstanding between degrees/radians and then sticking with
53
/// it because it felt nice.
54
const RADIANS_PER_DOT: f32 = 1.0 / 180.0;
55
56
/// Stores the settings for the [`FreeCamera`] controller.
57
///
58
/// This component defines static configuration for camera controls,
59
/// including movement speed, sensitivity, and input bindings.
60
///
61
/// From the controller’s perspective, this data is treated as immutable,
62
/// but it may be modified externally (e.g., by a settings UI) at runtime.
63
///
64
/// Add this component to a [`Camera`] entity to enable `FreeCamera` controls.
65
/// The associated dynamic state is automatically handled by [`FreeCameraState`],
66
/// which is added to the entity as a required component.
67
///
68
/// To activate the controller, add the [`FreeCameraPlugin`] to your [`App`].
69
#[derive(Component)]
70
#[require(FreeCameraState)]
71
pub struct FreeCamera {
72
/// Multiplier for pitch and yaw rotation speed.
73
pub sensitivity: f32,
74
/// [`KeyCode`] for forward translation.
75
pub key_forward: KeyCode,
76
/// [`KeyCode`] for backward translation.
77
pub key_back: KeyCode,
78
/// [`KeyCode`] for left translation.
79
pub key_left: KeyCode,
80
/// [`KeyCode`] for right translation.
81
pub key_right: KeyCode,
82
/// [`KeyCode`] for up translation.
83
pub key_up: KeyCode,
84
/// [`KeyCode`] for down translation.
85
pub key_down: KeyCode,
86
/// [`KeyCode`] to use [`run_speed`](FreeCamera::run_speed) instead of
87
/// [`walk_speed`](FreeCamera::walk_speed) for translation.
88
pub key_run: KeyCode,
89
/// [`MouseButton`] for grabbing the mouse focus.
90
pub mouse_key_cursor_grab: MouseButton,
91
/// [`KeyCode`] for grabbing the keyboard focus.
92
pub keyboard_key_toggle_cursor_grab: KeyCode,
93
/// Base multiplier for unmodified translation speed.
94
pub walk_speed: f32,
95
/// Base multiplier for running translation speed.
96
pub run_speed: f32,
97
/// Multiplier for how the mouse scroll wheel modifies [`walk_speed`](FreeCamera::walk_speed)
98
/// and [`run_speed`](FreeCamera::run_speed).
99
pub scroll_factor: f32,
100
/// Friction factor used to exponentially decay [`velocity`](FreeCameraState::velocity) over time.
101
pub friction: f32,
102
}
103
104
impl Default for FreeCamera {
105
fn default() -> Self {
106
Self {
107
sensitivity: 0.2,
108
key_forward: KeyCode::KeyW,
109
key_back: KeyCode::KeyS,
110
key_left: KeyCode::KeyA,
111
key_right: KeyCode::KeyD,
112
key_up: KeyCode::KeyE,
113
key_down: KeyCode::KeyQ,
114
key_run: KeyCode::ShiftLeft,
115
mouse_key_cursor_grab: MouseButton::Left,
116
keyboard_key_toggle_cursor_grab: KeyCode::KeyM,
117
walk_speed: 5.0,
118
run_speed: 15.0,
119
scroll_factor: 0.5,
120
friction: 40.0,
121
}
122
}
123
}
124
125
impl fmt::Display for FreeCamera {
126
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127
write!(
128
f,
129
"
130
Freecamera Controls:
131
Mouse\t- Move camera orientation
132
Scroll\t- Adjust movement speed
133
{:?}\t- Hold to grab cursor
134
{:?}\t- Toggle cursor grab
135
{:?} & {:?}\t- Fly forward & backwards
136
{:?} & {:?}\t- Fly sideways left & right
137
{:?} & {:?}\t- Fly up & down
138
{:?}\t- Fly faster while held",
139
self.mouse_key_cursor_grab,
140
self.keyboard_key_toggle_cursor_grab,
141
self.key_forward,
142
self.key_back,
143
self.key_left,
144
self.key_right,
145
self.key_up,
146
self.key_down,
147
self.key_run,
148
)
149
}
150
}
151
152
/// Tracks the runtime state of a [`FreeCamera`] controller.
153
///
154
/// This component holds dynamic data that changes during camera operation,
155
/// such as pitch, yaw, velocity, and whether the controller is currently enabled.
156
///
157
/// It is automatically added to any entity that has a [`FreeCamera`] component,
158
/// and is updated by the [`FreeCameraPlugin`] systems in response to user input.
159
#[derive(Component)]
160
pub struct FreeCameraState {
161
/// Enables [`FreeCamera`] controls when `true`.
162
pub enabled: bool,
163
/// Internal flag indicating if this controller has been initialized by the [`FreeCameraPlugin`].
164
initialized: bool,
165
/// This [`FreeCamera`]'s pitch rotation.
166
pub pitch: f32,
167
/// This [`FreeCamera`]'s yaw rotation.
168
pub yaw: f32,
169
/// Multiplier applied to movement speed.
170
pub speed_multiplier: f32,
171
/// This [`FreeCamera`]'s translation velocity.
172
pub velocity: Vec3,
173
}
174
175
impl Default for FreeCameraState {
176
fn default() -> Self {
177
Self {
178
enabled: true,
179
initialized: false,
180
pitch: 0.0,
181
yaw: 0.0,
182
speed_multiplier: 1.0,
183
velocity: Vec3::ZERO,
184
}
185
}
186
}
187
188
/// Updates the camera's position and orientation based on user input.
189
///
190
/// - [`FreeCamera`] contains static configuration such as key bindings, movement speed, and sensitivity.
191
/// - [`FreeCameraState`] stores the dynamic runtime state, including pitch, yaw, velocity, and enable flags.
192
///
193
/// This system is typically added via the [`FreeCameraPlugin`].
194
pub fn run_freecamera_controller(
195
time: Res<Time<Real>>,
196
mut windows: Query<(&Window, &mut CursorOptions)>,
197
accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
198
accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
199
mouse_button_input: Res<ButtonInput<MouseButton>>,
200
key_input: Res<ButtonInput<KeyCode>>,
201
mut toggle_cursor_grab: Local<bool>,
202
mut mouse_cursor_grab: Local<bool>,
203
mut query: Query<(&mut Transform, &mut FreeCameraState, &FreeCamera), With<Camera>>,
204
) {
205
let dt = time.delta_secs();
206
207
let Ok((mut transform, mut state, config)) = query.single_mut() else {
208
return;
209
};
210
211
if !state.initialized {
212
let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
213
state.yaw = yaw;
214
state.pitch = pitch;
215
state.initialized = true;
216
info!("{}", *config);
217
}
218
219
if !state.enabled {
220
// don't keep the cursor grabbed if the camera controller was disabled.
221
if *toggle_cursor_grab || *mouse_cursor_grab {
222
*toggle_cursor_grab = false;
223
*mouse_cursor_grab = false;
224
225
for (_, mut cursor_options) in &mut windows {
226
cursor_options.grab_mode = CursorGrabMode::None;
227
cursor_options.visible = true;
228
}
229
}
230
return;
231
}
232
233
let mut scroll = 0.0;
234
235
let amount = match accumulated_mouse_scroll.unit {
236
MouseScrollUnit::Line => accumulated_mouse_scroll.delta.y,
237
MouseScrollUnit::Pixel => {
238
accumulated_mouse_scroll.delta.y / MouseScrollUnit::SCROLL_UNIT_CONVERSION_FACTOR
239
}
240
};
241
scroll += amount;
242
state.speed_multiplier += scroll * config.scroll_factor;
243
// Clamp the speed multiplier for safety
244
state.speed_multiplier = state.speed_multiplier.clamp(0.0, f32::MAX);
245
246
// Handle key input
247
let mut axis_input = Vec3::ZERO;
248
if key_input.pressed(config.key_forward) {
249
axis_input.z += 1.0;
250
}
251
if key_input.pressed(config.key_back) {
252
axis_input.z -= 1.0;
253
}
254
if key_input.pressed(config.key_right) {
255
axis_input.x += 1.0;
256
}
257
if key_input.pressed(config.key_left) {
258
axis_input.x -= 1.0;
259
}
260
if key_input.pressed(config.key_up) {
261
axis_input.y += 1.0;
262
}
263
if key_input.pressed(config.key_down) {
264
axis_input.y -= 1.0;
265
}
266
267
let mut cursor_grab_change = false;
268
if key_input.just_pressed(config.keyboard_key_toggle_cursor_grab) {
269
*toggle_cursor_grab = !*toggle_cursor_grab;
270
cursor_grab_change = true;
271
}
272
if mouse_button_input.just_pressed(config.mouse_key_cursor_grab) {
273
*mouse_cursor_grab = true;
274
cursor_grab_change = true;
275
}
276
if mouse_button_input.just_released(config.mouse_key_cursor_grab) {
277
*mouse_cursor_grab = false;
278
cursor_grab_change = true;
279
}
280
let cursor_grab = *mouse_cursor_grab || *toggle_cursor_grab;
281
282
// Update velocity
283
if axis_input != Vec3::ZERO {
284
let max_speed = if key_input.pressed(config.key_run) {
285
config.run_speed * state.speed_multiplier
286
} else {
287
config.walk_speed * state.speed_multiplier
288
};
289
state.velocity = axis_input.normalize() * max_speed;
290
} else {
291
let friction = config.friction.clamp(0.0, f32::MAX);
292
state.velocity.smooth_nudge(&Vec3::ZERO, friction, dt);
293
if state.velocity.length_squared() < 1e-6 {
294
state.velocity = Vec3::ZERO;
295
}
296
}
297
298
// Apply movement update
299
if state.velocity != Vec3::ZERO {
300
let forward = *transform.forward();
301
let right = *transform.right();
302
transform.translation += state.velocity.x * dt * right
303
+ state.velocity.y * dt * Vec3::Y
304
+ state.velocity.z * dt * forward;
305
}
306
307
// Handle cursor grab
308
if cursor_grab_change {
309
if cursor_grab {
310
for (window, mut cursor_options) in &mut windows {
311
if !window.focused {
312
continue;
313
}
314
315
cursor_options.grab_mode = CursorGrabMode::Locked;
316
cursor_options.visible = false;
317
}
318
} else {
319
for (_, mut cursor_options) in &mut windows {
320
cursor_options.grab_mode = CursorGrabMode::None;
321
cursor_options.visible = true;
322
}
323
}
324
}
325
326
// Handle mouse input
327
if accumulated_mouse_motion.delta != Vec2::ZERO && cursor_grab {
328
// Apply look update
329
state.pitch = (state.pitch
330
- accumulated_mouse_motion.delta.y * RADIANS_PER_DOT * config.sensitivity)
331
.clamp(-PI / 2., PI / 2.);
332
state.yaw -= accumulated_mouse_motion.delta.x * RADIANS_PER_DOT * config.sensitivity;
333
transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, state.yaw, state.pitch);
334
}
335
}
336
337