smooth_bevy_cameras/controllers/
orbit.rs

1use crate::{LookAngles, LookTransform, LookTransformBundle, Smoother};
2
3use bevy::{
4    app::prelude::*,
5    ecs::{bundle::Bundle, prelude::*},
6    input::{
7        mouse::{MouseMotion, MouseScrollUnit, MouseWheel},
8        prelude::*,
9    },
10    math::prelude::*,
11    prelude::ReflectDefault,
12    reflect::Reflect,
13    time::Time,
14    transform::components::Transform,
15};
16
17#[derive(Default)]
18pub struct OrbitCameraPlugin {
19    pub override_input_system: bool,
20}
21
22impl OrbitCameraPlugin {
23    pub fn new(override_input_system: bool) -> Self {
24        Self {
25            override_input_system,
26        }
27    }
28}
29
30impl Plugin for OrbitCameraPlugin {
31    fn build(&self, app: &mut App) {
32        let app = app
33            .add_systems(PreUpdate, on_controller_enabled_changed)
34            .add_systems(Update, control_system)
35            .add_event::<ControlEvent>();
36
37        if !self.override_input_system {
38            app.add_systems(Update, default_input_map);
39        }
40    }
41}
42
43#[derive(Bundle)]
44pub struct OrbitCameraBundle {
45    controller: OrbitCameraController,
46    look_transform: LookTransformBundle,
47    transform: Transform,
48}
49
50impl OrbitCameraBundle {
51    pub fn new(controller: OrbitCameraController, eye: Vec3, target: Vec3, up: Vec3) -> Self {
52        // Make sure the transform is consistent with the controller to start.
53        let transform = Transform::from_translation(eye).looking_at(target, up);
54
55        Self {
56            controller,
57            look_transform: LookTransformBundle {
58                transform: LookTransform::new(eye, target, up),
59                smoother: Smoother::new(controller.smoothing_weight),
60            },
61            transform,
62        }
63    }
64}
65
66/// A 3rd person camera that orbits around the target.
67#[derive(Clone, Component, Copy, Debug, Reflect)]
68#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
69#[reflect(Component, Default, Debug)]
70pub struct OrbitCameraController {
71    pub enabled: bool,
72    pub mouse_rotate_sensitivity: Vec2,
73    pub mouse_translate_sensitivity: Vec2,
74    pub mouse_wheel_zoom_sensitivity: f32,
75    pub pixels_per_line: f32,
76    pub smoothing_weight: f32,
77}
78
79impl Default for OrbitCameraController {
80    fn default() -> Self {
81        Self {
82            mouse_rotate_sensitivity: Vec2::splat(0.08),
83            mouse_translate_sensitivity: Vec2::splat(0.1),
84            mouse_wheel_zoom_sensitivity: 0.2,
85            smoothing_weight: 0.8,
86            enabled: true,
87            pixels_per_line: 53.0,
88        }
89    }
90}
91
92#[derive(Event)]
93pub enum ControlEvent {
94    Orbit(Vec2),
95    TranslateTarget(Vec2),
96    Zoom(f32),
97}
98
99define_on_controller_enabled_changed!(OrbitCameraController);
100
101pub fn default_input_map(
102    mut events: EventWriter<ControlEvent>,
103    mut mouse_wheel_reader: EventReader<MouseWheel>,
104    mut mouse_motion_events: EventReader<MouseMotion>,
105    mouse_buttons: Res<ButtonInput<MouseButton>>,
106    keyboard: Res<ButtonInput<KeyCode>>,
107    controllers: Query<&OrbitCameraController>,
108) {
109    // Can only control one camera at a time.
110    let controller = if let Some(controller) = controllers.iter().find(|c| c.enabled) {
111        controller
112    } else {
113        return;
114    };
115    let OrbitCameraController {
116        mouse_rotate_sensitivity,
117        mouse_translate_sensitivity,
118        mouse_wheel_zoom_sensitivity,
119        pixels_per_line,
120        ..
121    } = *controller;
122
123    let mut cursor_delta = Vec2::ZERO;
124    for event in mouse_motion_events.read() {
125        cursor_delta += event.delta;
126    }
127
128    if keyboard.pressed(KeyCode::ControlLeft) {
129        events.write(ControlEvent::Orbit(mouse_rotate_sensitivity * cursor_delta));
130    }
131
132    if mouse_buttons.pressed(MouseButton::Right) {
133        events.write(ControlEvent::TranslateTarget(
134            mouse_translate_sensitivity * cursor_delta,
135        ));
136    }
137
138    let mut scalar = 1.0;
139    for event in mouse_wheel_reader.read() {
140        // scale the event magnitude per pixel or per line
141        let scroll_amount = match event.unit {
142            MouseScrollUnit::Line => event.y,
143            MouseScrollUnit::Pixel => event.y / pixels_per_line,
144        };
145        scalar *= 1.0 - scroll_amount * mouse_wheel_zoom_sensitivity;
146    }
147    events.write(ControlEvent::Zoom(scalar));
148}
149
150pub fn control_system(
151    time: Res<Time>,
152    mut events: EventReader<ControlEvent>,
153    mut cameras: Query<(&OrbitCameraController, &mut LookTransform, &Transform)>,
154) {
155    // Can only control one camera at a time.
156    let (mut transform, scene_transform) =
157        if let Some((_, transform, scene_transform)) = cameras.iter_mut().find(|c| c.0.enabled) {
158            (transform, scene_transform)
159        } else {
160            return;
161        };
162
163    let mut look_angles = LookAngles::from_vector(-transform.look_direction().unwrap());
164    let mut radius_scalar = 1.0;
165    let radius = transform.radius();
166
167    let dt = time.delta_secs();
168    for event in events.read() {
169        match event {
170            ControlEvent::Orbit(delta) => {
171                look_angles.add_yaw(dt * -delta.x);
172                look_angles.add_pitch(dt * delta.y);
173            }
174            ControlEvent::TranslateTarget(delta) => {
175                let right_dir = scene_transform.rotation * -Vec3::X;
176                let up_dir = scene_transform.rotation * Vec3::Y;
177                transform.target += dt * delta.x * right_dir + dt * delta.y * up_dir;
178            }
179            ControlEvent::Zoom(scalar) => {
180                radius_scalar *= scalar;
181            }
182        }
183    }
184
185    look_angles.assert_not_looking_up();
186
187    let new_radius = (radius_scalar * radius).min(1000000.0).max(0.001);
188    transform.eye = transform.target + new_radius * look_angles.unit_vector();
189}