use bevy::render::mesh::MeshAabb; use bevy::render::primitives::Aabb; use super::*; pub struct ParallaxPlugin; impl Plugin for ParallaxPlugin { fn build(&self, app: &mut App) { app.add_systems( Update, ( move_parallax_items.run_if(any_component_changed::), wrap_parallax_items.run_if(any_component_changed::), ), ); } } /// /// ParallaxDepth describes how far from the camera something is /// /// A parallax depth of 1 means it moves 1:1 with camera movement; /// If the camera moves 1 pixel to the left, the background moves 1px to the right /// /// A parallax depth of 2 means the movement is 1:2 /// Camera moves 2px, the element moves 1px /// #[derive(Component)] pub struct ParallaxDepth(pub f32); fn move_parallax_items( mut q: Query<(&mut Transform, &ParallaxDepth), Without>, camera: Single<&Transform, (With, Changed)>, mut prev_camera_pos: Local, ) { // Unpack the camera data let cam_t = *camera; // Calculate how far the camera moved since the last update let delta = cam_t.translation.truncate() - *prev_camera_pos; debug!("Cam Delta: {:?}", delta); // For each object q.iter_mut().for_each(|(mut t, pd)| { // Update ParallaxPosition let depth_movement = delta / pd.0; debug!("Depth: {:?} | Move: {:?}", pd.0, depth_movement); // Update actual position t.translation.x -= depth_movement.x; t.translation.y -= depth_movement.y; }); *prev_camera_pos = cam_t.translation.truncate(); } fn wrap_parallax_items( mut items: Query< (&mut Transform, &GlobalTransform, &ViewVisibility, &Mesh2d), ( With, Without, Changed, ), >, camera: Single<(&Camera, &GlobalTransform), With>, window: Single<&Window>, meshes: Res>, ) { if !items.is_empty() { let (cam, cam_gt) = *camera; // get the window size in world space let window_size_in_world_space = { let top_left = cam.viewport_to_world_2d(cam_gt, Vec2::ZERO).unwrap(); let bottom_right = cam.viewport_to_world_2d(cam_gt, window.size()).unwrap(); Vec2::abs(Vec2::new( bottom_right.x - top_left.x, bottom_right.y - top_left.y, )) }; // for each item in the paralax items items.iter_mut().for_each(|(mut t, gt, v, Mesh2d(m))| { if !v.get() { debug!("Item is not visible"); // Get the total size (window + scale) let half_extents = { let Aabb { half_extents, .. } = meshes.get(m).unwrap().compute_aabb().unwrap(); half_extents.truncate() }; let object_size = t.scale.truncate() * 2.0; let total_size = window_size_in_world_space + (object_size * half_extents); debug!("Sizes:\n\twindow {window_size_in_world_space}\n\tobject size: {object_size}\n\tAabb Half extents: {half_extents}\n\tTotal size: {total_size}"); // Double check that item is out of bounds let bounds_check = Vec2::abs(cam_gt.translation().truncate() - gt.translation().truncate()); let out_of_bounds = (total_size / 2.0) - bounds_check; debug!("Bounds check {bounds_check} | Out of bounds: {out_of_bounds:?}"); debug!("Starting position: {:?}", t.translation); if out_of_bounds.x < 0.0 { debug!("Object is actually out of bounds horizontally"); // determine if should move to the left or right relative to camera let move_right = cam_gt.translation().x > gt.translation().x; // move left or right if move_right { t.translation.x += total_size.x; } else { t.translation.x -= total_size.x; } } if out_of_bounds.y < 0.0 { debug!("Object is actually out of bounds vertically"); let move_up = cam_gt.translation().y > gt.translation().y; // move up or down if move_up { t.translation.y += total_size.y; } else { t.translation.y -= total_size.y; } } debug!("Moved to {:?}", t.translation); } }); } }