src/camera.rs 5.9 K · 192 lines · raw · history

1 use bevy::{
2 input::mouse::{AccumulatedMouseScroll, MouseScrollPixelsPerLine},
3 prelude::*,
4 window::PrimaryWindow,
5 };
6
7 use crate::world::WorldMap;
8
9 pub struct CameraPlugin;
10
11 const DRAG_BUTTONS: [MouseButton; 2] = [MouseButton::Middle, MouseButton::Right];
12
13 impl Plugin for CameraPlugin {
14 fn build(&self, app: &mut App) {
15 app.add_systems(Startup, spawn_camera).add_systems(
16 Update,
17 (keyboard_pan, drag_pan, scroll_zoom, clamp_to_world).chain(),
18 );
19 }
20 }
21
22 #[derive(Component, Clone, Debug)]
23 pub struct CameraController {
24 pub pan_speed: f32,
25 pub fast_multiplier: f32,
26 pub zoom_per_line: f32,
27 pub min_scale: f32,
28 pub max_scale: f32,
29 }
30
31 impl Default for CameraController {
32 fn default() -> Self {
33 Self {
34 pan_speed: 600.0,
35 fast_multiplier: 3.0,
36 zoom_per_line: 0.15,
37 min_scale: 0.2,
38 max_scale: 4.0,
39 }
40 }
41 }
42
43 fn spawn_camera(mut commands: Commands, map: Res<WorldMap>) {
44 commands.spawn((
45 Camera2d,
46 CameraController::default(),
47 Transform::from_translation((map.world_size() / 2.0).extend(0.0)),
48 ));
49 }
50
51 fn ortho(projection: &mut Projection) -> &mut OrthographicProjection {
52 match projection {
53 Projection::Orthographic(o) => o,
54 _ => panic!("camera must use an orthographic projection"),
55 }
56 }
57
58 fn keyboard_pan(
59 keys: Res<ButtonInput<KeyCode>>,
60 time: Res<Time>,
61 camera: Single<(&mut Transform, &mut Projection, &CameraController)>,
62 ) {
63 let (mut transform, mut projection, controller) = camera.into_inner();
64 let mut dir = Vec2::ZERO;
65 for (key, d) in [
66 (KeyCode::KeyW, Vec2::Y),
67 (KeyCode::ArrowUp, Vec2::Y),
68 (KeyCode::KeyS, Vec2::NEG_Y),
69 (KeyCode::ArrowDown, Vec2::NEG_Y),
70 (KeyCode::KeyA, Vec2::NEG_X),
71 (KeyCode::ArrowLeft, Vec2::NEG_X),
72 (KeyCode::KeyD, Vec2::X),
73 (KeyCode::ArrowRight, Vec2::X),
74 ] {
75 if keys.pressed(key) {
76 dir += d;
77 }
78 }
79 if dir == Vec2::ZERO {
80 return;
81 }
82 let fast = keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight]);
83 let speed = controller.pan_speed * if fast { controller.fast_multiplier } else { 1.0 };
84 let scale = ortho(&mut projection).scale;
85 transform.translation += (dir.normalize() * speed * scale * time.delta_secs()).extend(0.0);
86 }
87
88 fn drag_pan(
89 buttons: Res<ButtonInput<MouseButton>>,
90 window: Single<&Window, With<PrimaryWindow>>,
91 camera: Single<(&mut Transform, &mut Projection), With<CameraController>>,
92 mut last_cursor: Local<Option<Vec2>>,
93 ) {
94 let cursor = window.cursor_position();
95 let previous = std::mem::replace(&mut *last_cursor, cursor);
96 if !buttons.any_pressed(DRAG_BUTTONS) {
97 return;
98 }
99 let (Some(cursor), Some(previous)) = (cursor, previous) else {
100 return;
101 };
102 let (mut transform, mut projection) = camera.into_inner();
103 let scale = ortho(&mut projection).scale;
104 transform.translation += (screen_to_world_delta(cursor - previous) * -scale).extend(0.0);
105 }
106
107 fn scroll_zoom(
108 scroll: Res<AccumulatedMouseScroll>,
109 pixels_per_line: Res<MouseScrollPixelsPerLine>,
110 window: Single<&Window, With<PrimaryWindow>>,
111 camera: Single<(
112 &Camera,
113 &GlobalTransform,
114 &mut Transform,
115 &mut Projection,
116 &CameraController,
117 )>,
118 ) {
119 let lines = scroll.to_lines(&pixels_per_line).delta.y;
120 if lines == 0.0 {
121 return;
122 }
123 let (camera, global, mut transform, mut projection, controller) = camera.into_inner();
124 let projection = ortho(&mut projection);
125 let old_scale = projection.scale;
126 let new_scale = (old_scale * (1.0 - lines * controller.zoom_per_line))
127 .clamp(controller.min_scale, controller.max_scale);
128 projection.scale = new_scale;
129
130 let focus = window
131 .cursor_position()
132 .and_then(|cursor| camera.viewport_to_world_2d(global, cursor).ok());
133 if let Some(focus) = focus {
134 let pos = zoom_towards(transform.translation.truncate(), focus, old_scale, new_scale);
135 transform.translation = pos.extend(transform.translation.z);
136 }
137 }
138
139 fn clamp_to_world(map: Res<WorldMap>, mut camera: Single<&mut Transform, With<CameraController>>) {
140 let clamped = clamp_to_rect(camera.translation.truncate(), Vec2::ZERO, map.world_size());
141 camera.translation = clamped.extend(camera.translation.z);
142 }
143
144 pub fn screen_to_world_delta(screen: Vec2) -> Vec2 {
145 Vec2::new(screen.x, -screen.y)
146 }
147
148 pub fn zoom_towards(camera: Vec2, focus: Vec2, old_scale: f32, new_scale: f32) -> Vec2 {
149 focus + (camera - focus) * (new_scale / old_scale)
150 }
151
152 pub fn clamp_to_rect(pos: Vec2, min: Vec2, max: Vec2) -> Vec2 {
153 pos.clamp(min, max)
154 }
155
156 #[cfg(test)]
157 mod tests {
158 use super::*;
159
160 fn world_point_at_offset(camera: Vec2, scale: f32, screen_offset: Vec2) -> Vec2 {
161 camera + screen_offset * scale
162 }
163
164 #[test]
165 fn zooming_keeps_the_point_under_the_cursor_fixed() {
166 let camera = Vec2::new(100.0, 50.0);
167 let cursor_offset = Vec2::new(200.0, -120.0);
168 let focus = world_point_at_offset(camera, 1.0, cursor_offset);
169
170 let new_camera = zoom_towards(camera, focus, 1.0, 0.5);
171
172 assert!(world_point_at_offset(new_camera, 0.5, cursor_offset).abs_diff_eq(focus, 1e-4));
173 }
174
175 #[test]
176 fn zooming_on_camera_center_does_not_move_camera() {
177 let camera = Vec2::new(10.0, 20.0);
178 assert_eq!(zoom_towards(camera, camera, 1.0, 3.0), camera);
179 }
180
181 #[test]
182 fn dragging_moves_world_opposite_to_screen_y_because_screen_y_points_down() {
183 assert_eq!(screen_to_world_delta(Vec2::new(3.0, 4.0)), Vec2::new(3.0, -4.0));
184 }
185
186 #[test]
187 fn camera_center_cannot_leave_the_world_so_the_map_stays_on_screen() {
188 let max = Vec2::new(100.0, 50.0);
189 assert_eq!(clamp_to_rect(Vec2::new(-5.0, 60.0), Vec2::ZERO, max), Vec2::new(0.0, 50.0));
190 assert_eq!(clamp_to_rect(Vec2::new(5.0, 6.0), Vec2::ZERO, max), Vec2::new(5.0, 6.0));
191 }
192 }