//! The [`CharacterController`] component — a kinematic capsule character. //! //! A character controller is *not* a rigid body: it never reacts to forces. //! Instead the game asks it to move by some desired translation each frame and //! the controller resolves that against the world's colliders — sliding along //! walls, stepping up small ledges, sticking to the ground on slopes, and //! refusing to climb anything too steep — then reports whether the character //! ended up grounded. It is the basis for player/NPC movement and is reused by //! the Stage-15 prototyping kit. //! //! The character is a **capsule** (radius + cylindrical half-height along `+Y`). //! An entity with this component is moved through //! [`PhysicsWorld::move_character`](crate::PhysicsWorld::move_character); it does //! **not** carry a [`RigidBody`](crate::RigidBody)/[`Collider`](crate::Collider) //! (it isn't simulated), so it never appears in the body set and never //! self-collides. use oxide_engine::math::Vec3; use oxide_engine::reflect::Reflect; use serde::{Deserialize, Serialize}; /// Component: a kinematic capsule character moved by move-and-slide. /// /// Tune the capsule shape (`radius`/`half_height`) and the movement rules /// (`max_slope_degrees`, `step_offset`, `snap_to_ground`, `skin_width`). Pass an /// entity carrying this to /// [`PhysicsWorld::move_character`](crate::PhysicsWorld::move_character). #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)] pub struct CharacterController { /// Capsule radius. pub radius: f32, /// Capsule cylindrical half-height (the straight segment between the two /// hemispherical caps), along local `+Y`. pub half_height: f32, /// Steepest ground slope (degrees from horizontal) the character will walk /// up; steeper surfaces are treated as walls. pub max_slope_degrees: f32, /// Tallest step/ledge the character will automatically climb. `0` disables /// auto-stepping. pub step_offset: f32, /// Distance below the feet within which the character snaps down to stay /// grounded (prevents bouncing down stairs/slopes). `0` disables snapping. pub snap_to_ground: f32, /// A small collision-detection margin kept around the capsule. pub skin_width: f32, } impl Default for CharacterController { fn default() -> Self { Self { radius: 0.3, half_height: 0.6, max_slope_degrees: 45.0, step_offset: 0.3, snap_to_ground: 0.2, skin_width: 0.01, } } } impl CharacterController { /// The total half-height of the capsule (cylinder half-height + one radius), /// i.e. the distance from the capsule center to either tip. pub fn total_half_height(&self) -> f32 { self.half_height + self.radius } /// The local-space offset from the capsule center down to the feet (the /// bottom tip), useful for placing/grounding the character. pub fn foot_offset(&self) -> Vec3 { Vec3::new(0.0, -self.total_half_height(), 0.0) } } #[cfg(test)] mod tests { use super::*; #[test] fn default_is_a_reasonable_humanoid_capsule() { let c = CharacterController::default(); assert_eq!(c.radius, 0.3); assert!((c.total_half_height() - 0.9).abs() < 1e-6); assert!((c.foot_offset() - Vec3::new(0.0, -0.9, 0.0)).length() < 1e-6); } #[test] fn round_trips_through_ron() { let c = CharacterController { radius: 0.4, half_height: 1.0, max_slope_degrees: 50.0, step_offset: 0.0, snap_to_ground: 0.0, skin_width: 0.02, }; let ron = ron::to_string(&c).unwrap(); let back: CharacterController = ron::from_str(&ron).unwrap(); assert_eq!(c, back); } }