big_space 0.12.0

A floating origin plugin for bevy
Documentation
//! Provides a camera controller compatible with the floating origin plugin.

use crate::prelude::*;
use bevy_app::prelude::*;
use bevy_camera::{
    primitives::Aabb,
    visibility::{InheritedVisibility, RenderLayers},
};
use bevy_ecs::entity::EntityHashSet;
use bevy_ecs::prelude::*;
use bevy_input::{mouse::MouseMotion, prelude::*};
use bevy_math::{prelude::*, DQuat, DVec3};
use bevy_platform::prelude::*;
use bevy_reflect::prelude::*;
use bevy_time::prelude::*;
use bevy_transform::{prelude::*, TransformSystems};

/// Runs the [`big_space`](crate) [`BigSpaceCameraController`].
pub struct BigSpaceCameraControllerPlugin;
impl Plugin for BigSpaceCameraControllerPlugin {
    fn build(&self, app: &mut App) {
        app.register_type::<BigSpaceCameraController>()
            .register_type::<BigSpaceCameraInput>()
            .init_resource::<BigSpaceCameraInput>()
            .add_systems(
                PostUpdate,
                (
                    default_camera_inputs
                        .before(camera_controller)
                        .run_if(|input: Res<BigSpaceCameraInput>| !input.defaults_disabled),
                    nearest_objects_in_grid.before(camera_controller),
                    camera_controller.before(TransformSystems::Propagate),
                ),
            );
    }
}

/// A simple fly-cam camera controller.
///
/// Add to a camera to enable the built-in [`big_space`](crate) camera controller.
#[derive(Clone, Debug, Reflect, Component)]
#[reflect(Component)]
pub struct BigSpaceCameraController {
    /// Smoothness of translation, from `0.0` to `1.0`.
    pub smoothness: f64,
    /// Rotational smoothness, from `0.0` to `1.0`.
    pub rotational_smoothness: f64,
    /// Base speed.
    pub speed: f64,
    /// Rotational yaw speed multiplier.
    pub speed_yaw: f64,
    /// Rotational pitch speed multiplier.
    pub speed_pitch: f64,
    /// Rotational roll speed multiplier.
    pub speed_roll: f64,
    /// Minimum and maximum speed.
    pub speed_bounds: [f64; 2],
    /// Whether the camera should slow down when approaching an entity's [`Aabb`].
    pub slow_near_objects: bool,
    nearest_object: Option<(Entity, f64)>,
    vel_translation: DVec3,
    vel_rotation: DQuat,
}

impl BigSpaceCameraController {
    /// Sets the `smoothness` parameter of the controller, and returns the modified result.
    pub fn with_smoothness(mut self, translation: f64, rotation: f64) -> Self {
        self.smoothness = translation;
        self.rotational_smoothness = rotation;
        self
    }

    /// Sets the `slow_near_objects` parameter of the controller, and returns the modified result.
    pub fn with_slowing(mut self, slow_near_objects: bool) -> Self {
        self.slow_near_objects = slow_near_objects;
        self
    }

    /// Sets the speed of the controller, and returns the modified result.
    pub fn with_speed(mut self, speed: f64) -> Self {
        self.speed = speed;
        self
    }

    /// Sets the yaw angular velocity of the controller, and returns the modified result.
    pub fn with_speed_yaw(mut self, speed: f64) -> Self {
        self.speed_yaw = speed;
        self
    }

    /// Sets the pitch angular velocity of the controller, and returns the modified result.
    pub fn with_speed_pitch(mut self, speed: f64) -> Self {
        self.speed_pitch = speed;
        self
    }

    /// Sets the pitch angular velocity of the controller, and returns the modified result.
    pub fn with_speed_roll(mut self, speed: f64) -> Self {
        self.speed_roll = speed;
        self
    }

    /// Sets the speed of the controller, and returns the modified result.
    pub fn with_speed_bounds(mut self, speed_limits: [f64; 2]) -> Self {
        self.speed_bounds = speed_limits;
        self
    }

