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
94#[allow(clippy::type_complexity)]
95fn update_rigid_body_trackers_system(
96    gravity: Res<Gravity>,
97    mut query: Query<(
98        &Position,
99        &Rotation,
100        &LinearVelocity,
101        &AngularVelocity,
102        &mut TnuaRigidBodyTracker,
103        Option<&TnuaToggle>,
104        Option<&TnuaGravity>,
105    )>,
106) {
107    for (
108        position,
109        rotation,
110        linaer_velocity,
111        angular_velocity,
112        mut tracker,
113        tnua_toggle,
114        tnua_gravity,
115    ) in query.iter_mut()
116    {
117        match tnua_toggle.copied().unwrap_or_default() {
118            TnuaToggle::Disabled => continue,
119            TnuaToggle::SenseOnly => {}
120            TnuaToggle::Enabled => {}
121        }
122        *tracker = TnuaRigidBodyTracker {
123            translation: position.adjust_precision(),
124            rotation: rotation.adjust_precision(),
125            velocity: linaer_velocity.0.adjust_precision(),
126            angvel: angular_velocity.0.adjust_precision(),
127            gravity: tnua_gravity.map(|g| g.0).unwrap_or(gravity.0),
128        };
129    }
130}
131
132#[allow(clippy::type_complexity)]
133fn update_proximity_sensors_system(
134    spatial_query: SpatialQuery,
135    mut sensor_query: Query<(
136        &mut TnuaProximitySensor,
137        &TnuaSensorOf,
138        Option<&TnuaAvian3dSensorShape>,
139        Option<&mut TnuaGhostSensor>,
140    )>,
141    owner_query: Query<(
142        &Position,
143        &Rotation,
144        Option<&Collider>,
145        Option<&TnuaAvian3dSensorShape>,
146        Option<&TnuaToggle>,
147    )>,
148    collision_layers_query: Query<&CollisionLayers>,
149    other_object_query: Query<(
150        Option<(
151            &Position,
152            &LinearVelocity,
153            &AngularVelocity,
154            Option<&RigidBody>,
155        )>,
156        Option<&CollisionLayers>,
157        Option<&ColliderOf>,
158        Has<TnuaGhostPlatform>,
159        Has<Sensor>,
160        Has<TnuaNotPlatform>,
161    )>,
162) {
163    sensor_query.par_iter_mut().for_each(
164        |(mut sensor, &TnuaSensorOf(owner_entity), shape, mut ghost_sensor)| {
165            let Ok((position, rotation, collider, owner_shape, tnua_toggle)) =
166                owner_query.get(owner_entity)
167            else {
168                return;
169            };
170            let shape = shape.or(owner_shape);
171            match tnua_toggle.copied().unwrap_or_default() {
172                TnuaToggle::Disabled => return,
173                TnuaToggle::SenseOnly => {}
174                TnuaToggle::Enabled => {}
175            }
176            let transform = Transform {
177                translation: position.0.f32(),
178                rotation: rotation.0.f32(),
179                scale: collider
180                    .map(|collider| collider.scale().f32())
181                    .unwrap_or(Vec3::ONE),
182            };
183
184            // TODO: is there any point in doing these transformations as f64 when that feature
185            // flag is active?
186            let cast_origin = transform
187                .transform_point(sensor.cast_origin.f32())
188                .adjust_precision();
189            let cast_direction = sensor.cast_direction;
190
191            struct CastResult {
192                entity: Entity,
193                proximity: Float,
194                intersection_point: Vector3,
195                normal: Dir3,
196            }
197
198            let collision_layers = collision_layers_query.get(owner_entity).ok();
199
200            let mut final_sensor_output: Option<TnuaProximitySensorOutput> = None;
201            if let Some(ghost_sensor) = ghost_sensor.as_mut() {
202                ghost_sensor.0.clear();
203            }
204            let mut apply_cast = |cast_result: CastResult| {
205                let CastResult {
206                    entity,
207                    proximity,
208                    intersection_point,
209                    normal,
210                } = cast_result;
211
212                let Ok((
213                    mut entity_kinematic_data,
214                    mut entity_collision_layers,
215                    entity_collider_of,
216                    mut entity_is_ghost,
217                    mut entity_is_sensor,
218                    mut entity_is_not_platform,
219                )) = other_object_query.get(entity)
220                else {
221                    return false;
222                };
223
224                if let Some(collider_of) = entity_collider_of {
225                    let parent_entity = collider_of.body;
226
227                    // Collider is child of our rigid body. ignore.
228                    if parent_entity == owner_entity {
229                        return true;
230                    }
231
232                    if let Ok((
233                        parent_kinematic_data,
234                        parent_collision_layers,
235                        _,
236                        parent_is_ghost,
237                        parent_is_sensor,
238                        parent_is_not_platform,
239                    )) = other_object_query.get(parent_entity)
240                    {
241                        if entity_kinematic_data.is_none() {
242                            entity_kinematic_data = parent_kinematic_data;
243                        }
244                        if entity_collision_layers.is_none() {
245                            entity_collision_layers = parent_collision_layers;
246                        }
247                        entity_is_ghost = entity_is_ghost || parent_is_ghost;
248                        entity_is_sensor = entity_is_sensor || parent_is_sensor;
249                        entity_is_not_platform = entity_is_not_platform || parent_is_not_platform;
250                    }
251                }
252
253                if entity_is_not_platform {
254                    return true;
255                }
256
257                let entity_linvel;
258                let entity_angvel;
259                if let Some((
260                    entity_position,
261                    entity_linear_velocity,
262                    entity_angular_velocity,
263                    rigid_body,
264                )) = entity_kinematic_data
265                {
266                    if rigid_body == Some(&RigidBody::Static) {
267                        entity_angvel = Vector3::ZERO;
268                        entity_linvel = Vector3::ZERO;
269                    } else {
270                        entity_angvel = entity_angular_velocity.0.adjust_precision();
271                        entity_linvel = entity_linear_velocity.0.adjust_precision()
272                            + if 0.0 < entity_angvel.length_squared() {
273                                let relative_point =
274                                    intersection_point - entity_position.adjust_precision();
275                                // NOTE: no need to project relative_point on the
276                                // rotation plane, it will not affect the cross
277                                // product.
278                                entity_angvel.cross(relative_point)
279                            } else {
280                                Vector3::ZERO
281                            };
282                    }
283                } else {
284                    entity_angvel = Vector3::ZERO;
285                    entity_linvel = Vector3::ZERO;
286                }
287                let sensor_output = TnuaProximitySensorOutput {
288                    entity,
289                    proximity,
290                    normal,
291                    entity_linvel,
292                    entity_angvel,
293                };
294
295                let excluded_by_collision_layers = || {
296                    let collision_layers = collision_layers.copied().unwrap_or_default();
297                    let entity_collision_layers =
298                        entity_collision_layers.copied().unwrap_or_default();
299                    !collision_layers.interacts_with(entity_collision_layers)
300                };
301
302                if entity_is_ghost {
303                    if let Some(ghost_sensor) = ghost_sensor.as_mut() {
304                        ghost_sensor.0.push(sensor_output);
305                    }
306                    true
307                } else if entity_is_sensor || excluded_by_collision_layers() {
308                    true
309                } else {
310                    if final_sensor_output.as_ref().is_none_or(|current_output| {
311                        sensor_output.proximity < current_output.proximity
312                    }) {
313                        // Hits are not guaranteed to be ordered, so we need to make them ordered.
314                        // See https://github.com/idanarye/bevy-tnua/issues/123
315                        final_sensor_output = Some(sensor_output);
316                    }
317                    false
318                }
319            };
320
321            let query_filter = SpatialQueryFilter::from_excluded_entities([owner_entity]);
322            if let Some(TnuaAvian3dSensorShape(shape)) = shape {
323                // TODO: can I bake `owner_rotation` into
324                // `sensor.cast_shape_rotation`?
325                let owner_rotation = Quaternion::from_axis_angle(
326                    cast_direction.adjust_precision(),
327                    rotation
328                        .to_scaled_axis()
329                        .dot(cast_direction.adjust_precision()),
330                );
331                spatial_query.shape_hits_callback(
332                    shape,
333                    cast_origin,
334                    owner_rotation.mul_quat(sensor.cast_shape_rotation.adjust_precision()),
335                    cast_direction,
336                    &ShapeCastConfig {
337                        max_distance: sensor.cast_range,
338                        ignore_origin_penetration: true,
339                        ..default()
340                    },
341                    &query_filter,
342                    |shape_hit_data| {
343                        apply_cast(CastResult {
344                            entity: shape_hit_data.entity,
345                            proximity: shape_hit_data.distance,
346                            intersection_point: shape_hit_data.point1,
347                            normal: Dir3::new(shape_hit_data.normal1.f32())
348                                .unwrap_or_else(|_| -cast_direction),
349                        })
350                    },
351                );
352            } else {
353                spatial_query.ray_hits_callback(
354                    cast_origin,
355                    cast_direction,
356                    sensor.cast_range,
357                    true,
358                    &query_filter,
359                    |ray_hit_data| {
360                        apply_cast(CastResult {
361                            entity: ray_hit_data.entity,
362                            proximity: ray_hit_data.distance,
363                            intersection_point: cast_origin
364                                + ray_hit_data.distance * cast_direction.adjust_precision(),
365                            normal: Dir3::new(ray_hit_data.normal.f32())
366                                .unwrap_or_else(|_| -cast_direction),
367                        })
368                    },
369                );
370            }
371            if let Some(ghost_sensor) = ghost_sensor.as_mut() {
372                // Hits are not guaranteed to be ordered, so we need to make them ordered.
373                // See https://github.com/idanarye/bevy-tnua/issues/123
374                if let Some(final_sensor_output) = final_sensor_output.as_ref() {
375                    ghost_sensor
376                        .0
377                        .retain(|ghost_hit| ghost_hit.proximity < final_sensor_output.proximity);
378                }
379                ghost_sensor
380                    .0
381                    .sort_by_key(|ghost_hit| OrderedFloat(ghost_hit.proximity));
382            }
383            sensor.output = final_sensor_output;
384        },
385    );
386}
387
388fn update_obstacle_radars_system(
389    spatial_query: SpatialQuery,
390    gravity: Res<Gravity>,
391    mut radars_query: Query<(Entity, &mut TnuaObstacleRadar, &Position)>,
392) {
393    if radars_query.is_empty() {
394        return;
395    }
396    for (radar_owner_entity, mut radar, radar_position) in radars_query.iter_mut() {
397        radar.pre_marking_update(
398            radar_owner_entity,
399            radar_position.0,
400            Dir3::new(gravity.0.f32()).unwrap_or(Dir3::Y),
401        );
402        spatial_query.shape_intersections_callback(
403            &Collider::cylinder(radar.radius, radar.height),
404            radar_position.0,
405            Default::default(),
406            &SpatialQueryFilter::DEFAULT,
407            |obstacle_entity| {
408                if radar_owner_entity == obstacle_entity {
409                    return true;
410                }
411                radar.mark_seen(obstacle_entity);
412                true
413            },
414        );
415    }
416}
417
418#[allow(clippy::type_complexity)]
419fn apply_motors_system(
420    mut query: Query<(
421        &TnuaMotor,
422        Forces,
423        Option<&TnuaToggle>,
424        Option<&TnuaGravity>,
425    )>,
426) {
427    for (motor, mut forces, tnua_toggle, tnua_gravity) in query.iter_mut() {
428        match tnua_toggle.copied().unwrap_or_default() {
429            TnuaToggle::Disabled | TnuaToggle::SenseOnly => {
430                return;
431            }
432            TnuaToggle::Enabled => {}
433        }
434
435        if motor.lin.boost.is_finite() {
436            *forces.linear_velocity_mut() += motor.lin.boost;
437        }
438        if motor.lin.acceleration.is_finite() {
439            forces.apply_linear_acceleration(motor.lin.acceleration);
440        }
441        if motor.ang.boost.is_finite() {
442            *forces.angular_velocity_mut() += motor.ang.boost;
443        }
444        if motor.ang.acceleration.is_finite() {
445            forces.apply_torque(motor.ang.acceleration);
446        }
447        if let Some(gravity) = tnua_gravity {
448            forces.apply_linear_acceleration(gravity.0);
449        }
450    }
451}