bevy_topdown_camera/
lib.rs

1// use std::ops::Range;
2use bevy::{input::mouse::MouseWheel, prelude::*};
3
4pub mod prelude {
5    pub use crate::*;
6}
7
8#[derive(Debug, Clone, Copy, SystemSet, PartialEq, Eq, Hash)]
9pub struct TopdownCameraSystemSet;
10
11
12pub struct TopdownCameraPlugin;
13
14impl Plugin for TopdownCameraPlugin {
15    fn build(&self, app: &mut App) {
16        
17        app
18            .add_systems(
19                PostUpdate, 
20                (
21                    camera_follow_target,
22                    camera_zoom,
23                ).in_set(TopdownCameraSystemSet)
24            );
25    }
26}
27
28
29#[derive(Component, Clone, Debug, PartialEq)]
30pub struct TopdownCamera {
31    pub follow_mode: FollowMode,
32    pub follow_speed: f32,
33    pub height: f32,
34    pub zoom: ZoomSettings,
35    pub zoom_keys: ZoomKeys,
36}
37
38#[derive(Clone, Debug, PartialEq)]
39pub enum FollowMode {
40    Smooth,
41    Fixed,
42}
43
44#[derive(Clone, Debug, PartialEq)]
45pub struct ZoomSettings {
46    pub can_zoom: bool,
47    pub allow_mouse_wheel: bool,
48    pub speed: f32,
49    pub max: f32,
50    pub min: f32,
51}
52
53impl Default for ZoomSettings {
54    fn default() -> Self {
55        Self {
56            can_zoom: true,
57            allow_mouse_wheel: true,
58            speed: 1.0,
59            max: 20.0,
60            min: 1.0,
61        }
62    }
63}
64
65/// Which keys to use for zooming in and out
66#[derive(Debug, Clone, PartialEq, Eq, Hash, Reflect)]
67pub struct ZoomKeys {
68    pub zoom_in: Vec<KeyCode>,
69    pub zoom_out: Vec<KeyCode>,
70}
71
72impl ZoomKeys {
73    /// No keys move the camera
74    pub const NONE: Self = Self {
75        zoom_in: vec![],
76        zoom_out: vec![],
77    };
78
79    pub fn plus_minus() -> Self {
80        Self {
81            zoom_in: vec![KeyCode::Equal],
82            zoom_out: vec![KeyCode::Minus],
83        }
84    }
85
86    fn direction(&self, keyboard_buttons: &Res<ButtonInput<KeyCode>>) -> f32 {
87        let mut factor: f32 = 0.0;
88
89        if self.zoom_in.iter().any(|key| keyboard_buttons.pressed(*key)) {
90            factor -= 1.;
91        }
92
93        if self.zoom_out.iter().any(|key| keyboard_buttons.pressed(*key)) {
94            factor += 1.;
95        }
96        factor
97    }
98}
99
100impl Default for TopdownCamera {
101    fn default() -> Self {
102        Self { 
103            follow_mode: FollowMode::Smooth,
104            follow_speed: 5.0,
105            height: 5.0,
106            zoom: ZoomSettings::default(),
107            zoom_keys: ZoomKeys::plus_minus(),
108         }
109    }
110}
111
112
113#[derive(Component)]
114pub struct TopdownFollowTarget;
115
116
117fn camera_follow_target(
118    time: Res<Time>,
119    mut camera_query: Query<(&mut Transform, &TopdownCamera), Without<TopdownFollowTarget>>,
120    target_query: Query<&Transform, (With<TopdownFollowTarget>, Without<TopdownCamera>)>,
121) {
122    if let Ok((mut camera_transform, camera)) = camera_query.get_single_mut() {
123        if let Ok(target_transform) = target_query.get_single() {
124            let target_position = target_transform.translation;
125            
126            // Calculate the desired camera position
127            let desired_position = target_position + Vec3::new(0.0, camera.height, camera.height);
128            
129            // Update camera position based on follow mode
130            match camera.follow_mode {
131                FollowMode::Smooth => {
132                    camera_transform.translation = camera_transform.translation.lerp(
133                        desired_position,
134                        camera.follow_speed * time.delta_seconds()
135                    );
136                }
137                FollowMode::Fixed => {
138                    camera_transform.translation = desired_position;
139                }
140            }
141            
142            // Make the camera look at the target
143            let forward = (target_position - camera_transform.translation).normalize();
144            camera_transform.look_to(forward, Vec3::Y);
145        }
146    }
147}
148
149fn camera_zoom(
150    mut mouse_wheel_events: EventReader<MouseWheel>,
151    mut query: Query<(&mut Transform, &mut TopdownCamera)>,
152    keyboard_buttons: Res<ButtonInput<KeyCode>>,
153    time: Res<Time>,
154) {
155    if let Ok((_, camera)) = query.get_single() {
156        if !camera.zoom.can_zoom {
157            return;
158        }
159    }
160
161    let zoom_factor: f32 = mouse_wheel_events
162    .read()
163    .map(|event| event.y)
164    .sum();
165
166    
167    for (mut transform, mut camera) in query.iter_mut() {
168        let mut zoom_amount = 0.0;
169
170        // Mouse wheel zoom
171        if camera.zoom.allow_mouse_wheel {
172            if zoom_factor != 0.0 {
173                zoom_amount += zoom_factor * camera.zoom.speed * time.delta_seconds();
174                
175            }
176        }
177
178        // Keyboard zoom
179        let direction = camera.zoom_keys.direction(&keyboard_buttons);
180        if direction != 0.0 {
181            zoom_amount -= direction * camera.zoom.speed * time.delta_seconds();
182        }
183
184        // Apply zoom amount
185        if zoom_amount != 0.0 {
186            camera.height -= zoom_amount;
187            camera.height = camera.height.clamp(camera.zoom.min, camera.zoom.max);
188
189            // Update camera position smoothly
190            let target_y = camera.height;
191            let target_z = camera.height;
192            transform.translation.y = transform.translation.y.lerp(target_y, camera.follow_speed * time.delta_seconds());
193            transform.translation.z = transform.translation.z.lerp(target_z, camera.follow_speed * time.delta_seconds());
194        }
195            
196    }
197}