codecraft 0.1.1

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, an immediate-mode UI, audio and gamepad haptics
Documentation
//! The camera that rides behind something and looks over its shoulder.
//!
//! What a third-person game does: the eye sits back and above whatever is
//! being driven, aimed a little over its head rather than at its feet, so the
//! ground it is about to cross is the part of the screen you are looking at.
//!
//! Apart from [`super::OrbitCamera`] because the two answer different
//! questions. An orbit rig is for *looking at* a scene and belongs to whoever
//! is working on it -- dev mode. A follow rig is for *being* something in one,
//! and belongs to a player.
//!
//! ```no_run
//! # use codecraft::{AppState, ecs::Entity, sceneobjects::cameras::Follow};
//! # fn demo(app: &mut AppState, tank: Entity) {
//! app.spawn_entity(Follow::behind(tank));
//! # }
//! ```
use glam::{Quat, Vec3};

use super::Camera;
use crate::ecs::{Component, Entity};

/// How far back the eye sits, and how far up, in world units.
///
/// Back and up rather than a distance and an angle: what matters is that the
/// thing being driven sits low in the frame with the ground ahead of it
/// visible, and those are the two numbers that say so directly.
const BEHIND: f32 = 7.0;
const ABOVE: f32 = 3.2;

/// How far above the target the camera aims.
///
/// Not at its feet: aiming at the ground under something puts it in the middle
/// of the screen with the sky above it, and the half of the picture worth
/// having is the ground in front.
const AIM_ABOVE: f32 = 1.0;

/// How quickly the rig catches up, as the fraction of the remaining distance
/// it closes each second.
///
/// Lagging on purpose. A camera welded to a moving thing makes the *world*
/// appear to move instead, which reads as the ground sliding about; letting it
/// trail means the thing being driven moves within the frame, which is what
/// tells you how fast it is going.
const CATCH_UP: f32 = 8.0;

/// A camera that follows something around.
///
/// A component rather than a setting on the camera, because what it follows is
/// an entity: the rig outlives any one frame's answer and a scene should not
/// have to recompute an eye position every update.
#[derive(Component, Clone, Copy, Debug, PartialEq)]
pub struct Follow {
    /// What it is riding behind.
    pub target: Entity,
    /// Which way round the target it sits, in radians. Turning this is what
    /// looking around does.
    pub yaw: f32,
    /// How far back and how far up.
    pub behind: f32,
    pub above: f32,
    /// How far over the target's own middle it aims.
    pub aim_above: f32,
    /// How much of the remaining gap it closes each second; see [`CATCH_UP`].
    /// Zero welds it to the target.
    pub catch_up: f32,
    /// Where the eye actually is, which trails where it is going. `None`
    /// until the first frame, which is what makes the first one snap rather
    /// than fly in from the origin.
    settled: Option<Vec3>,
}

impl Follow {
    /// Behind `target`, at the usual distance, looking the way it faces.
    pub fn behind(target: Entity) -> Self {
        Self {
            target,
            yaw: 0.0,
            behind: BEHIND,
            above: ABOVE,
            aim_above: AIM_ABOVE,
            catch_up: CATCH_UP,
            settled: None,
        }
    }

    /// Which way round the target the eye sits.
    pub fn yaw(mut self, radians: f32) -> Self {
        self.yaw = radians;
        self
    }

    /// How far back and how far up, in world units.
    pub fn at(mut self, behind: f32, above: f32) -> Self {
        self.behind = behind;
        self.above = above;
        self
    }

    /// Where the eye is going, given where the target is now.
    pub fn wants(&self, target: Vec3) -> Vec3 {
        // Yaw 0 puts the eye on +Z, behind something facing -Z, which is the
        // way a model exported facing forward looks.
        let back = Quat::from_rotation_y(self.yaw) * Vec3::Z;
        target + back * self.behind + Vec3::Y * self.above
    }

    /// What it is looking at: a little over the target's middle.
    pub fn aim(&self, target: Vec3) -> Vec3 {
        target + Vec3::Y * self.aim_above
    }

    /// Moves the eye towards where it is going and answers with the camera.
    ///
    /// Exponential rather than linear, so it closes fast when it is far behind
    /// and settles without overshooting -- and framerate-independent, which
    /// linear catching-up is not.
    pub fn follow(&mut self, target: Vec3, delta: f32) -> Camera {
        let wants = self.wants(target);
        let eye = match self.settled {
            // The first frame: there is nowhere to come from, so it starts
            // where it belongs rather than flying in from the origin.
            None => wants,
            Some(_) if self.catch_up <= 0.0 => wants,
            Some(settled) => {
                let caught = 1.0 - (-self.catch_up * delta.max(0.0)).exp();
                settled + (wants - settled) * caught
            }
        };
        self.settled = Some(eye);
        Camera::looking_at(eye, self.aim(target))
    }

