bevy_prank/
three.rs

1//! Provides three-dimensional camera functionality.
2
3use self::{gizmo::Prank3dGizmoPlugin, hud::Prank3dHudPlugin};
4use bevy::{
5    input::mouse::{MouseMotion, MouseWheel},
6    prelude::*,
7    render::camera::NormalizedRenderTarget,
8    window::{CursorGrabMode, PrimaryWindow},
9};
10use std::f32::consts;
11
12pub mod gizmo;
13pub mod hud;
14
15pub(super) struct Prank3dPlugin;
16
17impl Plugin for Prank3dPlugin {
18    fn build(&self, app: &mut App) {
19        app.add_plugins((Prank3dGizmoPlugin, Prank3dHudPlugin))
20            .register_type::<Prank3d>()
21            .init_resource::<Prank3dActive>()
22            .add_state::<Prank3dMode>()
23            .add_systems(PreUpdate, (sync_active, mode.after(sync_active)))
24            .add_systems(
25                Update,
26                (
27                    initialize,
28                    sync_cursor.run_if(
29                        |active: Res<Prank3dActive>, mode: Res<State<Prank3dMode>>| {
30                            active.is_changed() || mode.is_changed()
31                        },
32                    ),
33                    interpolation,
34                    fly.run_if(in_state(Prank3dMode::Fly)),
35                    offset.run_if(in_state(Prank3dMode::Offset)),
36                ),
37            );
38    }
39}
40
41#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, States)]
42enum Prank3dMode {
43    Fly,
44    Offset,
45    #[default]
46    None,
47}
48
49#[derive(Default, Resource)]
50struct Prank3dActive(Option<Entity>);
51
52/// Adds debug functionality to [`Camera3dBundle`].
53///
54/// Once [`Prank3d`] is attached to an entity, its `rotation` field of [`Transform`] should not be
55/// mutated manually.
56///
57/// # Example
58///
59/// ```
60/// # use bevy::prelude::*;
61/// # use bevy_prank::prelude::*;
62/// #
63/// fn setup(mut commands: Commands) {
64///     commands.spawn((
65///         Prank3d::default(),
66///         Camera3dBundle::default(),
67///     ));
68/// }
69/// #
70/// # bevy::ecs::system::assert_is_system(setup);
71/// ```
72#[derive(Reflect, Component)]
73#[reflect(Component)]
74pub struct Prank3d {
75    /// Whether user inputs should be applied to this [`Camera`].
76    ///
77    /// If more than one [`Camera`] with their `target` field set to the same window have this
78    /// enabled, only one of them will be picked.
79    pub is_active: bool,
80
81    /// Constant speed that the [`Camera`] moves at.
82    pub speed: f32,
83
84    /// Scalar of `speed` field to adjust during gameplay with [`MouseWheel`].
85    pub speed_scalar: f32,
86
87    /// The rate that the [`Camera`] approaches its translation.
88    ///
89    /// Values closer to zero make the approaching faster.
90    /// Zero disables interpolation.
91    ///
92    /// # Panic
93    ///
94    /// If its not in range `[0.0, 1.0)`.
95    pub lerp_rate: f32,
96
97    /// Sensitivity of [`MouseMotion`].
98    pub sensitivity: Vec2,
99
100    /// The current translation that the camera approaches towards.
101    ///
102    /// This should be used instead of [`Transform`]'s `translation` field, with the exception of
103    /// initializing the [`Transform`] component.
104    pub translation: Vec3,
105}
106
107impl Default for Prank3d {
108    fn default() -> Self {
109        Self {
110            is_active: true,
111            speed: 25.0,
112            speed_scalar: 1.0,
113            lerp_rate: 0.001,
114            sensitivity: Vec2::splat(0.08),
115            translation: Vec3::ZERO,
116        }
117    }
118}
119
120fn sync_active(
121    primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
122    windows: Query<(Entity, &Window), Without<PrimaryWindow>>,
123    pranks: Query<(Entity, &Camera, &Prank3d)>,
124    mut active: ResMut<Prank3dActive>,
125) {
126    let primary_window = primary_window.get_single().ok();
127    let Some(focused_window) = windows
128        .iter()
129        .find(|(_, window)| window.focused)
130        .map(|(entity, _)| entity)
131        .or_else(|| primary_window.and_then(|(entity, window)| window.focused.then_some(entity)))
132    else {
133        return;
134    };
135
136    let active_entity = pranks
137        .iter()
138        .find(|(_, camera, prank)| {
139            if !prank.is_active {
140                return false;
141            }
142            let Some(NormalizedRenderTarget::Window(winref)) = camera
143                .target
144                .normalize(primary_window.map(|(entity, _)| entity))
145            else {
146                return false;
147            };
148
149            winref.entity() == focused_window
150        })
151        .map(|(entity, _, _)| entity);
152
153    if active_entity != active.0 {
154        *active = Prank3dActive(active_entity);
155    }
156}
157
158fn mode(
159    active: Res<Prank3dActive>,
160    prev_mode: Res<State<Prank3dMode>>,
161    mut mode: ResMut<NextState<Prank3dMode>>,
162    mouse: Res<Input<MouseButton>>,
163) {
164    if active.0.is_none() {
165        mode.set(Prank3dMode::None);
166        return;
167    }
168
169    match **prev_mode {
170        Prank3dMode::Fly => {
171            if !mouse.pressed(MouseButton::Right) {
172                mode.set(Prank3dMode::None);
173            }
174        }
175        Prank3dMode::Offset => {
176            if !mouse.pressed(MouseButton::Middle) {
177                mode.set(Prank3dMode::None);
178            }
179        }
180        Prank3dMode::None => {
181            if mouse.pressed(MouseButton::Right) {
182                mode.set(Prank3dMode::Fly);
183            } else if mouse.pressed(MouseButton::Middle) {
184                mode.set(Prank3dMode::Offset);
185            }
186        }
187    }
188}
189
190fn initialize(mut pranks: Query<(&mut Prank3d, &Transform), Added<Prank3d>>) {
191    for (mut prank, transform) in pranks.iter_mut() {
192        if !(0.0..1.0).contains(&prank.lerp_rate) {
193            panic!("`lerp_rate` field of `bevy_prank::three::Prank3d` must be in range [0.0, 1.0)");
194        }
195
196        prank.translation = transform.translation;
197    }
198}
199
200fn sync_cursor(
201    mut primary_window: Query<(Entity, &mut Window), With<PrimaryWindow>>,
202    mut windows: Query<&mut Window, Without<PrimaryWindow>>,
203    active: Res<Prank3dActive>,
204    pranks: Query<&Camera, With<Prank3d>>,
205    mode: Res<State<Prank3dMode>>,
206) {
207    let Some(entity) = active.0 else {
208        return;
209    };
210    let camera = pranks.get(entity).expect("exists");
211
212    let Some(NormalizedRenderTarget::Window(winref)) = camera
213        .target
214        .normalize(primary_window.get_single().ok().map(|(entity,_)| entity))
215    else {
216        return;
217    };
218
219    let mut window = match windows.get_mut(winref.entity()) {
220        Ok(window) => window,
221        Err(_) => {
222            let Ok((_, window)) = primary_window.get_single_mut() else {
223                return;
224            };
225
226            window
227        }
228    };
229
230    match **mode {
231        Prank3dMode::Fly => {
232            window.cursor.visible = false;
233            window.cursor.grab_mode = CursorGrabMode::Locked;
234        }
235        Prank3dMode::Offset => {
236            window.cursor.visible = false;
237            window.cursor.grab_mode = CursorGrabMode::Locked;
238        }
239        Prank3dMode::None => {
240            window.cursor.visible = true;
241            window.cursor.grab_mode = CursorGrabMode::None;
242        }
243    }
244}
245
246fn interpolation(
247    active: Res<Prank3dActive>,
248    mut pranks: Query<(&mut Transform, &Prank3d)>,
249    time: Res<Time>,
250) {
251    let Some(entity) = active.0 else {
252        return;
253    };
254    let (mut transform, prank) = pranks.get_mut(entity).expect("exists");
255
256    transform.translation = transform.translation.lerp(
257        prank.translation,
258        1.0 - prank.lerp_rate.powf(time.delta_seconds()),
259    );
260}
261
262fn fly(
263    active: Res<Prank3dActive>,
264    mut pranks: Query<(&mut Transform, &mut Prank3d)>,
265    time: Res<Time>,
266    mut motion: EventReader<MouseMotion>,
267    mut wheel: EventReader<MouseWheel>,
268    keyboard: Res<Input<KeyCode>>,
269) {
270    let Some(entity) = active.0 else {
271        return;
272    };
273    let (mut transform, mut prank) = pranks.get_mut(entity).expect("exists");
274    let motion = motion.iter().fold(Vec2::ZERO, |acc, m| acc + m.delta);
275    let wheel = wheel.iter().fold(0.0, |acc, w| acc + w.y);
276    let mut movement = Vec3::ZERO;
277    if keyboard.pressed(KeyCode::W) {
278        movement += transform.forward();
279    }
280    if keyboard.pressed(KeyCode::A) {
281        movement += transform.left();
282    }
283    if keyboard.pressed(KeyCode::S) {
284        movement += transform.back();
285    }
286    if keyboard.pressed(KeyCode::D) {
287        movement += transform.right();
288    }
289    if keyboard.pressed(KeyCode::ShiftLeft) {
290        movement = Vec3::new(movement.x, 0.0, movement.z);
291    }
292    if keyboard.pressed(KeyCode::E) {
293        movement += Vec3::Y;
294    }
295    if keyboard.pressed(KeyCode::Q) {
296        movement += Vec3::NEG_Y;
297    }
298
299    prank.speed_scalar = (prank.speed_scalar + 0.1 * wheel).clamp(0.1, 10.0);
300
301    let speed = prank.speed_scalar.powi(2) * prank.speed;
302    prank.translation += speed * movement.normalize_or_zero() * time.delta_seconds();
303
304    let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
305    transform.rotation = Quat::from_euler(
306        EulerRot::YXZ,
307        yaw - prank.sensitivity.x * motion.x * time.delta_seconds(),
308        (pitch - prank.sensitivity.y * motion.y * time.delta_seconds())
309            .clamp(-consts::FRAC_PI_3, consts::FRAC_PI_3),
310        0.0,
311    );
312}
313
314fn offset(
315    active: Res<Prank3dActive>,
316    mut pranks: Query<(&mut Transform, &mut Prank3d)>,
317    time: Res<Time>,
318    mut motion: EventReader<MouseMotion>,
319) {
320    let Some(entity) = active.0 else {
321        return;
322    };
323    let (mut transform, mut prank) = pranks.get_mut(entity).expect("exists");
324    let motion = motion.iter().fold(Vec2::ZERO, |acc, m| acc + m.delta);
325
326    let r = transform.rotation;
327    transform.translation += r * Vec3::new(motion.x, -motion.y, 0.0) * time.delta_seconds();
328    prank.translation = transform.translation;
329}