    /// Returns the translational and rotational velocity of the camera.
    pub fn velocity(&self) -> (DVec3, DQuat) {
        (self.vel_translation, self.vel_rotation)
    }

    /// Returns the object nearest the camera, and its distance.
    pub fn nearest_object(&self) -> Option<(Entity, f64)> {
        self.nearest_object
    }
}

impl Default for BigSpaceCameraController {
    fn default() -> Self {
        Self {
            smoothness: 0.85,
            rotational_smoothness: 0.8,
            speed: 1.0,
            speed_pitch: 2.0,
            speed_yaw: 2.0,
            speed_roll: 1.0,
            speed_bounds: [1e-17, 1e30],
            slow_near_objects: true,
            nearest_object: None,
            vel_translation: DVec3::ZERO,
            vel_rotation: DQuat::IDENTITY,
        }
    }
}

/// `ButtonInput` state used to command [`BigSpaceCameraController`] motion. Reset every time the values
/// are read to update the camera. Allows you to map any input to camera motions. Uses aircraft
/// principle axes conventions.
#[derive(Clone, Debug, Default, Reflect, Resource)]
#[reflect(Resource)]
pub struct BigSpaceCameraInput {
    /// When disabled, the camera input system is not run.
    pub defaults_disabled: bool,
    /// Z-negative
    pub forward: f64,
    /// Y-positive
    pub up: f64,
    /// X-positive
    pub right: f64,
    /// Positive = right wing down
    pub roll: f64,
    /// Positive = nose up
    pub pitch: f64,
    /// Positive = nose right
    pub yaw: f64,
    /// Modifier to increase speed, e.g. "sprint"
    pub boost: bool,
}

impl BigSpaceCameraInput {
    /// Reset the controller back to zero to ready for the next grid.
    pub fn reset(&mut self) {
        *self = BigSpaceCameraInput {
            defaults_disabled: self.defaults_disabled,
            ..Default::default()
        };
    }

    /// Returns the desired velocity transform.
    pub fn target_velocity(
        &self,
        controller: &BigSpaceCameraController,
        speed: f64,
        dt: f64,
    ) -> (DVec3, DQuat) {
        let rotation = DQuat::from_euler(
            EulerRot::XYZ,
            self.pitch * dt * controller.speed_pitch,
            self.yaw * dt * controller.speed_yaw,
            self.roll * dt * controller.speed_roll,
        );

        let translation = DVec3::new(self.right, self.up, self.forward) * speed * dt;

        (translation, rotation)
    }
}

/// Provides sensible keyboard and mouse input defaults
pub fn default_camera_inputs(
    keyboard: Res<ButtonInput<KeyCode>>,
    mut mouse_move: MessageReader<MouseMotion>,
    mut cam: ResMut<BigSpaceCameraInput>,
) {
    keyboard.pressed(KeyCode::KeyW).then(|| cam.forward -= 1.0);
    keyboard.pressed(KeyCode::KeyS).then(|| cam.forward += 1.0);
    keyboard.pressed(KeyCode::KeyA).then(|| cam.right -= 1.0);
    keyboard.pressed(KeyCode::KeyD).then(|| cam.right += 1.0);
    keyboard.pressed(KeyCode::Space).then(|| cam.up += 1.0);
    keyboard
        .pressed(KeyCode::ControlLeft)
        .then(|| cam.up -= 1.0);
    keyboard.pressed(KeyCode::KeyQ).then(|| cam.roll += 2.0);
    keyboard.pressed(KeyCode::KeyE).then(|| cam.roll -= 2.0);
    keyboard
        .pressed(KeyCode::ShiftLeft)
        .then(|| cam.boost = true);
    if let Some(total_mouse_motion) = mouse_move.read().map(|e| e.delta).reduce(|sum, i| sum + i) {
        cam.pitch += total_mouse_motion.y as f64 * -0.1;
        cam.yaw += total_mouse_motion.x as f64 * -0.1;
    }
}

