bevy_cameraman/
lib.rs

1use bevy::{
2    app::{PostStartup, Update},
3    asset::Assets,
4    ecs::{
5        bundle::Bundle,
6        component::Component,
7        system::{Commands, Res, ResMut},
8    },
9    gizmos::{gizmos::Gizmos, GizmoConfig},
10    math::{Vec2, Vec3},
11    prelude::{
12        default, App, Camera2dBundle, Entity, Plugin, PostUpdate, Query, Time, Timer, TimerMode,
13        Transform, With, Without,
14    },
15    render::{
16        color::Color,
17        mesh::{shape, Mesh},
18    },
19    sprite::{ColorMaterial, MaterialMesh2dBundle},
20};
21
22#[derive(Component)]
23pub struct Target;
24
25#[derive(Component)]
26pub struct Cameraman {
27    target: Entity, // TODO: find a way to have multiple targets per camera, but also being able to have multi cameras (n-n)
28    dead_zone: Vec2,
29    target_prev_translation: Vec3,
30    // look at this position, this is the player + velocity + factor
31    // it allow us to place the camera a bit ahead of time
32    look_at: Vec3,
33    ahead_factor: Vec3,
34    traveling: bool,
35    center_after: Timer,
36}
37
38impl Cameraman {
39    pub fn new(target: Entity, dead_zone: Vec2, ahead_factor: Vec3) -> Self {
40        Self {
41            target,
42            dead_zone,
43            target_prev_translation: Vec3::ZERO,
44            look_at: Vec3::ZERO,
45            ahead_factor,
46            traveling: false,
47            center_after: Timer::from_seconds(0.4, TimerMode::Once),
48        }
49    }
50
51    pub fn new_default(target: Entity) -> Self {
52        Self {
53            target,
54            dead_zone: Vec2::new(30.0, 15.0),
55            target_prev_translation: Vec3::ZERO,
56            look_at: Vec3::ZERO,
57            ahead_factor: Vec3::ONE,
58            traveling: false,
59            center_after: Timer::from_seconds(0.4, TimerMode::Once),
60        }
61    }
62}
63
64#[derive(Bundle)]
65pub struct CameraBundle {
66    camera: Cameraman,
67    bundle: Camera2dBundle,
68}
69
70impl CameraBundle {
71    pub fn new(camera: Cameraman, bundle: Camera2dBundle) -> Self {
72        Self { camera, bundle }
73    }
74
75    pub fn new_with_default_bundle(target: Entity) -> Self {
76        Self {
77            camera: Cameraman::new_default(target),
78            bundle: Camera2dBundle::default(),
79        }
80    }
81}
82
83pub struct CameraPlugin;
84
85impl Plugin for CameraPlugin {
86    fn build(&self, app: &mut App) {
87        app.add_systems(PostStartup, center)
88            .add_systems(PostUpdate, cameraman);
89    }
90}
91
92fn center(
93    mut query_camera: Query<(&mut Transform, &mut Cameraman), Without<Target>>,
94    query_targets: Query<(&Transform, Entity), With<Target>>,
95) {
96    for (mut camera_transform, mut camera) in &mut query_camera {
97        for (target_transform, target_entity) in &query_targets {
98            if camera.target != target_entity {
99                continue;
100            }
101
102            // TODO: for now we follow the first target but we could think of doing an average positions of all the targets
103            if camera.target == target_entity {
104                camera_transform.translation.x = target_transform.translation.x;
105                camera_transform.translation.y = target_transform.translation.y;
106                camera.target_prev_translation = target_transform.translation;
107                camera.look_at = target_transform.translation;
108                break;
109            }
110        }
111    }
112}
113
114fn cameraman(
115    mut query_camera: Query<(&mut Transform, &mut Cameraman), Without<Target>>,
116    query_targets: Query<(&Transform, Entity), With<Target>>,
117    time: Res<Time>,
118) {
119    for (mut camera_transform, mut camera) in &mut query_camera {
120        for (target_transform, target_entity) in &query_targets {
121            if camera.target != target_entity {
122                continue;
123            }
124
125            // process target velocity
126            let mut target_velocity = target_transform.translation - camera.target_prev_translation;
127            let target_moving = target_velocity != Vec3::ZERO;
128            if target_moving {
129                target_velocity /= time.delta_seconds();
130            }
131            camera.look_at = target_transform.translation + (target_velocity * camera.ahead_factor);
132            camera.target_prev_translation = target_transform.translation;
133
134            // process dead zone
135            let diff_pos_abs = (target_transform.translation - camera_transform.translation)
136                .truncate()
137                .abs();
138            let dead_zone =
139                diff_pos_abs.x <= camera.dead_zone.x && diff_pos_abs.y <= camera.dead_zone.y;
140            let centered = diff_pos_abs.x < 3.0 && diff_pos_abs.y < 3.0;
141
142            // center after some time in the dead zone
143            if dead_zone && !centered && !target_moving && !camera.traveling {
144                camera.center_after.tick(time.delta());
145            } else {
146                camera.center_after.reset();
147            }
148
149            // triggers travelling if we are out of dead zone
150            if !dead_zone {
151                camera.traveling = true;
152            }
153
154            // once the camera is moving we keep moving until we reach the center
155            if camera.traveling || camera.center_after.finished() {
156                let next_pos = camera.look_at - camera_transform.translation;
157                camera_transform.translation.x += next_pos.x * 0.02;
158                camera_transform.translation.y += next_pos.y * 0.02;
159
160                // we arrived
161                if centered && !target_moving {
162                    camera.traveling = false;
163                }
164            }
165
166            // if the target is in the dead zone, do nothing on camera
167        }
168    }
169}
170
171pub struct CameraDebugPlugin;
172
173impl Plugin for CameraDebugPlugin {
174    fn build(&self, app: &mut App) {
175        app.add_systems(PostStartup, setup_debug)
176            .add_systems(Update, debug);
177    }
178}
179
180#[derive(Component)]
181pub struct CameraDebug(Entity);
182
183fn setup_debug(
184    mut commands: Commands,
185    mut meshes: ResMut<Assets<Mesh>>,
186    mut materials: ResMut<Assets<ColorMaterial>>,
187    query_cameras: Query<(&Transform, &Cameraman, Entity)>,
188) {
189    for (camera_transform, _camera, entity) in &query_cameras {
190        commands.spawn((
191            MaterialMesh2dBundle {
192                mesh: meshes.add(shape::Circle::new(2.).into()).into(),
193                material: materials.add(ColorMaterial::from(Color::GREEN)),
194                transform: Transform::from_translation(Vec3::new(
195                    camera_transform.translation.x,
196                    camera_transform.translation.y,
197                    100.0,
198                )),
199                ..default()
200            },
201            CameraDebug(entity),
202        ));
203    }
204}
205
206#[allow(clippy::type_complexity)] // Because of query_targets
207fn debug(
208    mut gizmos: Gizmos,
209    mut config: ResMut<GizmoConfig>,
210    query_cameras: Query<(&Transform, &Cameraman, Entity)>,
211    query_targets: Query<(&Transform, Entity), (With<Target>, Without<CameraDebug>)>,
212    mut query_camera_debug: Query<(&mut Transform, &CameraDebug), Without<Cameraman>>,
213) {
214    config.line_width = 1.0;
215
216    // TODO: Unspawn camera debug object if camera do not exist anymore
217
218    for (camera_transform, camera, entity) in &query_cameras {
219        gizmos.rect_2d(
220            camera_transform.translation.truncate(),
221            0.,
222            camera.dead_zone * 2.0,
223            Color::RED,
224        );
225
226        for (target_transform, target_entity) in &query_targets {
227            if camera.target != target_entity {
228                continue;
229            }
230
231            gizmos.line_2d(
232                target_transform.translation.truncate(),
233                camera.look_at.truncate(),
234                Color::GREEN,
235            );
236        }
237
238        for (mut transform, camera_debug) in &mut query_camera_debug {
239            if camera_debug.0 != entity {
240                continue;
241            }
242            transform.translation.x = camera.look_at.x;
243            transform.translation.y = camera.look_at.y;
244        }
245    }
246}