Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_camera_controller/src/pan_camera.rs
7228 views
1
//! A controller for 2D cameras that supports panning, zooming, and rotation.
2
//!
3
//! To use this controller, add [`PanCameraPlugin`] to your app,
4
//! and insert a [`PanCamera`] component into your camera entity.
5
//!
6
//! To configure the settings of this controller, modify the fields of the [`PanCamera`] component.
7
8
use bevy_app::{App, Plugin, RunFixedMainLoop, RunFixedMainLoopSystems};
9
use bevy_camera::Camera;
10
use bevy_ecs::prelude::*;
11
use bevy_input::keyboard::KeyCode;
12
use bevy_input::mouse::{AccumulatedMouseScroll, MouseScrollUnit};
13
use bevy_input::ButtonInput;
14
use bevy_math::{Vec2, Vec3};
15
use bevy_time::{Real, Time};
16
use bevy_transform::prelude::Transform;
17
18
use core::{f32::consts::*, fmt};
19
20
/// A plugin that enables 2D camera panning and zooming controls.
21
///
22
/// Add this plugin to your [`App`] to enable [`PanCamera`] behavior
23
/// on any camera entity that has the [`PanCamera`] component.
24
pub struct PanCameraPlugin;
25
26
impl Plugin for PanCameraPlugin {
27
fn build(&self, app: &mut App) {
28
app.add_systems(
29
RunFixedMainLoop,
30
run_pancamera_controller.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop),
31
);
32
}
33
}
34
35
/// Configuration and state for a 2D panning camera controller.
36
///
37
/// Add this component to a [`Camera`] entity to enable keyboard and mouse controls
38
/// for panning, zooming, and optional rotation. Requires the [`PanCameraPlugin`].
39
#[derive(Component)]
40
pub struct PanCamera {
41
/// Enables this [`PanCamera`] when `true`.
42
pub enabled: bool,
43
/// Current zoom level (factor applied to camera scale).
44
pub zoom_factor: f32,
45
/// Minimum allowed zoom level.
46
pub min_zoom: f32,
47
/// Maximum allowed zoom level.
48
pub max_zoom: f32,
49
/// Translation speed for panning movement.
50
pub zoom_speed: f32,
51
/// [`KeyCode`] to zoom in.
52
pub key_zoom_in: Option<KeyCode>,
53
/// [`KeyCode`] to zoom out.
54
pub key_zoom_out: Option<KeyCode>,
55
/// This [`PanCamera`]'s translation speed.
56
pub pan_speed: f32,
57
/// [`KeyCode`] for upward translation.
58
pub key_up: Option<KeyCode>,
59
/// [`KeyCode`] for downward translation.
60
pub key_down: Option<KeyCode>,
61
/// [`KeyCode`] for leftward translation.
62
pub key_left: Option<KeyCode>,
63
/// [`KeyCode`] for rightward translation.
64
pub key_right: Option<KeyCode>,
65
/// Rotation speed multiplier (in radians per second).
66
pub rotation_speed: f32,
67
/// [`KeyCode`] for counter-clockwise rotation.
68
pub key_rotate_ccw: Option<KeyCode>,
69
/// [`KeyCode`] for clockwise rotation.
70
pub key_rotate_cw: Option<KeyCode>,
71
}
72
73
/// Provides the default values for the `PanCamera` controller.
74
///
75
/// The default settings are:
76
/// - Zoom factor: 1.0
77
/// - Min zoom: 0.1
78
/// - Max zoom: 5.0
79
/// - Zoom speed: 0.1
80
/// - Zoom in/out key: +/-
81
/// - Pan speed: 500.0
82
/// - Move up/down: W/S
83
/// - Move left/right: A/D
84
/// - Rotation speed: PI (radiradians per second)
85
/// - Rotation ccw/cw: Q/E
86
impl Default for PanCamera {
87
/// Provides the default values for the `PanCamera` controller.
88
///
89
/// Users can override these values by manually creating a `PanCamera` instance
90
/// or modifying the default instance.
91
fn default() -> Self {
92
Self {
93
enabled: true,
94
zoom_factor: 1.0,
95
min_zoom: 0.1,
96
max_zoom: 5.0,
97
zoom_speed: 0.1,
98
key_zoom_in: Some(KeyCode::Equal),
99
key_zoom_out: Some(KeyCode::Minus),
100
pan_speed: 500.0,
101
key_up: Some(KeyCode::KeyW),
102
key_down: Some(KeyCode::KeyS),
103
key_left: Some(KeyCode::KeyA),
104
key_right: Some(KeyCode::KeyD),
105
rotation_speed: PI,
106
key_rotate_ccw: Some(KeyCode::KeyQ),
107
key_rotate_cw: Some(KeyCode::KeyE),
108
}
109
}
110
}
111
112
impl PanCamera {
113
fn key_to_string(key: &Option<KeyCode>) -> String {
114
key.map_or("None".to_string(), |k| format!("{:?}", k))
115
}
116
}
117
118
impl fmt::Display for PanCamera {
119
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120
write!(
121
f,
122
"
123
PanCamera Controls:
124
Move Up / Down - {} / {}
125
Move Left / Right - {} / {}
126
Rotate CCW / CW - {} / {}
127
Zoom - Mouse Scroll + {} / {}
128
",
129
Self::key_to_string(&self.key_up),
130
Self::key_to_string(&self.key_down),
131
Self::key_to_string(&self.key_left),
132
Self::key_to_string(&self.key_right),
133
Self::key_to_string(&self.key_rotate_ccw),
134
Self::key_to_string(&self.key_rotate_cw),
135
Self::key_to_string(&self.key_zoom_in),
136
Self::key_to_string(&self.key_zoom_out),
137
)
138
}
139
}
140
141
/// This system is typically added via the [`PanCameraPlugin`].
142
///
143
/// Reads inputs and then moves the camera entity according
144
/// to the settings given in [`PanCamera`].
145
///
146
/// **Note**: The zoom applied in this controller is linear. The zoom factor is directly adjusted
147
/// based on the input (either from the mouse scroll or keyboard).
148
fn run_pancamera_controller(
149
time: Res<Time<Real>>,
150
key_input: Res<ButtonInput<KeyCode>>,
151
accumulated_mouse_scroll: Res<AccumulatedMouseScroll>,
152
mut query: Query<(&mut Transform, &mut PanCamera), With<Camera>>,
153
) {
154
let dt = time.delta_secs();
155
156
let Ok((mut transform, mut controller)) = query.single_mut() else {
157
return;
158
};
159
160
if !controller.enabled {
161
return;
162
}
163
164
// === Movement
165
let mut movement = Vec2::ZERO;
166
if let Some(key) = controller.key_left {
167
if key_input.pressed(key) {
168
movement.x -= 1.0;
169
}
170
}
171
if let Some(key) = controller.key_right {
172
if key_input.pressed(key) {
173
movement.x += 1.0;
174
}
175
}
176
if let Some(key) = controller.key_down {
177
if key_input.pressed(key) {
178
movement.y -= 1.0;
179
}
180
}
181
if let Some(key) = controller.key_up {
182
if key_input.pressed(key) {
183
movement.y += 1.0;
184
}
185
}
186
187
if movement != Vec2::ZERO {
188
let right = transform.right();
189
let up = transform.up();
190
191
let delta = (right * movement.x + up * movement.y).normalize() * controller.pan_speed * dt;
192
193
transform.translation.x += delta.x;
194
transform.translation.y += delta.y;
195
}
196
197
// === Rotation
198
if let Some(key) = controller.key_rotate_ccw {
199
if key_input.pressed(key) {
200
transform.rotate_z(controller.rotation_speed * dt);
201
}
202
}
203
if let Some(key) = controller.key_rotate_cw {
204
if key_input.pressed(key) {
205
transform.rotate_z(-controller.rotation_speed * dt);
206
}
207
}
208
209
// === Zoom
210
let mut zoom_amount = 0.0;
211
212
// (with keys)
213
if let Some(key) = controller.key_zoom_in {
214
if key_input.pressed(key) {
215
zoom_amount -= controller.zoom_speed;
216
}
217
}
218
if let Some(key) = controller.key_zoom_out {
219
if key_input.pressed(key) {
220
zoom_amount += controller.zoom_speed;
221
}
222
}
223
224
// (with mouse wheel)
225
let mouse_scroll = match accumulated_mouse_scroll.unit {
226
MouseScrollUnit::Line => accumulated_mouse_scroll.delta.y,
227
MouseScrollUnit::Pixel => {
228
accumulated_mouse_scroll.delta.y / MouseScrollUnit::SCROLL_UNIT_CONVERSION_FACTOR
229
}
230
};
231
zoom_amount += mouse_scroll * controller.zoom_speed;
232
233
controller.zoom_factor =
234
(controller.zoom_factor - zoom_amount).clamp(controller.min_zoom, controller.max_zoom);
235
236
transform.scale = Vec3::splat(controller.zoom_factor);
237
}
238
239