Skip to main content

bevy_tnua_avian3d/
lib.rs

1//! # avian3d Integration for bevy-tnua
2//!
3//! In addition to the instruction in bevy-tnua's documentation:
4//!
5//! * Add [`TnuaAvian3dPlugin`] to the Bevy app.
6//! * Optionally: Add [`TnuaAvian3dSensorShape`] to either entity of the character controller by
7//!   Tnua or to the to the sensor entities. If exists, the shape on the sensor entity overrides
8//!   the one on the character entity for that specific sensor.
9mod spatial_ext;
10
11use avian3d::{prelude::*, schedule::PhysicsStepSystems};
12use bevy::ecs::schedule::{InternedScheduleLabel, ScheduleLabel};
13use bevy::prelude::*;
14use ordered_float::OrderedFloat;
15pub use spatial_ext::TnuaSpatialExtAvian3d;
16
17use bevy_tnua_physics_integration_layer::{
18    TnuaPipelineSystems, TnuaSystems,
19    data_for_backends::{
20        TnuaGhostPlatform, TnuaGhostSensor, TnuaGravity, TnuaMotor, TnuaNotPlatform,
21        TnuaProximitySensor, TnuaProximitySensorOutput, TnuaRigidBodyTracker, TnuaSensorOf,
22        TnuaToggle,
23    },
24    math::{AdjustPrecision, AsF32, Float, Quaternion, Vector3},
25    obstacle_radar::TnuaObstacleRadar,
26};
27
28pub mod prelude {
29    pub use crate::{TnuaAvian3dPlugin, TnuaAvian3dSensorShape, TnuaSpatialExtAvian3d};
30}
31
32/// Add this plugin to use avian3d as a physics backend.
33///
34/// This plugin should be used in addition to `TnuaControllerPlugin`.
35/// Note that you should make sure both of these plugins use the same schedule.
36/// This should usually be `PhysicsSchedule`, which by default is `FixedUpdate`.
37///
38/// # Example
39///
40/// ```ignore
41/// App::new()
42///     .add_plugins((
43///         DefaultPlugins,
44///         PhysicsPlugins::default(),
45///         TnuaControllerPlugin::new(PhysicsSchedule),
46///         TnuaAvian3dPlugin::new(PhysicsSchedule),
47///     ));
48/// ```
49pub struct TnuaAvian3dPlugin {
50    schedule: InternedScheduleLabel,
51}
52
53impl TnuaAvian3dPlugin {
54    pub fn new(schedule: impl ScheduleLabel) -> Self {
55        Self {
56            schedule: schedule.intern(),
57        }
58    }
59}
60
61impl Plugin for TnuaAvian3dPlugin {
62    fn build(&self, app: &mut App) {
63        app.configure_sets(
64            self.schedule,
65            TnuaSystems
66                // Need to run _before_ `First`, not after it. The documentation is misleading. See
67                // https://github.com/Jondolf/avian/issues/675
68                .before(PhysicsStepSystems::First)
69                .run_if(|physics_time: Res<Time<Physics>>| !physics_time.is_paused()),
70        );
71        app.add_systems(
72            self.schedule,
73            (
74                update_rigid_body_trackers_system,
75                update_proximity_sensors_system,
76                update_obstacle_radars_system
77                    // Both use SpatialQuery, which uses a ResMut
78                    .ambiguous_with(update_proximity_sensors_system),
79            )
80                .in_set(TnuaPipelineSystems::Sensors),
81        );
82        app.add_systems(
83            self.schedule,
84            apply_motors_system.in_set(TnuaPipelineSystems::Motors),
85        );
86        app.register_required_components_with::<TnuaGravity, GravityScale>(|| GravityScale(0.0));
87    }
88}
89
90/// Add this component to make [`TnuaProximitySensor`] cast a shape instead of a ray.
91#[derive(Component)]
92pub struct TnuaAvian3dSensorShape(pub Collider);
93
94impl FromTemplate for TnuaAvian3dSensorShape {
95    type Template = TnuaAvian3dSensorShapeTemplate;
96}
97
98#[derive(Default)]
99pub struct TnuaAvian3dSensorShapeTemplate(pub Option<Collider>);
100
101impl Template for TnuaAvian3dSensorShapeTemplate {
102    type Output = TnuaAvian3dSensorShape;
103
104    fn build_template(&self, _: &mut bevy::ecs::template::TemplateContext) -> Result<Self::Output> {
105        Ok(TnuaAvian3dSensorShape(self.0.clone().ok_or_else(|| {
106            BevyError::error(
107                "TnuaAvian3dSensorShape does not have a default value. Must provide shape",
108            )
109        })?))
110    }
111
112    fn clone_template(&self) -> Self {
113        Self(self.0.clone())
114    }
115}
116
117#[allow(clippy::type_complexity)]
118fn update_rigid_body_trackers_system(
119    gravity: Res<Gravity>,
120    mut query: Query<(
121        &Position,
122        &Rotation,
123        &LinearVelocity,
124        &AngularVelocity,
125        &mut TnuaRigidBodyTracker,
126        Option<&TnuaToggle>,
127        Option<&TnuaGravity>,
128    )>,
129) {
130    for (
131        position,
132        rotation,
133        linaer_velocity,
134        angular_velocity,
135        mut tracker,
136        tnua_toggle,
137        tnua_gravity,
138    ) in query.iter_mut()
139    {
140        match tnua_toggle.copied().unwrap_or_default() {
141            TnuaToggle::Disabled => continue,
142            TnuaToggle::SenseOnly => {}
143            TnuaToggle::Enabled => {}
144        }
145        *tracker = TnuaRigidBodyTracker {
146            translation: position.adjust_precision(),
147            rotation: rotation.adjust_precision(),
148            velocity: linaer_velocity.0.adjust_precision(),
149            angvel: angular_velocity.0.adjust_precision(),
150            gravity: tnua_gravity
151                .map(|g| g.0)
152                .unwrap_or(gravity.0.adjust_precision()),
153        };
154    }
155}
156
157#[allow(clippy::type_complexity)]
158fn update_proximity_sensors_system(
159    spatial_query: SpatialQuery,
160    mut sensor_query: Query<(
161        &mut TnuaProximitySensor,
162        &TnuaSensorOf,
163        Option<&TnuaAvian3dSensorShape>,
164        Option<&mut TnuaGhostSensor>,
165    )>,
166    owner_query: Query<(
167        &Position,
168        &Rotation,
169        Option<&Collider>,
170        Option<&TnuaAvian3dSensorShape>,
171        Option<&TnuaToggle>,
172    )>,
173    collision_layers_query: Query<&CollisionLayers>,
174    other_object_query: Query<(
175        Option<(
176            &Position,
177            &LinearVelocity,
178            &AngularVelocity,
179            Option<&RigidBody>,
180        )>,
181        Option<&CollisionLayers>,
182        Option<&ColliderOf>,
183        Has<TnuaGhostPlatform>,
184        Has<Sensor>,
185        Has<TnuaNotPlatform>,
186    )>,
187) {
188    sensor_query.par_iter_mut().for_each(
189        |(mut sensor, &TnuaSensorOf(owner_entity), shape, mut ghost_sensor)| {
190            let Ok((position, rotation, collider, owner_shape, tnua_toggle)) =
191                owner_query.get(owner_entity)
192            else {
193                return;
194            };
195            let shape = shape.or(owner_shape);
196            match tnua_toggle.copied().unwrap_or_default() {
197                TnuaToggle::Disabled => return,
198                TnuaToggle::SenseOnly => {}
199                TnuaToggle::Enabled => {}
200            }
201            let transform = Transform {
202                translation: position.0.f32(),
203                rotation: rotation.0.f32(),
204                scale: collider
205                    .map(|collider| collider.scale().f32())
206                    .unwrap_or(Vec3::ONE),
207            };
208
209            // TODO: is there any point in doing these transformations as f64 when that feature
210            // flag is active?
211            let cast_origin = transform
212                .transform_point(sensor.cast_origin.f32())
213                .adjust_precision();
214            let cast_direction = sensor.cast_direction;
215
216            struct CastResult {
217                entity: Entity,
218                proximity: Float,
219                intersection_point: Vector3,
220                normal: Dir3,
221            }
222
223            let collision_layers = collision_layers_query.get(owner_entity).ok();
224
225            let mut final_sensor_output: Option<TnuaProximitySensorOutput> = None;
226            if let Some(ghost_sensor) = ghost_sensor.as_mut() {
227                ghost_sensor.0.clear();
228            }
229            let mut apply_cast = |cast_result: CastResult| {
230                let CastResult {
231                    entity,
232                    proximity,
233                    intersection_point,
234                    normal,
235                } = cast_result;
236
237                let Ok((
238                    mut entity_kinematic_data,
239                    mut entity_collision_layers,
240                    entity_collider_of,
241                    mut entity_is_ghost,
242                    mut entity_is_sensor,
243                    mut entity_is_not_platform,
244                )) = other_object_query.get(entity)
245                else {
246                    return false;
247                };
248
249                if let Some(collider_of) = entity_collider_of {
250                    let parent_entity = collider_of.body;
251
252                    // Collider is child of our rigid body. ignore.
253                    if parent_entity == owner_entity {
254                        return true;
255                    }
256
257                    if let Ok((
258                        parent_kinematic_data,
259                        parent_collision_layers,
260                        _,
261                        parent_is_ghost,
262                        parent_is_sensor,
263                        parent_is_not_platform,
264                    )) = other_object_query.get(parent_entity)
265                    {
266                        if entity_kinematic_data.is_none() {
267                            entity_kinematic_data = parent_kinematic_data;
268                        }
269                        if entity_collision_layers.is_none() {
270                            entity_collision_layers = parent_collision_layers;
271                        }
272                        entity_is_ghost = entity_is_ghost || parent_is_ghost;
273                        entity_is_sensor = entity_is_sensor || parent_is_sensor;
274                        entity_is_not_platform = entity_is_not_platform || parent_is_not_platform;
275                    }
276                }
277
278                if entity_is_not_platform {
279                    return true;
280                }
281
282                let entity_linvel;
283                let entity_angvel;
284                if let Some((
285                    entity_position,
286                    entity_linear_velocity,
287                    entity_angular_velocity,
288                    rigid_body,
289                )) = entity_kinematic_data
290                {
291                    if rigid_body == Some(&RigidBody::Static) {
292                        entity_angvel = Vector3::ZERO;
293                        entity_linvel = Vector3::ZERO;
294                    } else {
295                        entity_angvel = entity_angular_velocity.0.adjust_precision();
296                        entity_linvel = entity_linear_velocity.0.adjust_precision()
297                            + if 0.0 < entity_angvel.length_squared() {
298                                let relative_point =
299                                    intersection_point - entity_position.adjust_precision();
300                                // NOTE: no need to project relative_point on the
301                                // rotation plane, it will not affect the cross
302                                // product.
303                                entity_angvel.cross(relative_point)
304                            } else {
305                                Vector3::ZERO
306                            };
307                    }
308                } else {
309                    entity_angvel = Vector3::ZERO;
310                    entity_linvel = Vector3::ZERO;
311                }
312                let sensor_output = TnuaProximitySensorOutput {
313                    entity,
314                    proximity,
315                    normal,
316                    entity_linvel,
317                    entity_angvel,
318                };
319
320                let excluded_by_collision_layers = || {
321                    let collision_layers = collision_layers.copied().unwrap_or_default();
322                    let entity_collision_layers =
323                        entity_collision_layers.copied().unwrap_or_default();
324                    !collision_layers.interacts_with(entity_collision_layers)
325                };
326
327                if entity_is_ghost {
328                    if let Some(ghost_sensor) = ghost_sensor.as_mut() {
329                        ghost_sensor.0.push(sensor_output);
330                    }
331                    true
332                } else if entity_is_sensor || excluded_by_collision_layers() {
333                    true
334                } else {
335                    if final_sensor_output.as_ref().is_none_or(|current_output| {
336                        sensor_output.proximity < current_output.proximity
337                    }) {
338                        // Hits are not guaranteed to be ordered, so we need to make them ordered.
339                        // See https://github.com/idanarye/bevy-tnua/issues/123
340                        final_sensor_output = Some(sensor_output);
341                    }
342                    false
343                }
344            };
345
346            let query_filter = SpatialQueryFilter::from_excluded_entities([owner_entity]);
347            if let Some(TnuaAvian3dSensorShape(shape)) = shape {
348                // TODO: can I bake `owner_rotation` into
349                // `sensor.cast_shape_rotation`?
350                let owner_rotation = Quaternion::from_axis_angle(
351                    cast_direction.adjust_precision(),
352                    rotation
353                        .to_scaled_axis()
354                        .adjust_precision()
355                        .dot(cast_direction.adjust_precision()),
356                );
357                spatial_query.shape_hits_callback(
358                    shape,
359                    cast_origin,
360                    owner_rotation
361                        .mul_quat(sensor.cast_shape_rotation.adjust_precision())
362                        .f32(),
363                    cast_direction,
364                    &ShapeCastConfig {
365                        max_distance: sensor.cast_range.f32(),
366                        ignore_origin_penetration: true,
367                        ..default()
368                    },
369                    &query_filter,
370                    |shape_hit_data| {
371                        apply_cast(CastResult {
372                            entity: shape_hit_data.entity,
373                            proximity: shape_hit_data.distance.adjust_precision(),
374                            intersection_point: shape_hit_data.point1,
375                            normal: Dir3::new(shape_hit_data.normal1.f32())
376                                .unwrap_or_else(|_| -cast_direction),
377                        })
378                    },
379                );
380            } else {
381                spatial_query.ray_hits_callback(
382                    cast_origin,
383                    cast_direction,
384                    sensor.cast_range.f32(),
385                    true,
386                    &query_filter,
387                    |ray_hit_data| {
388                        apply_cast(CastResult {
389                            entity: ray_hit_data.entity,
390                            proximity: ray_hit_data.distance.adjust_precision(),
391                            intersection_point: cast_origin
392                                + ray_hit_data.distance.adjust_precision()
393                                    * cast_direction.adjust_precision(),
394                            normal: Dir3::new(ray_hit_data.normal.f32())
395                                .unwrap_or_else(|_| -cast_direction),
396                        })
397                    },
398                );
399            }
400            if let Some(ghost_sensor) = ghost_sensor.as_mut() {
401                // Hits are not guaranteed to be ordered, so we need to make them ordered.
402                // See https://github.com/idanarye/bevy-tnua/issues/123
403                if let Some(final_sensor_output) = final_sensor_output.as_ref() {
404                    ghost_sensor
405                        .0
406                        .retain(|ghost_hit| ghost_hit.proximity < final_sensor_output.proximity);
407                }
408                ghost_sensor
409                    .0
410                    .sort_by_key(|ghost_hit| OrderedFloat(ghost_hit.proximity));
411            }
412            sensor.output = final_sensor_output;
413        },
414    );
415}
416
417fn update_obstacle_radars_system(
418    spatial_query: SpatialQuery,
419    gravity: Res<Gravity>,
420    mut radars_query: Query<(Entity, &mut TnuaObstacleRadar, &Position)>,
421) {
422    if radars_query.is_empty() {
423        return;
424    }
425    for (radar_owner_entity, mut radar, radar_position) in radars_query.iter_mut() {
426        radar.pre_marking_update(
427            radar_owner_entity,
428            radar_position.0,
429            Dir3::new(gravity.0.f32()).unwrap_or(Dir3::Y),
430        );
431        spatial_query.shape_intersections_callback(
432            &Collider::cylinder(radar.radius.f32(), radar.height.f32()),
433            radar_position.0,
434            Rotation::default(),
435            &SpatialQueryFilter::DEFAULT,
436            |obstacle_entity| {
437                if radar_owner_entity == obstacle_entity {
438                    return true;
439                }
440                radar.mark_seen(obstacle_entity);
441                true
442            },
443        );
444    }
445}
446
447#[allow(clippy::type_complexity)]
448fn apply_motors_system(
449    mut query: Query<(
450        &TnuaMotor,
451        Forces,
452        Option<&TnuaToggle>,
453        Option<&TnuaGravity>,
454    )>,
455) {
456    for (motor, mut forces, tnua_toggle, tnua_gravity) in query.iter_mut() {
457        match tnua_toggle.copied().unwrap_or_default() {
458            TnuaToggle::Disabled | TnuaToggle::SenseOnly => {
459                return;
460            }
461            TnuaToggle::Enabled => {}
462        }
463
464        if motor.lin.boost.is_finite() {
465            *forces.linear_velocity_mut() += motor.lin.boost.f32();
466        }
467        if motor.lin.acceleration.is_finite() {
468            forces.apply_linear_acceleration(motor.lin.acceleration.f32());
469        }
470        if motor.ang.boost.is_finite() {
471            *forces.angular_velocity_mut() += motor.ang.boost.f32();
472        }
473        if motor.ang.acceleration.is_finite() {
474            forces.apply_torque(motor.ang.acceleration.f32());
475        }
476        if let Some(gravity) = tnua_gravity {
477            forces.apply_linear_acceleration(gravity.0.f32());
478        }
479    }
480}