src/camera.rs 6.6 K · 227 lines · raw · history

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