Skip to main content

free_camera_controller/
free_camera_controller.rs

1//! This example showcases the default `FreeCamera` camera controller.
2//!
3//! The default `FreeCamera` controller is useful for exploring large scenes, debugging and editing purposes. To use it,
4//! simply add the [`FreeCameraPlugin`] to your [`App`] and attach the [`FreeCamera`] component to the camera entity you
5//! wish to control.
6//!
7//! ## Default Controls
8//!
9//! This controller has a simple 6-axis control scheme, and mouse controls for camera orientation. There are also
10//! bindings for capturing the mouse, both while holding the button and toggle, a run feature that increases the
11//! max speed, and scrolling changes the movement speed. All keybinds can be changed by editing the [`FreeCamera`]
12//! component.
13//!
14//! | Default Key Binding | Action                 |
15//! |:--------------------|:-----------------------|
16//! | Mouse               | Look around            |
17//! | Right click         | Capture mouse (hold)   |
18//! | M                   | Capture mouse (toggle) |
19//! | WASD                | Horizontal movement    |
20//! | QE                  | Vertical movement      |
21//! | Left shift          | Run                    |
22//! | Scroll wheel        | Change movement speed  |
23//! | Numpad1             | Snap to front          |
24//! | `LCtrl` + Numpad1   | Snap to back           |
25//! | Numpad3             | Snap to right          |
26//! | `LCtrl` + Numpad3   | Snap to left           |
27//! | Numpad7             | Snap to top            |
28//! | `LCtrl` + Numpad7   | Snap to bottom         |
29//!
30//! The movement speed, sensitivity and friction can also be changed by the [`FreeCamera`] component.
31//!
32//! ## Example controls
33//!
34//! This example also provides a few extra keybinds to change the camera sensitivity, friction (how fast the camera
35//! stops), scroll factor (how much scrolling changes speed) and enabling/disabling the controller.
36//!
37//! | Key Binding | Action                        |
38//! |:------------|:------------------------------|
39//! | Z           | Decrease sensitivity          |
40//! | X           | Increase sensitivity          |
41//! | C           | Decrease friction             |
42//! | V           | Increase friction             |
43//! | F           | Decrease scroll factor        |
44//! | G           | Increase scroll factor        |
45//! | B           | Enable/Disable                |
46//! | T           | World/Local vertical movement |
47
48use std::f32::consts::{FRAC_PI_4, PI};
49
50use bevy::{
51    camera_controller::free_camera::{
52        FreeCamera, FreeCameraPlugin, FreeCameraState, VerticalMovementAxis,
53    },
54    color::palettes::tailwind,
55    prelude::*,
56};
57
58fn main() {
59    App::new()
60        .add_plugins(DefaultPlugins)
61        // Plugin that enables FreeCamera functionality
62        .add_plugins(FreeCameraPlugin)
63        // Example code plugins
64        .add_plugins((CameraPlugin, CameraSettingsPlugin, ScenePlugin))
65        .run();
66}
67
68// Plugin that spawns the camera.
69struct CameraPlugin;
70impl Plugin for CameraPlugin {
71    fn build(&self, app: &mut App) {
72        app.add_systems(Startup, spawn_camera);
73    }
74}
75
76fn spawn_camera(mut commands: Commands) {
77    commands.spawn((
78        Camera3d::default(),
79        Transform::from_xyz(0.0, 1.0, 0.0).looking_to(Vec3::X, Vec3::Y),
80        // This component stores all camera settings and state, which is used by the FreeCameraPlugin to
81        // control it. These properties can be changed at runtime, but beware the controller system is
82        // constantly using and modifying those values unless the enabled field is false.
83        FreeCamera {
84            sensitivity: 0.2,
85            friction: 25.0,
86            walk_speed: 3.0,
87            run_speed: 9.0,
88            ..default()
89        },
90    ));
91}
92
93// Plugin that handles camera settings controls and information text
94struct CameraSettingsPlugin;
95impl Plugin for CameraSettingsPlugin {
96    fn build(&self, app: &mut App) {
97        app.add_systems(PostStartup, spawn_text)
98            .add_systems(Update, (update_camera_settings, update_text));
99    }
100}
101
102#[derive(Component)]
103struct InfoText;
104
105fn spawn_text(mut commands: Commands, free_camera_query: Query<&FreeCamera>) {
106    commands.spawn((
107        Node {
108            position_type: PositionType::Absolute,
109            top: px(-16),
110            left: px(12),
111            ..default()
112        },
113        children![Text::new(format!(
114            "{}",
115            free_camera_query.single().unwrap(),
116        ))],
117    ));
118    commands.spawn((
119        Node {
120            position_type: PositionType::Absolute,
121            bottom: px(12),
122            left: px(12),
123            ..default()
124        },
125        children![Text::new(concat![
126            "Z/X: decrease/increase sensitivity\n",
127            "C/V: decrease/increase friction\n",
128            "F/G: decrease/increase scroll factor\n",
129            "B: enable/disable controller\n",
130            "T: world/local vertical movement"
131        ]),],
132    ));
133
134    // Mutable text marked with component
135    commands.spawn((
136        Node {
137            position_type: PositionType::Absolute,
138            top: px(12),
139            right: px(12),
140            ..default()
141        },
142        children![(InfoText, Text::new(""))],
143    ));
144}
145
146fn update_camera_settings(
147    mut camera_query: Query<(&mut FreeCamera, &mut FreeCameraState)>,
148    input: Res<ButtonInput<KeyCode>>,
149) {
150    let (mut free_camera, mut free_camera_state) = camera_query.single_mut().unwrap();
151
152    if input.pressed(KeyCode::KeyZ) {
153        free_camera.sensitivity = (free_camera.sensitivity - 0.005).max(0.005);
154    }
155    if input.pressed(KeyCode::KeyX) {
156        free_camera.sensitivity += 0.005;
157    }
158    if input.pressed(KeyCode::KeyC) {
159        free_camera.friction = (free_camera.friction - 0.2).max(0.0);
160    }
161    if input.pressed(KeyCode::KeyV) {
162        free_camera.friction += 0.2;
163    }
164    if input.pressed(KeyCode::KeyF) {
165        free_camera.scroll_factor = (free_camera.scroll_factor - 0.02).max(0.02);
166    }
167    if input.pressed(KeyCode::KeyG) {
168        free_camera.scroll_factor += 0.02;
169    }
170    if input.just_pressed(KeyCode::KeyB) {
171        free_camera_state.enabled = !free_camera_state.enabled;
172    }
173    if input.just_pressed(KeyCode::KeyT) {
174        free_camera.vertical_movement_axis = match free_camera.vertical_movement_axis {
175            VerticalMovementAxis::World => VerticalMovementAxis::Local,
176            VerticalMovementAxis::Local => VerticalMovementAxis::World,
177        };
178    }
179}
180
181fn update_text(
182    mut text_query: Query<&mut Text, With<InfoText>>,
183    camera_query: Query<(&FreeCamera, &FreeCameraState)>,
184) {
185    let mut text = text_query.single_mut().unwrap();
186
187    let (free_camera, free_camera_state) = camera_query.single().unwrap();
188
189    text.0 = format!(
190        "Enabled: {},\nSensitivity: {:.03}\nFriction: {:.01}\nScroll factor: {:.02}\nWalk Speed: {:.02}\nRun Speed: {:.02}\nSpeed: {:.02}",
191        free_camera_state.enabled,
192        free_camera.sensitivity,
193        free_camera.friction,
194        free_camera.scroll_factor,
195        free_camera.walk_speed * free_camera_state.speed_multiplier,
196        free_camera.run_speed * free_camera_state.speed_multiplier,
197        free_camera_state.velocity.length(),
198    );
199}
200
201// Plugin that spawns the scene and lighting.
202struct ScenePlugin;
203impl Plugin for ScenePlugin {
204    fn build(&self, app: &mut App) {
205        app.add_systems(Startup, (spawn_lights, spawn_world));
206    }
207}
208
209fn spawn_lights(mut commands: Commands) {
210    // Main light
211    commands.spawn((
212        PointLight {
213            color: Color::from(tailwind::ORANGE_300),
214            shadow_maps_enabled: true,
215            ..default()
216        },
217        Transform::from_xyz(0.0, 3.0, 0.0),
218    ));
219    // Light behind wall
220    commands.spawn((
221        PointLight {
222            color: Color::WHITE,
223            shadow_maps_enabled: true,
224            ..default()
225        },
226        Transform::from_xyz(-3.5, 3.0, 0.0),
227    ));
228    // Light under floor
229    commands.spawn((
230        PointLight {
231            color: Color::from(tailwind::RED_300),
232            shadow_maps_enabled: true,
233            ..default()
234        },
235        Transform::from_xyz(0.0, -0.5, 0.0),
236    ));
237}
238
239fn spawn_world(
240    mut commands: Commands,
241    mut materials: ResMut<Assets<StandardMaterial>>,
242    mut meshes: ResMut<Assets<Mesh>>,
243) {
244    let cube = meshes.add(Cuboid::new(1.0, 1.0, 1.0));
245    let floor = meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(10.0)));
246    let sphere = meshes.add(Sphere::new(0.5));
247    let wall = meshes.add(Cuboid::new(0.2, 4.0, 3.0));
248
249    let blue_material = materials.add(Color::from(tailwind::BLUE_700));
250    let red_material = materials.add(Color::from(tailwind::RED_950));
251    let white_material = materials.add(Color::WHITE);
252
253    // Top side of floor
254    commands.spawn((
255        Mesh3d(floor.clone()),
256        MeshMaterial3d(white_material.clone()),
257    ));
258    // Under side of floor
259    commands.spawn((
260        Mesh3d(floor.clone()),
261        MeshMaterial3d(white_material.clone()),
262        Transform::from_xyz(0.0, -0.01, 0.0).with_rotation(Quat::from_rotation_x(PI)),
263    ));
264    // Blue sphere
265    commands.spawn((
266        Mesh3d(sphere.clone()),
267        MeshMaterial3d(blue_material.clone()),
268        Transform::from_xyz(3.0, 1.5, 0.0),
269    ));
270    // Tall wall
271    commands.spawn((
272        Mesh3d(wall.clone()),
273        MeshMaterial3d(white_material.clone()),
274        Transform::from_xyz(-3.0, 2.0, 0.0),
275    ));
276    // Cube behind wall
277    commands.spawn((
278        Mesh3d(cube.clone()),
279        MeshMaterial3d(blue_material.clone()),
280        Transform::from_xyz(-4.2, 0.5, 0.0),
281    ));
282    // Hidden cube under floor
283    commands.spawn((
284        Mesh3d(cube.clone()),
285        MeshMaterial3d(red_material.clone()),
286        Transform {
287            translation: Vec3::new(3.0, -2.0, 0.0),
288            rotation: Quat::from_euler(EulerRot::YXZEx, FRAC_PI_4, FRAC_PI_4, 0.0),
289            ..default()
290        },
291    ));
292}