Skip to main content

concinnity_asset/
rigid_body.rs

1// Grounded character-body schema.
2
3/// Gives a player [Camera3D](#camera3d) gravity, jumping, and a grounded
4/// character body.
5///
6/// Every [Camera3D](#camera3d) already collides with the world as a capsule.
7/// Adding a RigidBody upgrades that camera from a free-flying spectator to a
8/// grounded character: it falls under gravity, lands on surfaces, climbs steps,
9/// slides off steep slopes, and can jump. The capsule size is configured here
10/// too.
11///
12/// ```json
13/// { "name": "player_body", "type": "RigidBody", "args": { "jump_height": 1.4 } }
14/// ```
15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
16#[serde(default)]
17pub struct RigidBody {
18    /// Multiplier applied to the global gravity constant. 1.0 = normal gravity.
19    pub gravity_scale: f32,
20    /// Radius of the player capsule used for collision, in world units.
21    pub capsule_radius: f32,
22    /// Total height of the player capsule. The camera eye sits at the top.
23    pub capsule_height: f32,
24    /// Apex height of a jump in world units. 0 disables jumping.
25    pub jump_height: f32,
26    /// Steepest slope the player can walk up, in degrees.
27    pub max_slope_deg: f32,
28    /// Tallest obstacle the controller auto-steps over, in world units.
29    pub step_height: f32,
30    /// True when the capsule is resting on a surface this frame.
31    /// Written by PhysicsSystem.
32    #[serde(skip)]
33    pub is_grounded: bool,
34}
35
36impl Default for RigidBody {
37    fn default() -> Self {
38        Self {
39            gravity_scale: 1.0,
40            capsule_radius: 0.3,
41            capsule_height: 1.7,
42            jump_height: 1.1,
43            max_slope_deg: 50.0,
44            step_height: 0.3,
45            is_grounded: true,
46        }
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53
54    #[test]
55    fn the_default_capsule_is_a_person_sized_walker() {
56        let b = RigidBody::default();
57        assert_eq!(b.capsule_radius, 0.3);
58        assert_eq!(b.capsule_height, 1.7);
59        assert_eq!(b.jump_height, 1.1);
60        assert_eq!(b.max_slope_deg, 50.0);
61        assert_eq!(b.step_height, 0.3);
62        assert_eq!(b.gravity_scale, 1.0);
63        // Starting grounded keeps the first frame from playing a fall.
64        assert!(b.is_grounded);
65    }
66
67    #[test]
68    fn ground_state_is_runtime_only_and_never_rides_the_wire() {
69        let b: RigidBody = serde_json::from_str(
70            r#"{"gravity_scale":2,"capsule_radius":0.4,"capsule_height":2,"jump_height":0,
71                "max_slope_deg":35,"step_height":0.5,"is_grounded":false}"#,
72        )
73        .unwrap();
74        // The authored `is_grounded` is skipped, so it keeps its default.
75        assert!(b.is_grounded);
76        assert_eq!(b.jump_height, 0.0);
77
78        let bytes = postcard::to_allocvec(&b).unwrap();
79        let back: RigidBody = postcard::from_bytes(&bytes).unwrap();
80        assert_eq!(back.gravity_scale, 2.0);
81        assert_eq!(back.capsule_radius, 0.4);
82        assert_eq!(back.capsule_height, 2.0);
83        assert_eq!(back.max_slope_deg, 35.0);
84        assert_eq!(back.step_height, 0.5);
85        assert!(back.is_grounded);
86    }
87}