1use crate::{math::*, *};
2
3#[derive(Clone, Copy, Debug)]
6pub struct CharacterSettings {
7 pub radius: f32,
8 pub skin: f32,
9 pub ground_snap: f32,
10 pub step_height: f32,
11 pub min_ground_normal: f32,
13 pub max_iterations: u32,
14}
15impl Default for CharacterSettings {
16 fn default() -> Self {
17 Self {
18 radius: 0.4,
19 skin: 0.002,
20 ground_snap: 0.08,
21 step_height: 0.25,
22 min_ground_normal: 0.7,
23 max_iterations: 5,
24 }
25 }
26}
27#[derive(Clone, Copy, Debug, PartialEq)]
28pub struct CharacterMotion<const D: usize> {
29 pub position: [f32; D],
30 pub translation: [f32; D],
31 pub grounded: bool,
32 pub stepped: bool,
33 pub blocked: bool,
34}
35impl<const D: usize> PhysicsWorld<D> {
36 pub fn move_character(
42 &self,
43 origin: [f32; D],
44 displacement: [f32; D],
45 settings: CharacterSettings,
46 filter: QueryFilter,
47 ) -> Result<CharacterMotion<D>, PhysicsError> {
48 if !valid(origin) || !valid(displacement) {
49 return Err(PhysicsError::InvalidBody);
50 }
51 if !Shape::<D>::ball(settings.radius).valid()
52 || !settings.skin.is_finite()
53 || !(1e-5..=0.1).contains(&settings.skin)
54 || !settings.ground_snap.is_finite()
55 || !(0. ..=10.).contains(&settings.ground_snap)
56 || !settings.step_height.is_finite()
57 || !(0. ..=10.).contains(&settings.step_height)
58 || !settings.min_ground_normal.is_finite()
59 || !(0.01..=1.).contains(&settings.min_ground_normal)
60 || !(1..=16).contains(&settings.max_iterations)
61 {
62 return Err(PhysicsError::InvalidConfiguration);
63 }
64 let down = unit(1, -1.);
65 let up = unit(1, 1.);
66 let mut position = origin;
67 let grounded_at_start = self
68 .cast_ball(
69 origin,
70 settings.radius,
71 down,
72 settings.ground_snap + settings.skin,
73 filter,
74 )
75 .is_some_and(|hit| hit.normal[1] >= settings.min_ground_normal);
76 let mut grounded = false;
77 let mut stepped = false;
78 let mut blocked = false;
79 let mut remaining = displacement;
80 for _ in 0..settings.max_iterations {
81 let distance = length(remaining);
82 if distance < 1e-7 {
83 break;
84 }
85 let direction = mul(remaining, distance.recip());
86 let Some(hit) = self.cast_ball(
87 position,
88 settings.radius,
89 direction,
90 distance + settings.skin,
91 filter,
92 ) else {
93 position = add(position, remaining);
94 break;
95 };
96 if hit.distance > distance + settings.skin * 0.5 {
98 position = add(position, remaining);
99 break;
100 }
101 let advance = (hit.distance - settings.skin).max(0.).min(distance);
102 position = add(position, mul(direction, advance));
103 remaining = mul(remaining, (distance - advance) / distance);
104 grounded |= hit.normal[1] >= settings.min_ground_normal;
105 blocked = true;
106 if grounded_at_start
107 && displacement[1] <= 0.
108 && settings.step_height > 0.
109 && hit.normal[1] < settings.min_ground_normal
110 {
111 let horizontal = std::array::from_fn(|i| if i == 1 { 0. } else { remaining[i] });
112 let distance = length(horizontal);
113 if distance > 1e-6
114 && self
115 .cast_ball(
116 position,
117 settings.radius,
118 up,
119 settings.step_height + settings.skin,
120 filter,
121 )
122 .is_none()
123 {
124 let raised = add(position, mul(up, settings.step_height));
125 if self
126 .cast_ball(
127 raised,
128 settings.radius,
129 mul(horizontal, distance.recip()),
130 distance + settings.skin,
131 filter,
132 )
133 .is_none()
134 {
135 let across = add(raised, horizontal);
136 if let Some(landing) = self.cast_ball(
137 across,
138 settings.radius,
139 down,
140 settings.step_height + settings.ground_snap,
141 filter,
142 ) && landing.normal[1] >= settings.min_ground_normal
143 {
144 position = add(
145 across,
146 mul(down, (landing.distance - settings.skin).max(0.)),
147 );
148 grounded = true;
149 stepped = true;
150 break;
151 }
152 }
153 }
154 }
155 let mut normal = hit.normal;
156 if normal[1] > 0. && normal[1] < settings.min_ground_normal {
157 normal[1] = 0.;
159 normal = normalize(normal).unwrap_or(hit.normal);
160 }
161 let into = dot(remaining, normal);
162 if into < 0. {
163 remaining = sub(remaining, mul(normal, into));
164 }
165 if advance < 1e-7 && into >= -1e-7 {
166 break;
167 }
168 }
169 if displacement[1] <= 0.
170 && let Some(ground) = self.cast_ball(
171 position,
172 settings.radius,
173 down,
174 settings.ground_snap + settings.skin,
175 filter,
176 )
177 && ground.normal[1] >= settings.min_ground_normal
178 {
179 position = add(
180 position,
181 mul(down, (ground.distance - settings.skin).max(0.)),
182 );
183 grounded = true;
184 }
185 if !valid(position) {
186 return Err(PhysicsError::NumericalLimit);
187 }
188 Ok(CharacterMotion {
189 position,
190 translation: sub(position, origin),
191 grounded,
192 stepped,
193 blocked,
194 })
195 }
196}