docs/bevy/examples/camera/free_camera_controller.rs 9.7 K · 292 lines · raw · history

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
48 use std::f32::consts::{FRAC_PI_4, PI};
49
50 use bevy::{
51 camera_controller::free_camera::{
52 FreeCamera, FreeCameraPlugin, FreeCameraState, VerticalMovementAxis,
53 },
54 color::palettes::tailwind,
55 prelude::*,
56 };
57
58 fn 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.
69 struct CameraPlugin;
70 impl Plugin for CameraPlugin {
71 fn build(&self, app: &mut App) {
72 app.add_systems(Startup, spawn_camera);
73 }
74 }
75
76 fn 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
94 struct CameraSettingsPlugin;
95 impl 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)]
103 struct InfoText;
104
105 fn 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
146 fn 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
181 fn 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.
202 struct ScenePlugin;
203 impl Plugin for ScenePlugin {
204 fn build(&self, app: &mut App) {
205 app.add_systems(Startup, (spawn_lights, spawn_world));
206 }
207 }
208
209 fn 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
239 fn 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 }