/// Find the object nearest the camera, within the same grid as the camera.
pub fn nearest_objects_in_grid(
    objects: Query<(
        Entity,
        &Transform,
        &GlobalTransform,
        &Aabb,
        Option<&RenderLayers>,
        &InheritedVisibility,
    )>,
    mut camera: Query<(
        Entity,
        &mut BigSpaceCameraController,
        &GlobalTransform,
        Option<&RenderLayers>,
    )>,
    children: Query<&Children>,
) {
    let Ok((cam_entity, mut camera, cam_pos, cam_layer)) = camera.single_mut() else {
        return;
    };
    if !camera.slow_near_objects {
        return;
    }
    let cam_layer = cam_layer.to_owned().unwrap_or_default();
    let cam_children: EntityHashSet = children.iter_descendants(cam_entity).collect();

    let nearest_object = objects
        .iter()
        .filter(|(entity, ..)| !cam_children.contains(entity))
        .filter(|(.., obj_layer, _)| {
            let obj_layer = obj_layer.unwrap_or_default();
            cam_layer.intersects(obj_layer)
        })
        .filter(|(.., visibility)| visibility.get())
        .map(|(entity, object_local, obj_pos, aabb, ..)| {
            let center_distance =
                obj_pos.translation().as_dvec3() - cam_pos.translation().as_dvec3();
            let nearest_distance = center_distance.length()
                - (aabb.half_extents.as_dvec3() * object_local.scale.as_dvec3())
                    .abs()
                    .min_element();
            (entity, nearest_distance)
        })
        .filter(|v| v.1.is_finite())
        .reduce(|nearest, this| if this.1 < nearest.1 { this } else { nearest });
    camera.nearest_object = nearest_object;
}

/// Uses [`BigSpaceCameraInput`] state to update the camera position.
pub fn camera_controller(
    time: Res<Time>,
    grids: Grids,
    mut input: ResMut<BigSpaceCameraInput>,
    mut camera: Query<(
        Entity,
        &mut CellCoord,
        &mut Transform,
        &mut BigSpaceCameraController,
    )>,
) {
    for (camera, mut cell, mut transform, mut controller) in camera.iter_mut() {
        let Some(grid) = grids.parent_grid(camera) else {
            continue;
        };
        let speed = match (controller.nearest_object, controller.slow_near_objects) {
            (Some(nearest), true) => nearest.1.abs(),
            _ => controller.speed,
        } * (controller.speed + input.boost as usize as f64);

        let [min, max] = controller.speed_bounds;
        let speed = speed.clamp(min, max);

        let lerp_translation = 1.0 - controller.smoothness.clamp(0.0, 0.999);
        let lerp_rotation = 1.0 - controller.rotational_smoothness.clamp(0.0, 0.999);

        let (vel_t_current, vel_r_current) = (controller.vel_translation, controller.vel_rotation);
        let (vel_t_target, vel_r_target) =
            input.target_velocity(&controller, speed, time.delta_secs_f64());

        let cam_rot = transform.rotation.as_dquat();
        let vel_t_next = cam_rot * vel_t_target; // Orients the translation to match the camera
        let vel_t_next = vel_t_current.lerp(vel_t_next, lerp_translation);
        // Convert the high precision translation to a grid cell and low precision translation
        let (cell_offset, new_translation) = grid.translation_to_grid(vel_t_next);
        let new = *cell.bypass_change_detection() + cell_offset;
        cell.set_if_neq(new);
        transform.translation += new_translation;

        let new_rotation = vel_r_current.slerp(vel_r_target, lerp_rotation);
        transform.rotation *= new_rotation.as_quat();

        // Store the new velocity to be used in the next grid
        controller.vel_translation = vel_t_next;
        controller.vel_rotation = new_rotation;

        input.reset();
    }
}