rantz_camera2d 1.1.2

A 2D camera plugin for Bevy, inspired by the Love2D camera plugin - STALKER-X
Documentation
use crate::prelude::*;
use bevy::prelude::*;

use rantz_spatial2d::prelude::*;

pub fn camera_follow_target(
    mut camera_query: Query<
        (
            &mut Position2D,
            &CameraStyle,
            &CameraLerp,
            &mut CameraLead,
            &CameraTarget,
            &OrthographicProjection,
        ),
        (With<Camera>, With<Camera2d>),
    >,
    all_positions_query: Query<&Position2D, Without<Camera>>,
    time: Res<Time>,
) {
    for (mut camera_position, style, lp, mut ld, camera_target, ortho) in camera_query.iter_mut() {
        if camera_target.0.is_none() {
            return;
        }

        if !all_positions_query.contains(camera_target.0.unwrap()) {
            return;
        }

        match style {
            CameraStyle::DeadZone(deadzone) => {
                handle_deadzone(
                    all_positions_query.get(camera_target.0.unwrap()).unwrap(),
                    &mut camera_position,
                    deadzone,
                    &mut ld,
                    lp,
                    time.delta_seconds(),
                );
            }
            CameraStyle::ScreenByScreen => {
                handle_screen_by_screen(
                    ortho,
                    all_positions_query.get(camera_target.0.unwrap()).unwrap(),
                    &mut camera_position,
                    &lp.factor,
                    time.delta_seconds(),
                );
            }
            CameraStyle::Exact => {
                handle_exact(
                    &mut camera_position,
                    all_positions_query.get(camera_target.0.unwrap()).unwrap(),
                );
            }
        }
    }
}

fn handle_exact(camera_position: &mut Position2D, target_pos: &Position2D) {
    *camera_position = *target_pos;
}

fn handle_deadzone(
    target_pos: &Position2D,
    cam_pos: &mut Position2D,
    deadzone: &DeadZone,
    ld: &mut CameraLead,
    lp: &CameraLerp,
    dt: f32,
) {
    let mut scroll_dist = calculate_scroll_distance_for_deadzone(deadzone, target_pos, cam_pos);

    // Apply lead
    if ld.last_target_position.is_none() {
        ld.last_target_position = Some(*target_pos);
    }
    let last_target = ld.last_target_position.unwrap();
    let dir_of_target_movement = Vec2::from(target_pos - last_target).normalize_or_zero();
    let lead_vec = dir_of_target_movement * ld.lead_amount;
    scroll_dist += lead_vec;
    ld.last_target_position = Some(*target_pos);

    // Apply lerp
    let new_pos = Position2D::new(
        lerp(cam_pos.x + scroll_dist.x, cam_pos.x, lp.factor.x * dt),
        lerp(cam_pos.y + scroll_dist.y, cam_pos.y, lp.factor.y * dt),
    );

    *cam_pos = new_pos;
}

fn handle_screen_by_screen(
    ortho: &OrthographicProjection,
    target_pos: &Position2D,
    camera_position: &mut Position2D,
    lerp_factor: &Vec2,
    dt: f32,
) {
    let scaled_bounds = calculate_scaled_screenbounds(ortho);
    let cam_pos = *camera_position;
    let sw = scaled_bounds.x;
    let sh = scaled_bounds.y;
    let hw = sw / 2.;
    let hh = sh / 2.;
    let scroll_dist = distance_to_target_with_screenbounds(
        target_pos,
        cam_pos,
        distance_to_next_screenbounds(cam_pos, sw, sh),
        hw,
        sw,
        hh,
        sh,
    );

    // Apply lerp
    let new_pos = Position2D::new(
        lerp(cam_pos.x + scroll_dist.x, cam_pos.x, lerp_factor.x * dt),
        lerp(cam_pos.y + scroll_dist.y, cam_pos.y, lerp_factor.y * dt),
    );

    *camera_position = new_pos;
}

fn calculate_scaled_screenbounds(ortho: &OrthographicProjection) -> Vec2 {
    ortho.scale * ortho.area.size()
}

fn distance_to_target_with_screenbounds(
    target_pos: &Position2D,
    cam_pos: Position2D,
    initial_scroll_dist: Vec2,
    hw: f32,
    sw: f32,
    hh: f32,
    sh: f32,
) -> Vec2 {
    let mut scroll_dist = initial_scroll_dist;
    if target_pos.x < (cam_pos.x + scroll_dist.x) - hw {
        scroll_dist.x -= sw;
    }
    if target_pos.x > (cam_pos.x + scroll_dist.x) + hw {
        scroll_dist.x += sw;
    }

    if target_pos.y < (cam_pos.y + scroll_dist.y) - hh {
        scroll_dist.y -= sh;
    }
    if target_pos.y > (cam_pos.y + scroll_dist.y) + hh {
        scroll_dist.y += sh;
    }

    scroll_dist
}

fn distance_to_next_screenbounds(cam_pos: Position2D, sw: f32, sh: f32) -> Vec2 {
    let mut scroll_dist = Vec2::new(0.0, 0.0);
    let mod_x = cam_pos.x % sw;
    let mod_y = cam_pos.y % sh;
    if mod_x != 0.0 {
        scroll_dist.x = -sw - mod_x;
    }

    if mod_x > 0.0 {
        scroll_dist.x = sw - mod_x;
    }

    if mod_y < 0.0 {
        scroll_dist.y = -sh - mod_y;
    }

    if mod_y > 0.0 {
        scroll_dist.y = sh - mod_y;
    }

    scroll_dist
}

fn lerp(target: f32, current: f32, factor: f32) -> f32 {
    current + (target - current) * factor
}

fn calculate_scroll_distance_for_deadzone(
    deadzone: &DeadZone,
    target_pos: &Position2D,
    cam_pos: &Position2D,
) -> Vec2 {
    let hw = deadzone.half_width();
    let hh = deadzone.half_height();
    let mut scroll_dist = Vec2::new(0.0, 0.0);

    if target_pos.x < cam_pos.x - hw {
        scroll_dist.x = target_pos.x - (cam_pos.x - hw);
    }
    if target_pos.x > cam_pos.x + hw {
        scroll_dist.x = target_pos.x - (cam_pos.x + hw);
    }

    if target_pos.y < cam_pos.y - hh {
        scroll_dist.y = target_pos.y - (cam_pos.y - hh);
    }
    if target_pos.y > cam_pos.y + hh {
        scroll_dist.y = target_pos.y - (cam_pos.y + hh);
    }

    scroll_dist
}