ferravex_physics/character/
motion.rs1use super::{
2 geometry::{Hit, Queries},
3 *,
4};
5#[derive(Clone, Copy, Debug, Default, PartialEq)]
6pub struct JumpState {
7 pub coyote: f32,
9 pub buffer: f32,
10 pub held: bool,
11}
12impl JumpState {
13 fn valid(self) -> bool {
14 self.coyote.is_finite()
15 && (0. ..=0.5).contains(&self.coyote)
16 && self.buffer.is_finite()
17 && (0. ..=0.5).contains(&self.buffer)
18 }
19}
20#[derive(Clone, Copy, Debug, PartialEq)]
21pub struct Support {
22 pub id: u64,
23 pub local_anchor: Vec3,
24 pub previous_anchor: Vec3,
25}
26impl Support {
27 fn new(o: Obstacle, feet: Vec3) -> Self {
28 Self {
29 id: o.id,
30 local_anchor: o.rotation.conjugate() * (feet - o.position),
31 previous_anchor: feet,
32 }
33 }
34}
35#[derive(Clone, Copy, Debug)]
36pub struct Motion {
37 pub position: Vec3,
38 pub translation: Vec3,
39 pub grounded: bool,
40 pub ground_normal: Vec3,
41 pub support: Option<Support>,
42 pub stepped: bool,
43 pub blocked: bool,
44 pub candidates: usize,
45 normals: [Vec3; 16],
46 normal_count: usize,
47}
48fn slide(mut value: Vec3, normal: Vec3, min_ground: f32) -> Vec3 {
49 let into = value.dot(normal);
50 if into < 0. {
51 value -= normal * into;
52 }
53 if normal.y > 0. && normal.y < min_ground && value.y > 0. {
54 value.y = 0.;
55 let horizontal = Vec3::new(normal.x, 0., normal.z).normalize_or_zero();
56 let into = value.dot(horizontal);
57 if into < 0. {
58 value -= horizontal * into;
59 }
60 }
61 value
62}
63fn ground<T: Terrain + ?Sized>(
64 queries: &mut Queries<'_, T>,
65 p: Vec3,
66 distance: f32,
67) -> Result<Option<Hit>, CharacterError> {
68 Ok(queries
69 .cast(p, -Vec3::Y * distance)?
70 .filter(|h| h.normal.y >= queries.settings.min_ground_normal))
71}
72fn try_step<T: Terrain + ?Sized>(
73 q: &mut Queries<'_, T>,
74 p: Vec3,
75 remaining: Vec3,
76) -> Result<Option<(Vec3, Hit)>, CharacterError> {
77 let up = Vec3::Y * q.settings.step_height;
78 let across = Vec3::new(remaining.x, 0., remaining.z);
79 if across.length_squared() < 1e-12 || q.cast(p, up)?.is_some() {
80 return Ok(None);
81 }
82 let raised = p + up;
83 if q.cast(raised, across)?.is_some() {
84 return Ok(None);
85 }
86 let destination = raised + across;
87 let distance = q.settings.step_height + q.settings.ground_snap;
88 let Some(mut hit) = q.cast(destination, -Vec3::Y * distance)? else {
89 return Ok(None);
90 };
91 if hit.normal.y < q.settings.min_ground_normal {
92 let ObstacleShape::Box { half } = hit.obstacle.shape else {
96 return Ok(None);
97 };
98 let top = hit.obstacle.rotation * Vec3::Y;
99 if top.y < q.settings.min_ground_normal || hit.normal.y <= 0. {
100 return Ok(None);
101 }
102 let plane = hit.obstacle.position + top * half.y;
103 let height = (plane.dot(top) - destination.x * top.x - destination.z * top.z) / top.y;
104 if height - p.y > q.settings.step_height + q.settings.skin || height < p.y - q.settings.skin
105 {
106 return Ok(None);
107 }
108 hit.normal = top;
109 }
110 Ok(Some((destination - Vec3::Y * distance * hit.fraction, hit)))
111}
112fn move_inner<T: Terrain + ?Sized>(
113 q: &mut Queries<'_, T>,
114 origin: Vec3,
115 delta: Vec3,
116 was_grounded: bool,
117) -> Result<Motion, CharacterError> {
118 let mut p = q.recover(origin)?;
119 let mut remaining = delta;
120 let mut result = Motion {
121 position: p,
122 translation: Vec3::ZERO,
123 grounded: false,
124 ground_normal: Vec3::Y,
125 support: None,
126 stepped: false,
127 blocked: false,
128 candidates: 0,
129 normals: [Vec3::ZERO; 16],
130 normal_count: 0,
131 };
132 let mut stair_support = None;
133 for _ in 0..q.settings.max_iterations {
134 if remaining.length_squared() < 1e-12 {
135 break;
136 }
137 let Some(hit) = q.cast(p, remaining)? else {
138 p += remaining;
139 break;
140 };
141 p += remaining * hit.fraction;
142 remaining *= 1. - hit.fraction;
143 result.blocked = true;
144 if was_grounded
145 && delta.y <= 0.
146 && hit.normal.y < q.settings.min_ground_normal
147 && q.settings.step_height > 0.
148 && let Some((stepped, landing)) = try_step(q, p, remaining)?
149 {
150 p = stepped;
151 stair_support = Some(landing);
152 result.stepped = true;
153 break;
154 }
155 if result.normal_count < result.normals.len() {
156 result.normals[result.normal_count] = hit.normal;
157 result.normal_count += 1;
158 }
159 remaining = slide(remaining, hit.normal, q.settings.min_ground_normal);
160 }
161 let support = if delta.y <= 0. {
162 ground(q, p, q.settings.ground_snap + q.settings.skin)?
163 } else {
164 None
165 };
166 if let Some(hit) = support {
167 p -= Vec3::Y * (q.settings.ground_snap + q.settings.skin) * hit.fraction;
168 }
169 if let Some(hit) = support.or(stair_support) {
170 result.grounded = true;
171 result.ground_normal = hit.normal;
172 result.support = Some(Support::new(hit.obstacle, p));
173 }
174 if !bounded(p) {
175 return Err(CharacterError::InvalidInput);
176 }
177 result.position = p;
178 result.translation = p - origin;
179 result.candidates = q.candidates;
180 Ok(result)
181}
182pub fn move_and_slide<T: Terrain + ?Sized>(
186 world: &T,
187 shape: CharacterShape,
188 settings: CharacterSettings,
189 position: Vec3,
190 displacement: Vec3,
191 was_grounded: bool,
192) -> Result<Motion, CharacterError> {
193 if !shape.valid()
194 || !settings.valid()
195 || !bounded(position)
196 || !bounded(displacement)
197 || (shape.planar() && (position.z != 0. || displacement.z != 0.))
198 {
199 return Err(CharacterError::InvalidInput);
200 }
201 let mut queries = Queries {
202 world,
203 shape,
204 settings,
205 candidates: 0,
206 ignored: None,
207 };
208 move_inner(&mut queries, position, displacement, was_grounded)
209}
210#[derive(Clone, Copy, Debug, Default)]
211pub struct CharacterInput {
212 pub direction: Vec3,
214 pub jump: bool,
216}
217#[derive(Clone, Copy, Debug, PartialEq)]
218pub struct Character {
219 pub position: Vec3,
220 pub velocity: Vec3,
222 pub grounded: bool,
223 pub ground_normal: Vec3,
224 pub jump: JumpState,
225 pub support: Option<Support>,
226}
227impl Character {
228 pub fn new(feet: Vec3) -> Self {
229 Self {
230 position: feet,
231 velocity: Vec3::ZERO,
232 grounded: false,
233 ground_normal: Vec3::Y,
234 jump: Default::default(),
235 support: None,
236 }
237 }
238 pub fn step<T: Terrain + ?Sized>(
241 &mut self,
242 world: &T,
243 shape: CharacterShape,
244 settings: CharacterSettings,
245 input: CharacterInput,
246 dt: f32,
247 ) -> Result<CharacterStep, CharacterError> {
248 if !dt.is_finite()
249 || !(0. ..=0.1).contains(&dt)
250 || dt == 0.
251 || !settings.valid()
252 || !shape.valid()
253 || !bounded(self.position)
254 || !bounded(self.velocity)
255 || !self.ground_normal.is_finite()
256 || !input.direction.is_finite()
257 || !self.jump.valid()
258 || (shape.planar() && (self.position.z != 0. || self.velocity.z != 0.))
259 || self
260 .support
261 .is_some_and(|s| !bounded(s.local_anchor) || !bounded(s.previous_anchor))
262 {
263 return Err(CharacterError::InvalidInput);
264 }
265 let mut next = *self;
266 let mut q = Queries {
267 world,
268 shape,
269 settings,
270 candidates: 0,
271 ignored: None,
272 };
273 let mut carry = Vec3::ZERO;
274 if let Some(support) = next.support
275 && let Some(o) = world.obstacle(support.id)
276 {
277 if !o.valid() {
278 return Err(CharacterError::InvalidObstacle);
279 }
280 if !o.sensor && o.membership & settings.membership_mask != 0 {
281 let delta =
282 o.position + o.rotation * support.local_anchor - support.previous_anchor;
283 if !bounded(delta) {
284 return Err(CharacterError::InvalidInput);
285 }
286 q.ignored = Some(o.id);
287 let motion = move_inner(&mut q, next.position, delta, false)?;
288 q.ignored = None;
289 next.position = motion.position;
290 carry = motion.translation / dt;
291 }
292 }
293 next.position = q.recover(next.position)?;
294 let on_ground = next.velocity.y <= 0.
295 && ground(&mut q, next.position, settings.ground_snap + settings.skin)?.is_some();
296 let was_grounded = next.grounded || on_ground;
297 next.jump.coyote = if was_grounded {
298 settings.coyote_time
299 } else {
300 (next.jump.coyote - dt).max(0.)
301 };
302 next.jump.buffer = if input.jump && !next.jump.held {
303 settings.jump_buffer.max(dt)
304 } else {
305 (next.jump.buffer - dt).max(0.)
306 };
307 next.jump.held = input.jump;
308 let jumped = next.jump.buffer > 0. && (was_grounded || next.jump.coyote > 0.);
309 let wish = Vec3::new(
310 input.direction.x,
311 0.,
312 if shape.planar() {
313 0.
314 } else {
315 input.direction.z
316 },
317 )
318 .clamp_length_max(1.)
319 * settings.speed;
320 let horizontal = Vec3::new(next.velocity.x, 0., next.velocity.z);
321 let acceleration = if was_grounded {
322 settings.ground_acceleration
323 } else {
324 settings.air_acceleration
325 };
326 next.velocity += (wish - horizontal).clamp_length_max(acceleration * dt);
327 if jumped {
328 next.velocity.y = settings.jump_speed;
329 next.velocity += carry;
330 next.jump.buffer = 0.;
331 next.jump.coyote = 0.;
332 next.grounded = false;
333 }
334 next.velocity.y = (next.velocity.y - settings.gravity * dt).max(-settings.terminal_speed);
335 let motion = move_inner(
336 &mut q,
337 next.position,
338 next.velocity * dt,
339 was_grounded && !jumped,
340 )?;
341 next.position = motion.position;
342 next.grounded = motion.grounded;
343 next.ground_normal = motion.ground_normal;
344 next.support = motion.support;
345 for normal in &motion.normals[..motion.normal_count] {
346 next.velocity = slide(next.velocity, *normal, settings.min_ground_normal);
347 }
348 if next.grounded {
349 next.velocity.y = 0.;
350 }
351 if !bounded(next.velocity) {
352 return Err(CharacterError::InvalidInput);
353 }
354 *self = next;
355 Ok(CharacterStep {
356 motion,
357 jumped,
358 carried: carry * dt,
359 })
360 }
361}
362#[derive(Clone, Copy, Debug)]
363pub struct CharacterStep {
364 pub motion: Motion,
365 pub jumped: bool,
366 pub carried: Vec3,
367}