9eead719b0
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
3.8 KiB
Rust
102 lines
3.8 KiB
Rust
//! 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);
|
|
}
|
|
}
|