Skip to main content

viewport_lib/camera/controllers/
movement.rs

1//! Movement-input helper shared across the character camera controllers.
2//!
3//! [`FirstPersonCameraController`](crate::camera::controllers::first_person::FirstPersonCameraController) and
4//! [`ThirdPersonCameraController`](crate::camera::controllers::third_person::ThirdPersonCameraController) expose a
5//! `forward_dir()` / `right_dir()` pair defining the ground-plane basis the
6//! player walks in. [`wish_xy_from_actions`] resolves the standard fly-movement
7//! actions against that basis and returns a normalised horizontal wish vector,
8//! so hosts do not reimplement the same movement block per view.
9//!
10//! ```ignore
11//! let wish = wish_xy_from_actions(&frame, fps.forward_dir(), fps.right_dir());
12//! let velocity_xy = wish * walk_speed;
13//! ```
14
15use glam::{Vec2, Vec3};
16
17use crate::interaction::input::action::Action;
18use crate::interaction::input::action_frame::ActionFrame;
19
20/// Resolve the forward / back / left / right movement actions into a horizontal
21/// wish vector in the camera's ground-plane basis.
22///
23/// `forward_xy` and `right_xy` are the camera's horizontal forward and right
24/// vectors (the controllers expose them via `forward_dir` / `right_dir`). Active
25/// actions accumulate; the result is normalised so diagonal motion does not move
26/// faster than orthogonal motion. Returns [`Vec2::ZERO`] when no movement action
27/// is active. Callers multiply by walk speed and apply any sprint modifier.
28///
29/// Reads the [`Action::FlyForward`] / [`Action::FlyBackward`] /
30/// [`Action::FlyLeft`] / [`Action::FlyRight`] entries from the frame, so the
31/// same binding preset that drives free-fly movement also drives character
32/// movement.
33pub fn wish_xy_from_actions(frame: &ActionFrame, forward_xy: Vec3, right_xy: Vec3) -> Vec2 {
34    let mut wish = Vec3::ZERO;
35    if frame.is_active(Action::FlyForward) {
36        wish += forward_xy;
37    }
38    if frame.is_active(Action::FlyBackward) {
39        wish -= forward_xy;
40    }
41    if frame.is_active(Action::FlyRight) {
42        wish += right_xy;
43    }
44    if frame.is_active(Action::FlyLeft) {
45        wish -= right_xy;
46    }
47    Vec2::new(wish.x, wish.y)
48        .try_normalize()
49        .unwrap_or(Vec2::ZERO)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::interaction::input::action_frame::ResolvedActionState;
56
57    fn frame_with(actions: &[Action]) -> ActionFrame {
58        let mut f = ActionFrame::default();
59        for &a in actions {
60            f.actions.insert(a, ResolvedActionState::Held);
61        }
62        f
63    }
64
65    fn approx_eq(a: Vec2, b: Vec2) -> bool {
66        (a - b).length() < 1e-5
67    }
68
69    #[test]
70    fn no_actions_returns_zero() {
71        let wish = wish_xy_from_actions(&frame_with(&[]), Vec3::Y, Vec3::X);
72        assert_eq!(wish, Vec2::ZERO);
73    }
74
75    #[test]
76    fn forward_only_returns_forward() {
77        let wish = wish_xy_from_actions(&frame_with(&[Action::FlyForward]), Vec3::Y, Vec3::X);
78        assert!(approx_eq(wish, Vec2::Y));
79    }
80
81    #[test]
82    fn right_only_returns_right() {
83        let wish = wish_xy_from_actions(&frame_with(&[Action::FlyRight]), Vec3::Y, Vec3::X);
84        assert!(approx_eq(wish, Vec2::X));
85    }
86
87    #[test]
88    fn diagonal_is_normalised() {
89        let wish = wish_xy_from_actions(
90            &frame_with(&[Action::FlyForward, Action::FlyRight]),
91            Vec3::Y,
92            Vec3::X,
93        );
94        let inv_sqrt2 = 1.0 / 2.0_f32.sqrt();
95        assert!(approx_eq(wish, Vec2::new(inv_sqrt2, inv_sqrt2)));
96    }
97
98    #[test]
99    fn opposing_actions_cancel() {
100        let wish = wish_xy_from_actions(
101            &frame_with(&[Action::FlyForward, Action::FlyBackward]),
102            Vec3::Y,
103            Vec3::X,
104        );
105        assert_eq!(wish, Vec2::ZERO);
106    }
107
108    #[test]
109    fn rotated_basis_steers_correctly() {
110        // yaw = PI/2: forward = +X, right = -Y.
111        let wish = wish_xy_from_actions(
112            &frame_with(&[Action::FlyForward]),
113            Vec3::X,
114            Vec3::new(0.0, -1.0, 0.0),
115        );
116        assert!(approx_eq(wish, Vec2::X));
117    }
118}