1#![doc = include_str!("../../docs/features/debug_camera.md")]
2
3use std::{fmt::Debug, ops::RangeInclusive};
4
5use bevy::{
6 input::mouse::{MouseMotion, MouseWheel},
7 prelude::*,
8 window::CursorOptions,
9};
10
11#[cfg(feature = "ui")]
12use crate::ui::popup::{PopupEvent, PopupPosition};
13
14mod controller;
15mod focus;
16mod initialization;
17#[cfg(feature = "ui")]
18mod ui;
19
20#[cfg(feature = "ui")]
21const SELECTOR_NEXT_ELEMENT_THRESHOLD_IN_SECONDS: f32 = 0.25;
22#[cfg(feature = "ui")]
23const SELECTOR_NEXT_ELEMENT_IN_SECONDS: f32 = 0.1;
24
25pub struct DebugCameraPlugin {
32 pub switcher: DebugCameraSwitcher,
34 #[cfg(feature = "ui")]
49 pub show_preview: bool,
50 pub spawn_debug_camera_if_any_camera_exist: bool,
55}
56
57impl Default for DebugCameraPlugin {
58 fn default() -> Self {
59 Self {
60 switcher: Default::default(),
61 #[cfg(feature = "ui")]
62 show_preview: true,
63 spawn_debug_camera_if_any_camera_exist: true,
64 }
65 }
66}
67
68impl Plugin for DebugCameraPlugin {
69 fn build(&self, app: &mut App) {
70 app.init_resource::<DebugCameraGlobalData>()
71 .init_resource::<DebugCameraControls>()
72 .add_systems(
73 Update,
74 (
75 initialization::system,
76 focus::system
77 .after(initialization::system)
78 .run_if(focus::run_if_changed),
79 controller::system,
80 ),
81 );
82
83 let active_spawner = match self.switcher {
84 DebugCameraSwitcher::Default => {
85 #[cfg(not(debug_assertions))]
86 bevy::log::warn!("Switcher from bevy_dev's `DebugCamera` is active in release builds. This allows the player to easily activate and manage debug cameras, set the `DebugCameraSpawner` value explicitly in the `DebugCameraPlugin`");
87 true
88 }
89 DebugCameraSwitcher::Active => true,
90 DebugCameraSwitcher::Disabled => false,
91 };
92 if active_spawner {
93 app.add_systems(Update, switcher.before(initialization::system));
94
95 #[cfg(feature = "ui")]
96 if self.show_preview {
97 app.add_plugins(ui::DebugCameraPreviewPlugin);
98 }
99 }
100
101 if self.spawn_debug_camera_if_any_camera_exist {
102 app.add_systems(PostUpdate, spawn_debug_camera_if_any_camera_exist);
103 }
104 }
105}
106
107#[derive(Default)]
109pub enum DebugCameraSwitcher {
110 #[default]
112 Default,
113 Active,
115 Disabled,
117}
118
119#[derive(Debug, Resource)]
121pub struct DebugCameraGlobalData {
122 pub default_value: DebugCamera,
124 pub last_used_debug_cameras: Vec<Entity>,
126 pub last_used_origin_camera: Option<DebugCameraLastUsedOriginCameraData>,
128 pub(super) selected_camera: Option<usize>,
129 #[cfg(feature = "ui")]
130 last_switch_time: f32,
131 next_id: u64,
132}
133
134impl Default for DebugCameraGlobalData {
135 fn default() -> Self {
136 Self {
137 default_value: DebugCamera::default(),
138 last_used_debug_cameras: Vec::new(),
139 last_used_origin_camera: None,
140 selected_camera: None,
141 #[cfg(feature = "ui")]
142 last_switch_time: 0.0,
143 next_id: 1,
144 }
145 }
146}
147
148#[derive(Debug, Resource)]
150pub struct DebugCameraControls {
151 pub move_forward: KeyCode,
153 pub move_backward: KeyCode,
155 pub move_left: KeyCode,
157 pub move_right: KeyCode,
159 pub move_up: KeyCode,
161 pub move_down: KeyCode,
163 pub switcher_special: KeyCode,
165 pub switcher_next: KeyCode,
167 pub new_debug_camera: KeyCode,
169 pub return_to_game_camera: KeyCode,
171}
172
173impl Default for DebugCameraControls {
174 fn default() -> Self {
175 Self {
176 move_forward: KeyCode::KeyW,
177 move_backward: KeyCode::KeyS,
178 move_left: KeyCode::KeyA,
179 move_right: KeyCode::KeyD,
180 move_up: KeyCode::KeyE,
181 move_down: KeyCode::KeyQ,
182 switcher_special: KeyCode::ShiftLeft,
183 switcher_next: KeyCode::Tab,
184 new_debug_camera: KeyCode::F1,
185 return_to_game_camera: KeyCode::Escape,
186 }
187 }
188}
189
190#[derive(Debug)]
192pub struct DebugCameraLastUsedOriginCameraData {
193 pub camera: Entity,
195 pub cursor: CursorOptions,
197}
198
199#[derive(Component, Debug, Clone)]
201#[non_exhaustive]
202pub struct DebugCamera {
203 pub speed_increase: f32,
205 pub speed_multiplier: f32,
207 pub speed_multiplier_range: RangeInclusive<f32>,
209 pub sensitivity: f32,
211 pub base_speed: f32,
213 pub focus: bool,
215}
216
217impl Default for DebugCamera {
218 fn default() -> Self {
219 Self {
220 speed_increase: 0.2,
221 speed_multiplier: 1.0,
222 speed_multiplier_range: 0.001..=10.0,
223 sensitivity: 0.1,
224 base_speed: 4.5,
225 focus: true,
226 }
227 }
228}
229
230#[derive(Debug, Component)]
231pub(super) struct DebugCameraData {
232 id: u64,
233 last_change_position_time: f32,
234 current_speed: f32,
235 speed_level: f32,
236}
237
238#[allow(clippy::too_many_arguments)]
239#[allow(clippy::type_complexity)]
240fn switcher(
241 mut commands: Commands,
242 #[cfg(not(feature = "ui"))] mut debug_cameras: Query<(
243 Entity,
244 &mut DebugCamera,
245 &DebugCameraData,
246 )>,
247 #[cfg(feature = "ui")] mut debug_cameras: Query<(
248 Entity,
249 &mut DebugCamera,
250 &DebugCameraData,
251 Option<&ui::DebugCameraPreview>,
252 )>,
253 mut global: ResMut<DebugCameraGlobalData>,
254 #[cfg(not(feature = "ui"))] cameras: Query<(), (With<Camera>, Without<DebugCamera>)>,
255 #[cfg(feature = "ui")] cameras: Query<
256 (),
257 (
258 With<Camera>,
259 Without<DebugCamera>,
260 Without<ui::PreviewCamera>,
261 ),
262 >,
263 keys: Res<ButtonInput<KeyCode>>,
264 controls: Res<DebugCameraControls>,
265 #[cfg(feature = "ui")] mut popup_event: EventWriter<PopupEvent>,
266 #[cfg(feature = "ui")] time: Res<Time>,
267) {
268 if !keys.pressed(controls.switcher_special) {
269 if let Some(selected_camera) = global.selected_camera.take() {
270 if selected_camera + 1 != global.last_used_debug_cameras.len() {
271 let entity = global.last_used_debug_cameras[selected_camera];
272 debug_cameras.get_mut(entity).unwrap().1.focus = true;
273 }
274 }
275 return;
276 }
277
278 if keys.just_pressed(controls.new_debug_camera) {
280 commands.spawn(DebugCamera::default());
281 return;
282 }
283
284 if keys.just_pressed(controls.return_to_game_camera) {
286 if cameras.is_empty() {
287 bevy::log::info!("Unable to switch to game camera, no any camera exist");
288 #[cfg(feature = "ui")]
289 popup_event.write(PopupEvent::new(
290 PopupPosition::BelowCenter,
291 1.0,
292 move |ui| {
293 ui.strong("Unable to switch to game camera, no any camera exist");
294 },
295 ));
296 } else {
297 for mut debug_camera in debug_cameras.iter_mut() {
298 debug_camera.1.focus = false;
299 }
300 }
301 }
302
303 #[cfg(not(feature = "ui"))]
305 let event = keys.just_pressed(controls.switcher_next);
306 #[cfg(feature = "ui")]
307 let event = select_next_camera_key_event(&mut global, &keys, &controls, &time);
308
309 if event {
310 global.selected_camera = Some(match global.selected_camera {
311 Some(selected_camera) => match selected_camera == 0 {
312 true => global.last_used_debug_cameras.len() - 1,
313 false => selected_camera - 1,
314 },
315 None => {
316 if global.last_used_debug_cameras.is_empty() {
317 commands.spawn(DebugCamera::default());
318 return;
319 }
320
321 let len = global.last_used_debug_cameras.len();
322 match len == 1 {
323 true => 0,
324 false => len - 2,
325 }
326 }
327 });
328 }
329
330 #[cfg(feature = "ui")]
332 if global.selected_camera.is_some() {
333 ui::debug_camera_selector_ui(&mut debug_cameras, &mut global, &mut popup_event);
334 }
335}
336
337#[cfg(feature = "ui")]
338fn select_next_camera_key_event(
339 global: &mut ResMut<DebugCameraGlobalData>,
340 keys: &Res<ButtonInput<KeyCode>>,
341 controls: &Res<DebugCameraControls>,
342 time: &Res<Time>,
343) -> bool {
344 if keys.just_pressed(controls.switcher_next) {
345 global.last_switch_time = time.elapsed_secs() + SELECTOR_NEXT_ELEMENT_THRESHOLD_IN_SECONDS;
346 return true;
347 }
348
349 if keys.pressed(controls.switcher_next)
350 && global.last_switch_time + SELECTOR_NEXT_ELEMENT_IN_SECONDS < time.elapsed_secs()
351 {
352 global.last_switch_time = time.elapsed_secs();
353 true
354 } else {
355 false
356 }
357}
358
359fn spawn_debug_camera_if_any_camera_exist(
360 mut commands: Commands,
361 mut mouse_motion: EventReader<MouseMotion>,
362 mut mouse_wheel: EventReader<MouseWheel>,
363 #[cfg(not(feature = "ui"))] query: Query<(), With<Camera>>,
364 #[cfg(feature = "ui")] query: Query<(), (With<Camera>, Without<ui::PreviewCamera>)>,
365) {
366 if query.is_empty() {
367 commands.spawn(DebugCamera::default()).insert(Transform {
368 translation: Vec3::new(0.0, 0.0, -5.0),
369 rotation: Quat::from_rotation_y(180.0f32.to_radians()),
370 ..Default::default()
371 });
372 }
373
374 mouse_motion.clear();
376 mouse_wheel.clear();
377}