bevy_tnua/builtins/
walk.rs

1use std::time::Duration;
2
3use bevy::prelude::*;
4use bevy_tnua_physics_integration_layer::data_for_backends::{
5    TnuaGhostSensor, TnuaProximitySensor,
6};
7use serde::{Deserialize, Serialize};
8
9use crate::TnuaBasis;
10use crate::basis_action_traits::TnuaBasisAccess;
11use crate::basis_capabilities::{
12    TnuaBasisWithDisplacement, TnuaBasisWithFloating, TnuaBasisWithFrameOfReferenceSurface,
13    TnuaBasisWithGround, TnuaBasisWithHeadroom, TnuaBasisWithSpring,
14};
15use crate::ghost_overrides::TnuaGhostOverwrite;
16use crate::math::*;
17use crate::sensor_sets::{ProximitySensorPreparationHelper, TnuaSensors};
18use crate::util::rotation_arc_around_axis;
19use crate::{TnuaBasisContext, TnuaMotor, TnuaVelChange};
20
21use super::walk_sensors::TnuaBuiltinWalkSensors;
22
23#[derive(Default)]
24#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
25pub struct TnuaBuiltinWalk {
26    /// The direction (in the world space) and speed to accelerate to.
27    ///
28    /// Tnua assumes that this vector is orthogonal to the up dierction.
29    pub desired_motion: Vector3,
30
31    /// If non-zero, Tnua will rotate the character so that its negative Z will face in that
32    /// direction.
33    ///
34    /// Tnua assumes that this vector is orthogonal to the up direction.
35    pub desired_forward: Option<Dir3>,
36}
37
38#[derive(Clone, Serialize, Deserialize)]
39pub struct TnuaBuiltinWalkConfig {
40    // How fast the character will go.
41    //
42    // Note that this will be the speed when [`desired_motion`](TnuaBuiltinWalk::desired_motion)
43    // is a unit vector - meaning that its length is 1.0. If its not 1.0, the speed will be a
44    // multiply of that length.
45    //
46    // Also note that this is the full speed - the character will gradually accelerate to this
47    // speed based on the acceleration configuration.
48    pub speed: Float,
49
50    /// The height at which the character will float above ground at rest.
51    ///
52    /// Note that this is the height of the character's center of mass - not the distance from its
53    /// collision mesh.
54    ///
55    /// To make a character crouch, instead of altering this field, prefer to use the
56    /// [`TnuaBuiltinCrouch`](crate::builtins::TnuaBuiltinCrouch) action.
57    pub float_height: Float,
58
59    /// Add an upward-facing proximity sensor that can check if the character has room above it.
60    ///
61    /// This is not (currently) used by `TnuaBuiltinWalk` itself, but
62    /// [`TnuaBuiltinCrouch`](crate::builtins::TnuaBuiltinCrouch) uses it to determine
63    pub headroom: Option<TnuaBuiltinWalkHeadroom>,
64
65    /// Extra distance above the `float_height` where the spring is still in effect.
66    ///
67    /// When the character is at at most this distance above the
68    /// [`float_height`](Self::float_height), the spring force will kick in and move it to the
69    /// float height - even if that means pushing it down. If the character is above that distance
70    /// above the `float_height`, Tnua will consider it to be in the air.
71    pub cling_distance: Float,
72
73    /// The force that pushes the character to the float height.
74    ///
75    /// The actual force applied is in direct linear relationship to the displacement from the
76    /// `float_height`.
77    pub spring_strength: Float,
78
79    /// A force that slows down the characters vertical spring motion.
80    ///
81    /// The actual dampening is in direct linear relationship to the vertical velocity it tries to
82    /// dampen.
83    ///
84    /// Note that as this approaches 2.0, the character starts to shake violently and eventually
85    /// get launched upward at great speed.
86    pub spring_dampening: Float,
87
88    /// The acceleration for horizontal movement.
89    ///
90    /// Note that this is the acceleration for starting the horizontal motion and for reaching the
91    /// top speed. When braking or changing direction the acceleration is greater, up to 2 times
92    /// `acceleration` when doing a 180 turn.
93    pub acceleration: Float,
94
95    /// The acceleration for horizontal movement while in the air.
96    ///
97    /// Set to 0.0 to completely disable air movement.
98    pub air_acceleration: Float,
99
100    /// The time, in seconds, the character can still jump after losing their footing.
101    pub coyote_time: Float,
102
103    /// Extra gravity for free fall (fall that's not initiated by a jump or some other action that
104    /// provides its own fall gravity)
105    ///
106    /// **NOTE**: This force will be added to the normal gravity.
107    ///
108    /// **NOTE**: If the parameter set to this option is too low, the character may be able to run
109    /// up a slope and "jump" potentially even higher than a regular jump, even without pressing
110    /// the jump button.
111    pub free_fall_extra_gravity: Float,
112
113    /// The maximum angular velocity used for keeping the character standing upright.
114    ///
115    /// NOTE: The character's rotation can also be locked to prevent it from being tilted, in which
116    /// case this paramter is redundant and can be set to 0.0.
117    pub tilt_offset_angvel: Float,
118
119    /// The maximum angular acceleration used for reaching `tilt_offset_angvel`.
120    ///
121    /// NOTE: The character's rotation can also be locked to prevent it from being tilted, in which
122    /// case this paramter is redundant and can be set to 0.0.
123    pub tilt_offset_angacl: Float,
124
125    /// The maximum angular velocity used for turning the character when the direction changes.
126    pub turning_angvel: Float,
127
128    /// The maximum slope, in radians, that the character can stand on without slipping.
129    pub max_slope: Float,
130}
131
132/// Definition for an upward-facing proximity sensor that checks for obstacles above the
133/// character's "head".
134#[derive(Clone, Serialize, Deserialize)]
135pub struct TnuaBuiltinWalkHeadroom {
136    /// Disnce from the collider's center to its top.
137    pub distance_to_collider_top: Float,
138
139    /// Extra distance, from the top of the collider, for the sensor to cover.
140    ///
141    /// Set this slightly higher than zero. Actions that rely on the headroom sensor will want
142    /// to add their own extra distance anyway by using
143    /// [`set_extra_headroom`](TnuaBasisWithHeadroom::set_extra_headroom).
144    pub sensor_extra_distance: Float,
145}
146
147impl Default for TnuaBuiltinWalkHeadroom {
148    fn default() -> Self {
149        Self {
150            distance_to_collider_top: 0.0,
151            sensor_extra_distance: 0.1,
152        }
153    }
154}
155
156impl Default for TnuaBuiltinWalkConfig {
157    fn default() -> Self {
158        Self {
159            speed: 20.0,
160            float_height: 0.0,
161            headroom: None,
162            cling_distance: 1.0,
163            spring_strength: 400.0,
164            spring_dampening: 1.2,
165            acceleration: 60.0,
166            air_acceleration: 20.0,
167            coyote_time: 30.15,
168            free_fall_extra_gravity: 60.0,
169            tilt_offset_angvel: 5.0,
170            tilt_offset_angacl: 500.0,
171            turning_angvel: 10.0,
172            max_slope: float_consts::FRAC_PI_2,
173        }
174    }
175}
176
177#[derive(Debug, Clone)]
178#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
179struct StandingOnState {
180    entity: Entity,
181    entity_linvel: Vector3,
182}
183
184#[derive(Default, Debug)]
185#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
186pub struct TnuaBuiltinWalkMemory {
187    airborne_timer: Option<Timer>,
188    /// The current distance of the character from the distance its supposed to float at.
189    pub standing_offset: Vector3,
190    standing_on: Option<StandingOnState>,
191    effective_velocity: Vector3,
192    vertical_velocity: Float,
193    /// The velocity, perpendicular to the up direction, that the character is supposed to move at.
194    ///
195    /// If the character is standing on something else
196    /// ([`standing_on_entity`](Self::standing_on_entity) returns `Some`) then the
197    /// `running_velocity` will be relative to the velocity of that entity.
198    pub running_velocity: Vector3,
199    extra_headroom: Float,
200}
201
202// TODO: move these to a trait?
203impl TnuaBuiltinWalkMemory {
204    /// Returns the entity that the character currently stands on.
205    pub fn standing_on_entity(&self) -> Option<Entity> {
206        Some(self.standing_on.as_ref()?.entity)
207    }
208}
209
210impl TnuaBasis for TnuaBuiltinWalk {
211    type Config = TnuaBuiltinWalkConfig;
212
213    type Memory = TnuaBuiltinWalkMemory;
214
215    type Sensors<'a> = TnuaBuiltinWalkSensors<'a>;
216
217    fn apply(
218        &self,
219        config: &Self::Config,
220        memory: &mut Self::Memory,
221        sensors: &Self::Sensors<'_>,
222        ctx: TnuaBasisContext,
223        motor: &mut TnuaMotor,
224    ) {
225        if let Some(stopwatch) = &mut memory.airborne_timer {
226            #[allow(clippy::unnecessary_cast)]
227            stopwatch.tick(Duration::from_secs_f64(ctx.frame_duration as f64));
228        }
229
230        // Reset this every frame - if there is an action that changes it, it will use
231        // `influence_basis` to set it back immediately after.
232        memory.extra_headroom = 0.0;
233
234        let climb_vectors: Option<ClimbVectors>;
235        let considered_in_air: bool;
236        let impulse_to_offset: Vector3;
237        let slipping_vector: Option<Vector3>;
238
239        if let Some(sensor_output) = &sensors.ground.output {
240            memory.effective_velocity = ctx.tracker.velocity - sensor_output.entity_linvel;
241            let sideways_unnormalized = sensor_output
242                .normal
243                .cross(*ctx.up_direction)
244                .adjust_precision();
245            if sideways_unnormalized == Vector3::ZERO {
246                climb_vectors = None;
247            } else {
248                climb_vectors = Some(ClimbVectors {
249                    direction: sideways_unnormalized
250                        .cross(sensor_output.normal.adjust_precision())
251                        .normalize_or_zero()
252                        .adjust_precision(),
253                    sideways: sideways_unnormalized.normalize_or_zero().adjust_precision(),
254                });
255            }
256
257            slipping_vector = {
258                let angle_with_floor = sensor_output
259                    .normal
260                    .angle_between(*ctx.up_direction)
261                    .adjust_precision();
262                if angle_with_floor <= config.max_slope {
263                    None
264                } else {
265                    Some(
266                        sensor_output
267                            .normal
268                            .reject_from(*ctx.up_direction)
269                            .adjust_precision(),
270                    )
271                }
272            };
273
274            if memory.airborne_timer.is_some() {
275                considered_in_air = true;
276                impulse_to_offset = Vector3::ZERO;
277                memory.standing_on = None;
278            } else {
279                if let Some(standing_on_state) = &memory.standing_on {
280                    if standing_on_state.entity != sensor_output.entity {
281                        impulse_to_offset = Vector3::ZERO;
282                    } else {
283                        impulse_to_offset =
284                            sensor_output.entity_linvel - standing_on_state.entity_linvel;
285                    }
286                } else {
287                    impulse_to_offset = Vector3::ZERO;
288                }
289
290                if slipping_vector.is_none() {
291                    considered_in_air = false;
292                    memory.standing_on = Some(StandingOnState {
293                        entity: sensor_output.entity,
294                        entity_linvel: sensor_output.entity_linvel,
295                    });
296                } else {
297                    considered_in_air = true;
298                    memory.standing_on = None;
299                }
300            }
301        } else {
302            memory.effective_velocity = ctx.tracker.velocity;
303            climb_vectors = None;
304            considered_in_air = true;
305            impulse_to_offset = Vector3::ZERO;
306            slipping_vector = None;
307            memory.standing_on = None;
308        }
309        memory.effective_velocity += impulse_to_offset;
310
311        let velocity_on_plane = memory
312            .effective_velocity
313            .reject_from(ctx.up_direction.adjust_precision());
314
315        let desired_velocity = self.desired_motion * config.speed;
316
317        let desired_boost = desired_velocity - velocity_on_plane;
318
319        let safe_direction_coefficient = desired_velocity
320            .normalize_or_zero()
321            .dot(velocity_on_plane.normalize_or_zero());
322        let direction_change_factor = 1.5 - 0.5 * safe_direction_coefficient;
323
324        let relevant_acceleration_limit = if considered_in_air {
325            config.air_acceleration
326        } else {
327            config.acceleration
328        };
329        let max_acceleration = direction_change_factor * relevant_acceleration_limit;
330
331        memory.vertical_velocity = if let Some(climb_vectors) = &climb_vectors {
332            memory.effective_velocity.dot(climb_vectors.direction)
333                * climb_vectors
334                    .direction
335                    .dot(ctx.up_direction.adjust_precision())
336        } else {
337            0.0
338        };
339
340        let walk_vel_change = if desired_velocity == Vector3::ZERO && slipping_vector.is_none() {
341            // When stopping, prefer a boost to be able to reach a precise stop (see issue #39)
342            let walk_boost = desired_boost.clamp_length_max(ctx.frame_duration * max_acceleration);
343            let walk_boost = if let Some(climb_vectors) = &climb_vectors {
344                climb_vectors.project(walk_boost)
345            } else {
346                walk_boost
347            };
348            TnuaVelChange::boost(walk_boost)
349        } else {
350            // When accelerating, prefer an acceleration because the physics backends treat it
351            // better (see issue #34)
352            let walk_acceleration =
353                (desired_boost / ctx.frame_duration).clamp_length_max(max_acceleration);
354            let walk_acceleration =
355                if let (Some(climb_vectors), None) = (&climb_vectors, slipping_vector) {
356                    climb_vectors.project(walk_acceleration)
357                } else {
358                    walk_acceleration
359                };
360
361            let slipping_boost = 'slipping_boost: {
362                let Some(slipping_vector) = slipping_vector else {
363                    break 'slipping_boost Vector3::ZERO;
364                };
365                let vertical_velocity = if 0.0 <= memory.vertical_velocity {
366                    ctx.tracker.gravity.dot(ctx.up_direction.adjust_precision())
367                        * ctx.frame_duration
368                } else {
369                    memory.vertical_velocity
370                };
371
372                let Ok((slipping_direction, slipping_per_vertical_unit)) =
373                    Dir3::new_and_length(slipping_vector.f32())
374                else {
375                    break 'slipping_boost Vector3::ZERO;
376                };
377
378                let required_veloicty_in_slipping_direction =
379                    slipping_per_vertical_unit.adjust_precision() * -vertical_velocity;
380                let expected_velocity = velocity_on_plane + walk_acceleration * ctx.frame_duration;
381                let expected_velocity_in_slipping_direction =
382                    expected_velocity.dot(slipping_direction.adjust_precision());
383
384                let diff = required_veloicty_in_slipping_direction
385                    - expected_velocity_in_slipping_direction;
386
387                if diff <= 0.0 {
388                    break 'slipping_boost Vector3::ZERO;
389                }
390
391                slipping_direction.adjust_precision() * diff
392            };
393            TnuaVelChange {
394                acceleration: walk_acceleration,
395                boost: slipping_boost,
396            }
397        };
398
399        let upward_impulse: TnuaVelChange = 'upward_impulse: {
400            let should_disable_due_to_slipping =
401                slipping_vector.is_some() && memory.vertical_velocity <= 0.0;
402            for _ in 0..2 {
403                #[allow(clippy::unnecessary_cast)]
404                match &mut memory.airborne_timer {
405                    None => {
406                        if let (false, Some(sensor_output)) =
407                            (should_disable_due_to_slipping, &sensors.ground.output)
408                        {
409                            // not doing the jump calculation here
410                            let spring_offset =
411                                config.float_height - sensor_output.proximity.adjust_precision();
412                            memory.standing_offset =
413                                -spring_offset * ctx.up_direction.adjust_precision();
414                            break 'upward_impulse Self::spring_force(
415                                &TnuaBasisAccess {
416                                    input: self,
417                                    config,
418                                    memory,
419                                },
420                                &ctx,
421                                spring_offset,
422                            );
423                        } else {
424                            memory.airborne_timer = Some(Timer::from_seconds(
425                                config.coyote_time as f32,
426                                TimerMode::Once,
427                            ));
428                            continue;
429                        }
430                    }
431                    Some(_) => {
432                        if let (false, Some(sensor_output)) =
433                            (should_disable_due_to_slipping, &sensors.ground.output)
434                            && sensor_output.proximity.adjust_precision() <= config.float_height
435                        {
436                            memory.airborne_timer = None;
437                            continue;
438                        }
439                        if memory.vertical_velocity <= 0.0 {
440                            break 'upward_impulse TnuaVelChange::acceleration(
441                                -config.free_fall_extra_gravity
442                                    * ctx.up_direction.adjust_precision(),
443                            );
444                        } else {
445                            break 'upward_impulse TnuaVelChange::ZERO;
446                        }
447                    }
448                }
449            }
450            error!("Tnua could not decide on jump state");
451            TnuaVelChange::ZERO
452        };
453
454        motor.lin = walk_vel_change + TnuaVelChange::boost(impulse_to_offset) + upward_impulse;
455        let new_velocity = memory.effective_velocity
456            + motor.lin.boost
457            + ctx.frame_duration * motor.lin.acceleration
458            - impulse_to_offset;
459        memory.running_velocity = new_velocity.reject_from(ctx.up_direction.adjust_precision());
460
461        // Tilt
462
463        let torque_to_fix_tilt = {
464            let tilted_up = ctx.tracker.rotation.mul_vec3(Vector3::Y);
465
466            let rotation_required_to_fix_tilt =
467                Quaternion::from_rotation_arc(tilted_up, ctx.up_direction.adjust_precision());
468
469            let desired_angvel = (rotation_required_to_fix_tilt.xyz() / ctx.frame_duration)
470                .clamp_length_max(config.tilt_offset_angvel);
471            let angular_velocity_diff = desired_angvel - ctx.tracker.angvel;
472            angular_velocity_diff.clamp_length_max(ctx.frame_duration * config.tilt_offset_angacl)
473        };
474
475        // Turning
476
477        let desired_angvel = if let Some(desired_forward) = self.desired_forward {
478            let current_forward = ctx.tracker.rotation.mul_vec3(Vector3::NEG_Z);
479            let rotation_along_up_axis = rotation_arc_around_axis(
480                ctx.up_direction,
481                current_forward,
482                desired_forward.adjust_precision(),
483            )
484            .unwrap_or(0.0);
485            (rotation_along_up_axis / ctx.frame_duration)
486                .clamp(-config.turning_angvel, config.turning_angvel)
487        } else {
488            0.0
489        };
490
491        // NOTE: This is the regular axis system so we used the configured up.
492        let existing_angvel = ctx.tracker.angvel.dot(ctx.up_direction.adjust_precision());
493
494        // This is the torque. Should it be clamped by an acceleration? From experimenting with
495        // this I think it's meaningless and only causes bugs.
496        let torque_to_turn = desired_angvel - existing_angvel;
497
498        let existing_turn_torque = torque_to_fix_tilt.dot(ctx.up_direction.adjust_precision());
499        let torque_to_turn = torque_to_turn - existing_turn_torque;
500
501        motor.ang = TnuaVelChange::boost(
502            torque_to_fix_tilt + torque_to_turn * ctx.up_direction.adjust_precision(),
503        );
504    }
505
506    fn get_or_create_sensors<'a: 'b, 'b>(
507        up_direction: Dir3,
508        config: &'a Self::Config,
509        memory: &Self::Memory,
510        entities: &'a mut <Self::Sensors<'static> as TnuaSensors<'static>>::Entities,
511        proximity_sensors_query: &'b Query<(&TnuaProximitySensor, Has<TnuaGhostSensor>)>,
512        controller_entity: Entity,
513        commands: &mut Commands,
514        has_ghost_overwrites: bool,
515    ) -> Option<Self::Sensors<'b>> {
516        let ground = ProximitySensorPreparationHelper {
517            cast_direction: -up_direction,
518            cast_range: config.float_height + config.cling_distance,
519            ghost_sensor: has_ghost_overwrites,
520            ..Default::default()
521        }
522        .prepare_for(
523            &mut entities.ground,
524            proximity_sensors_query,
525            controller_entity,
526            commands,
527        );
528
529        let headroom = if let Some(headroom) = config.headroom.as_ref() {
530            ProximitySensorPreparationHelper {
531                cast_direction: up_direction,
532                cast_range: headroom.distance_to_collider_top
533                    + headroom.sensor_extra_distance
534                    + memory.extra_headroom,
535                ..Default::default()
536            }
537            .prepare_for(
538                &mut entities.headroom,
539                proximity_sensors_query,
540                controller_entity,
541                commands,
542            )
543        } else {
544            ProximitySensorPreparationHelper::ensure_not_existing(
545                &mut entities.headroom,
546                proximity_sensors_query,
547                commands,
548            )
549        };
550        // .prepare_for(
551
552        Some(Self::Sensors {
553            ground: ground?,
554            headroom,
555        })
556    }
557
558    fn ghost_sensor_overwrites<'a>(
559        ghost_overwrites: &'a mut <Self::Sensors<'static> as TnuaSensors<'static>>::GhostOverwrites,
560        entities: &<Self::Sensors<'static> as TnuaSensors<'static>>::Entities,
561    ) -> impl Iterator<Item = (&'a mut TnuaGhostOverwrite, Entity)> {
562        [(&mut ghost_overwrites.ground, entities.ground)]
563            .into_iter()
564            .flat_map(|(o, e)| Some((o, e?)))
565    }
566}
567
568impl TnuaBasisWithFrameOfReferenceSurface for TnuaBuiltinWalk {
569    fn effective_velocity(access: &TnuaBasisAccess<Self>) -> Vector3 {
570        access.memory.effective_velocity
571    }
572
573    fn vertical_velocity(access: &TnuaBasisAccess<Self>) -> Float {
574        access.memory.vertical_velocity
575    }
576}
577impl TnuaBasisWithDisplacement for TnuaBuiltinWalk {
578    fn displacement(access: &TnuaBasisAccess<Self>) -> Option<Vector3> {
579        match access.memory.airborne_timer {
580            None => Some(access.memory.standing_offset),
581            Some(_) => None,
582        }
583    }
584}
585impl TnuaBasisWithGround for TnuaBuiltinWalk {
586    fn is_airborne(access: &TnuaBasisAccess<Self>) -> bool {
587        access
588            .memory
589            .airborne_timer
590            .as_ref()
591            .is_some_and(|timer| timer.is_finished())
592    }
593
594    fn violate_coyote_time(memory: &mut Self::Memory) {
595        if let Some(timer) = &mut memory.airborne_timer {
596            timer.set_duration(Duration::ZERO);
597        }
598    }
599
600    fn ground_sensor<'a>(sensors: &Self::Sensors<'a>) -> &'a TnuaProximitySensor {
601        sensors.ground
602    }
603}
604impl TnuaBasisWithHeadroom for TnuaBuiltinWalk {
605    fn headroom_intrusion<'a>(
606        access: &TnuaBasisAccess<Self>,
607        sensors: &Self::Sensors<'a>,
608    ) -> Option<std::ops::Range<Float>> {
609        let headroom_config = access.config.headroom.as_ref()?;
610        let headroom_sensor_output = sensors.headroom?.output.as_ref()?;
611        Some(headroom_config.distance_to_collider_top..headroom_sensor_output.proximity)
612    }
613
614    fn set_extra_headroom(memory: &mut Self::Memory, extra_headroom: Float) {
615        memory.extra_headroom = extra_headroom.max(0.0);
616    }
617}
618impl TnuaBasisWithFloating for TnuaBuiltinWalk {
619    fn float_height(access: &TnuaBasisAccess<Self>) -> Float {
620        access.config.float_height
621    }
622}
623impl TnuaBasisWithSpring for TnuaBuiltinWalk {
624    fn spring_force(
625        access: &TnuaBasisAccess<Self>,
626        ctx: &TnuaBasisContext,
627        spring_offset: Float,
628    ) -> TnuaVelChange {
629        let spring_force: Float = spring_offset * access.config.spring_strength;
630
631        let relative_velocity = access
632            .memory
633            .effective_velocity
634            .dot(ctx.up_direction.adjust_precision())
635            - access.memory.vertical_velocity;
636
637        let gravity_compensation = -ctx.tracker.gravity;
638
639        let dampening_boost = relative_velocity * access.config.spring_dampening;
640
641        TnuaVelChange {
642            acceleration: ctx.up_direction.adjust_precision() * spring_force + gravity_compensation,
643            boost: ctx.up_direction.adjust_precision() * -dampening_boost,
644        }
645    }
646}
647
648#[derive(Debug, Clone)]
649struct ClimbVectors {
650    direction: Vector3,
651    sideways: Vector3,
652}
653
654impl ClimbVectors {
655    fn project(&self, vector: Vector3) -> Vector3 {
656        let axis_direction = vector.dot(self.direction) * self.direction;
657        let axis_sideways = vector.dot(self.sideways) * self.sideways;
658        axis_direction + axis_sideways
659    }
660}