lunar_render/
camera_follow.rs1use bevy_ecs::prelude::*;
24use lunar_math::{Rect, Vec2, WorldTransform};
25
26use crate::Camera;
27
28#[derive(Resource)]
33pub struct CameraFollow2d {
34 pub target: Entity,
36 pub lead: Vec2,
39 pub deadzone: Vec2,
43 pub bounds: Option<Rect>,
46 pub lerp_speed: f32,
48}
49
50pub(crate) fn camera_follow_system(
51 follow: Option<Res<CameraFollow2d>>,
52 mut camera: Option<ResMut<Camera>>,
53 transforms: Query<&WorldTransform>,
54 time: Res<lunar_core::Time>,
55) {
56 let (Some(follow), Some(camera)) = (follow, camera.as_mut()) else {
57 return;
58 };
59 let Ok(target_transform) = transforms.get(follow.target) else {
60 return;
61 };
62
63 let desired = target_transform.translation + follow.lead;
64 let delta = desired - camera.position;
65
66 if delta.x.abs() <= follow.deadzone.x && delta.y.abs() <= follow.deadzone.y {
68 return;
69 }
70
71 let new_position = if follow.lerp_speed <= 0.0 {
72 desired
73 } else {
74 let t = (follow.lerp_speed * time.delta_seconds()).min(1.0);
75 camera.position + delta * t
76 };
77
78 camera.position = match follow.bounds {
79 None => new_position,
80 Some(bounds) => Vec2::new(
81 new_position.x.clamp(bounds.x, bounds.x + bounds.w),
82 new_position.y.clamp(bounds.y, bounds.y + bounds.h),
83 ),
84 };
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use lunar_core::Time;
91
92 fn make_world_with_target(position: Vec2) -> (World, Entity) {
93 let mut world = World::new();
94 let entity = world
95 .spawn(WorldTransform::from_xy(position.x, position.y))
96 .id();
97 world.insert_resource(Time::default());
98 (world, entity)
99 }
100
101 #[test]
102 fn snaps_to_target_with_zero_lerp() {
103 let (mut world, target) = make_world_with_target(Vec2::new(200.0, 100.0));
104 world.insert_resource(Camera::new());
105 world.insert_resource(CameraFollow2d {
106 target,
107 lead: Vec2::ZERO,
108 deadzone: Vec2::ZERO,
109 bounds: None,
110 lerp_speed: 0.0,
111 });
112
113 let mut system = IntoSystem::into_system(camera_follow_system);
114 system.initialize(&mut world);
115 let _ = system.run((), &mut world);
116
117 let camera = world.resource::<Camera>();
118 assert!((camera.position.x - 200.0).abs() < 0.001);
119 assert!((camera.position.y - 100.0).abs() < 0.001);
120 }
121
122 #[test]
123 fn deadzone_prevents_movement() {
124 let (mut world, target) = make_world_with_target(Vec2::new(10.0, 5.0));
125 world.insert_resource(Camera::new()); world.insert_resource(CameraFollow2d {
127 target,
128 lead: Vec2::ZERO,
129 deadzone: Vec2::new(50.0, 50.0),
130 bounds: None,
131 lerp_speed: 0.0,
132 });
133
134 let mut system = IntoSystem::into_system(camera_follow_system);
135 system.initialize(&mut world);
136 let _ = system.run((), &mut world);
137
138 let camera = world.resource::<Camera>();
139 assert!((camera.position.x - 0.0).abs() < 0.001);
141 assert!((camera.position.y - 0.0).abs() < 0.001);
142 }
143
144 #[test]
145 fn bounds_clamp_camera_position() {
146 let (mut world, target) = make_world_with_target(Vec2::new(9999.0, 9999.0));
147 world.insert_resource(Camera::new());
148 world.insert_resource(CameraFollow2d {
149 target,
150 lead: Vec2::ZERO,
151 deadzone: Vec2::ZERO,
152 bounds: Some(Rect::new(0.0, 0.0, 800.0, 600.0)),
153 lerp_speed: 0.0,
154 });
155
156 let mut system = IntoSystem::into_system(camera_follow_system);
157 system.initialize(&mut world);
158 let _ = system.run((), &mut world);
159
160 let camera = world.resource::<Camera>();
161 assert!((camera.position.x - 800.0).abs() < 0.001);
162 assert!((camera.position.y - 600.0).abs() < 0.001);
163 }
164
165 #[test]
166 fn lead_offset_applied() {
167 let (mut world, target) = make_world_with_target(Vec2::new(100.0, 100.0));
168 world.insert_resource(Camera::new());
169 world.insert_resource(CameraFollow2d {
170 target,
171 lead: Vec2::new(50.0, 0.0),
172 deadzone: Vec2::ZERO,
173 bounds: None,
174 lerp_speed: 0.0,
175 });
176
177 let mut system = IntoSystem::into_system(camera_follow_system);
178 system.initialize(&mut world);
179 let _ = system.run((), &mut world);
180
181 let camera = world.resource::<Camera>();
182 assert!((camera.position.x - 150.0).abs() < 0.001);
184 assert!((camera.position.y - 100.0).abs() < 0.001);
185 }
186}