bevy_dev/debug_camera/
mod.rs

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
25/// Plugin for [`crate::debug_camera`] feature.
26///
27/// # Remarks
28/// This plugin is necessary to use [`crate::debug_camera`] feature. It is added to App by [`crate::DevPlugins`].
29///
30/// If `ui` feature is enabled, it will require to add [`crate::ui::DebugUiPlugin`] to App, before adding this.
31pub struct DebugCameraPlugin {
32    /// Allows to switch between cameras, and spawn new debug cameras.
33    pub switcher: DebugCameraSwitcher,
34    /// Show debug camera renderer preview in selector UI.
35    ///
36    /// If enabled
37    ///
38    /// ![Preview enabled](https://raw.githubusercontent.com/Vixenka/bevy_dev/master/images/debug_camera/switching.webp)
39    ///
40    /// If disabled
41    ///
42    /// ![Preview disabled](https://raw.githubusercontent.com/Vixenka/bevy_dev/master/images/debug_camera/switching_without_preview.webp)
43    ///
44    /// # Remarks
45    /// This feature requires `ui` feature to be enabled.
46    ///
47    /// Preview is rendered only when `UI` is showed, and rendered in low resolution. Only one debug camera refresh their preview in one frame, what do not affect performance so much.
48    #[cfg(feature = "ui")]
49    pub show_preview: bool,
50    /// Spawn debug camera if any camera exist.
51    ///
52    /// # Remarks
53    /// Camera is spawned with default values in any [`PostUpdate`] stage if any camera exist.
54    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/// Setting for debug camera switcher.
108#[derive(Default)]
109pub enum DebugCameraSwitcher {
110    /// The same as [`DebugCameraSwitcher::Active`], but in release builds it will make a warning.
111    #[default]
112    Default,
113    /// Active debug camera switcher.
114    Active,
115    /// Disable debug camera switcher.
116    Disabled,
117}
118
119/// Global data for debug camera.
120#[derive(Debug, Resource)]
121pub struct DebugCameraGlobalData {
122    /// Default values for new created debug cameras.
123    pub default_value: DebugCamera,
124    /// Last used debug cameras, in order.
125    pub last_used_debug_cameras: Vec<Entity>,
126    /// Last used origin camera.
127    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/// Controls used for debug camera.
149#[derive(Debug, Resource)]
150pub struct DebugCameraControls {
151    /// Move forward key, default is [`KeyCode::KeyW`].
152    pub move_forward: KeyCode,
153    /// Move backward key, default is [`KeyCode::KeyS`].
154    pub move_backward: KeyCode,
155    /// Move left key, default is [`KeyCode::KeyA`].
156    pub move_left: KeyCode,
157    /// Move right key, default is [`KeyCode::KeyD`].
158    pub move_right: KeyCode,
159    /// Move up key, default is [`KeyCode::KeyE`].
160    pub move_up: KeyCode,
161    /// Move down key, default is [`KeyCode::KeyQ`].
162    pub move_down: KeyCode,
163    /// Base key used to use switcher, default is [`KeyCode::ShiftLeft`].
164    pub switcher_special: KeyCode,
165    /// Select next debug camera, default is [`KeyCode::Tab`].
166    pub switcher_next: KeyCode,
167    /// Spawn new debug camera, default is [`KeyCode::F1`].
168    pub new_debug_camera: KeyCode,
169    /// Return to game camera, default is [`KeyCode::Escape`].
170    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/// Container which contains data about last used origin camera.
191#[derive(Debug)]
192pub struct DebugCameraLastUsedOriginCameraData {
193    /// Entity of last used origin camera.
194    pub camera: Entity,
195    /// Cursor of window before switching to debug camera.
196    pub cursor: CursorOptions,
197}
198
199/// Debug camera component. Apply to entity to make it debug camera.
200#[derive(Component, Debug, Clone)]
201#[non_exhaustive]
202pub struct DebugCamera {
203    /// Speed increase during flight.
204    pub speed_increase: f32,
205    /// Sensitivity of managing speed multiplier via mouse wheel.
206    pub speed_multiplier: f32,
207    /// Range of speed multiplier.
208    pub speed_multiplier_range: RangeInclusive<f32>,
209    /// Sensitivity of camera rotation.
210    pub sensitivity: f32,
211    /// Base speed of camera.
212    pub base_speed: f32,
213    /// Focus on camera. Manage it activation.
214    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    // Spawn new
279    if keys.just_pressed(controls.new_debug_camera) {
280        commands.spawn(DebugCamera::default());
281        return;
282    }
283
284    // Switch to game camera
285    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    // Switch to selected debug camera
304    #[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    // Show UI for debug camera selection
331    #[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    // Clear first gained events
375    mouse_motion.clear();
376    mouse_wheel.clear();
377}