    /// Where the eye is right now, before any more following.
    pub fn eye(&self) -> Option<Vec3> {
        self.settled
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn rig() -> Follow {
        Follow::behind(Entity::from_raw_u32(1).unwrap())
    }

    /// Behind and above, which is the whole point of the thing.
    #[test]
    fn the_eye_sits_back_and_up_from_what_it_follows() {
        let follow = rig();
        let at = Vec3::new(4.0, 0.5, -2.0);
        let eye = follow.wants(at);

        assert!(eye.y > at.y, "above it");
        assert!(
            (eye - at).with_y(0.0).length() > 1.0,
            "and back from it: {eye:?} against {at:?}",
        );
    }

    /// It aims over the target, not at it: the half of the picture worth
    /// having is the ground in front.
    #[test]
    fn it_aims_over_the_target_rather_than_at_it() {
        let follow = rig();
        let at = Vec3::new(0.0, 0.5, 0.0);
        assert!(follow.aim(at).y > at.y);
    }

    /// Turning the rig walks the eye round the target without moving it away.
    #[test]
    fn yaw_walks_the_eye_around_at_the_same_range() {
        let at = Vec3::new(1.0, 0.0, 1.0);
        let straight = rig().wants(at);
        let turned = rig().yaw(std::f32::consts::FRAC_PI_2).wants(at);

        assert!(
            (straight - turned).length() > 1.0,
            "a quarter turn should move it a long way",
        );
        let range = |eye: Vec3| (eye - at).length();
        assert!(
            (range(straight) - range(turned)).abs() < 1e-4,
            "but not further away: {} against {}",
            range(straight),
            range(turned),
        );
    }

    /// The first frame has nowhere to come from, so it starts where it
    /// belongs rather than flying in from the origin.
    #[test]
    fn the_first_frame_snaps_into_place() {
        let mut follow = rig();
        let at = Vec3::new(10.0, 0.0, 10.0);
        let camera = follow.follow(at, 1.0 / 60.0);

        assert_eq!(camera.eye, follow.wants(at));
    }

    /// After that it trails, which is what makes the thing being driven move
    /// within the frame rather than the world move around it.
    #[test]
    fn it_trails_a_target_that_moves() {
        let mut follow = rig();
        follow.follow(Vec3::ZERO, 1.0 / 60.0);
        let camera = follow.follow(Vec3::new(0.0, 0.0, -10.0), 1.0 / 60.0);

        let wants = follow.wants(Vec3::new(0.0, 0.0, -10.0));
        assert_ne!(camera.eye, wants, "it should not be there yet");
        assert!(
            (camera.eye - wants).length() < 10.0,
            "but should have set off: {:?} against {wants:?}",
            camera.eye,
        );
    }

    /// And gets there if the target stops.
    #[test]
    fn it_catches_up_once_the_target_stops() {
        let mut follow = rig();
        follow.follow(Vec3::ZERO, 1.0 / 60.0);
        let at = Vec3::new(0.0, 0.0, -10.0);
        for _ in 0..240 {
            follow.follow(at, 1.0 / 60.0);
        }

        assert!(
            (follow.eye().unwrap() - follow.wants(at)).length() < 0.01,
            "four seconds is long enough to settle",
        );
    }

    /// How far it gets in a second cannot depend on how many frames that
    /// second was cut into, or the camera lags differently on a slow machine.
    #[test]
    fn catching_up_does_not_depend_on_the_framerate() {
        let at = Vec3::new(0.0, 0.0, -10.0);
        let run = |steps: u32| {
            let mut follow = rig();
            follow.follow(Vec3::ZERO, 0.0);
            let delta = 1.0 / steps as f32;
            for _ in 0..steps {
                follow.follow(at, delta);
            }
            follow.eye().unwrap()
        };

        let (slow, fast) = (run(15), run(240));
        assert!(
            (slow - fast).length() < 0.05,
            "a second of following should land in the same place: \
             {slow:?} at 15fps against {fast:?} at 240",
        );
    }

    /// A rig told not to lag is welded on, for anything that wants the
    /// picture exact rather than comfortable.
    #[test]
    fn no_catch_up_welds_it_on() {
        let mut follow = rig();
        follow.catch_up = 0.0;
        follow.follow(Vec3::ZERO, 1.0 / 60.0);

        let at = Vec3::new(5.0, 0.0, 5.0);
        assert_eq!(follow.follow(at, 1.0 / 60.0).eye, follow.wants(at));
    }
}