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