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 the sensor entities. This means the entity of
7//!   the characters controlled by Tnua, but also other things like the entity generated by
8//!   `TnuaCrouchEnforcer`, that can be affected with a closure.
9mod spatial_ext;
10
11use avian3d::{prelude::*, schedule::PhysicsStepSystems};
12use bevy::ecs::schedule::{InternedScheduleLabel, ScheduleLabel};
13use bevy::prelude::*;
14use bevy_tnua_physics_integration_layer::math::AsF32;
15use bevy_tnua_physics_integration_layer::math::Float;
16use bevy_tnua_physics_integration_layer::math::Vector3;
17use bevy_tnua_physics_integration_layer::math::{AdjustPrecision, Quaternion};
18pub use spatial_ext::TnuaSpatialExtAvian3d;
19
20use bevy_tnua_physics_integration_layer::data_for_backends::TnuaGhostSensor;
21use bevy_tnua_physics_integration_layer::data_for_backends::TnuaGravity;
22use bevy_tnua_physics_integration_layer::data_for_backends::TnuaToggle;
23use bevy_tnua_physics_integration_layer::data_for_backends::{TnuaGhostPlatform, TnuaNotPlatform};
24use bevy_tnua_physics_integration_layer::data_for_backends::{
25    TnuaMotor, TnuaProximitySensor, TnuaProximitySensorOutput, TnuaRigidBodyTracker,
26};
27use bevy_tnua_physics_integration_layer::obstacle_radar::TnuaObstacleRadar;
28use bevy_tnua_physics_integration_layer::subservient_sensors::TnuaSubservientSensor;
29use bevy_tnua_physics_integration_layer::TnuaPipelineSystems;
30use bevy_tnua_physics_integration_layer::TnuaSystems;
31
32pub mod prelude {
33    pub use crate::{TnuaAvian3dPlugin, TnuaAvian3dSensorShape, TnuaSpatialExtAvian3d};
34}
35
36/// Add this plugin to use avian3d as a physics backend.
37///
38/// This plugin should be used in addition to `TnuaControllerPlugin`.
39/// Note that you should make sure both of these plugins use the same schedule.
40/// This should usually be `PhysicsSchedule`, which by default is `FixedUpdate`.
41///
42/// # Example
43///
44/// ```ignore
45/// App::new()
46///     .add_plugins((
47///         DefaultPlugins,
48///         PhysicsPlugins::default(),
49///         TnuaControllerPlugin::new(PhysicsSchedule),
50///         TnuaAvian3dPlugin::new(PhysicsSchedule),
51///     ));
52/// ```
53pub struct TnuaAvian3dPlugin {
54    schedule: InternedScheduleLabel,
55}
56
57impl TnuaAvian3dPlugin {
58    pub fn new(schedule: impl ScheduleLabel) -> Self {
59        Self {
60            schedule: schedule.intern(),
61        }
62    }
63}
64
65impl Plugin for TnuaAvian3dPlugin {
66    fn build(&self, app: &mut App) {
67        app.configure_sets(
68            self.schedule,
69            TnuaSystems
70                // Need to run _before_ `First`, not after it. The documentation is misleading. See
71                // https://github.com/Jondolf/avian/issues/675
72                .before(PhysicsStepSystems::First)
73                .run_if(|physics_time: Res<Time<Physics>>| !physics_time.is_paused()),
74        );
75        app.add_systems(
76            self.schedule,
77            (
78                update_rigid_body_trackers_system,
79                update_proximity_sensors_system,
80                update_obstacle_radars_system,
81            )
82                .in_set(TnuaPipelineSystems::Sensors),
83        );
84        app.add_systems(
85            self.schedule,
86            apply_motors_system.in_set(TnuaPipelineSystems::Motors),
87        );
88        app.add_systems(
89            Update,
90            ensure_subservient_sensors_are_colliders_of_their_bevy_parents,
91        );
92        app.register_required_components::<TnuaSubservientSensor, Position>();
93        app.register_required_components::<TnuaSubservientSensor, Rotation>();
94        app.register_required_components_with::<TnuaGravity, GravityScale>(|| GravityScale(0.0));
95    }
96}
97
98#[allow(clippy::type_complexity)]
99fn ensure_subservient_sensors_are_colliders_of_their_bevy_parents(
100    query: Query<(Entity, &ChildOf), (With<TnuaSubservientSensor>, Without<ColliderOf>)>,
101    mut commands: Commands,
102) {
103    for (entity, child_of) in query.iter() {
104        commands
105            .entity(entity)
106            // NOTE: Use the parent from child_of instead of `TnuaSubservientSensor::owner_entity`
107            // because Bevy parenting is how it works in Rapier and the sensor's owner is allowed
108            // to be different than the Bevy parent.
109            .insert_if_new(ColliderOf { body: child_of.0 });
110    }
111}
112
113/// Add this component to make [`TnuaProximitySensor`] cast a shape instead of a ray.
114#[derive(Component)]
115pub struct TnuaAvian3dSensorShape(pub Collider);
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.map(|g| g.0).unwrap_or(gravity.0),
151        };
152    }
153}
154
155#[allow(clippy::type_complexity)]
156fn update_proximity_sensors_system(
157    spatial_query_pipeline: Res<SpatialQueryPipeline>,
158    mut query: Query<(
159        Entity,
160        &Position,
161        &Rotation,
162        Option<&Collider>,
163        &mut TnuaProximitySensor,
164        Option<&TnuaAvian3dSensorShape>,
165        Option<&mut TnuaGhostSensor>,
166        Option<&TnuaSubservientSensor>,
167        Option<&TnuaToggle>,
168    )>,
169    collision_layers_query: Query<&CollisionLayers>,
170    other_object_query: Query<(
171        Option<(
172            &Position,
173            &LinearVelocity,
174            &AngularVelocity,
175            Option<&RigidBody>,
176        )>,
177        Option<&CollisionLayers>,
178        Option<&ColliderOf>,
179        Has<TnuaGhostPlatform>,
180        Has<Sensor>,
181        Has<TnuaNotPlatform>,
182    )>,
183) {
184    query.par_iter_mut().for_each(
185        |(
186            owner_entity,
187            position,
188            rotation,
189            collider,
190            mut sensor,
191            shape,
192            mut ghost_sensor,
193            subservient,
194            tnua_toggle,
195        )| {
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 owner_entity = if let Some(subservient) = subservient {
224                subservient.owner_entity
225            } else {
226                owner_entity
227            };
228
229            let collision_layers = collision_layers_query.get(owner_entity).ok();
230
231            let mut final_sensor_output = None;
232            if let Some(ghost_sensor) = ghost_sensor.as_mut() {
233                ghost_sensor.0.clear();
234            }
235            let mut apply_cast = |cast_result: CastResult| {
236                let CastResult {
237                    entity,
238                    proximity,
239                    intersection_point,
240                    normal,
241                } = cast_result;
242
243                let Ok((
244                    mut entity_kinematic_data,
245                    mut entity_collision_layers,
246                    entity_collider_of,
247                    mut entity_is_ghost,
248                    mut entity_is_sensor,
249                    mut entity_is_not_platform,
250                )) = other_object_query.get(entity)
251                else {
252                    return false;
253                };
254
255                if let Some(collider_of) = entity_collider_of {
256                    let parent_entity = collider_of.body;
257
258                    // Collider is child of our rigid body. ignore.
259                    if parent_entity == owner_entity {
260                        return true;
261                    }
262
263                    if let Ok((
264                        parent_kinematic_data,
265                        parent_collision_layers,
266                        _,
267                        parent_is_ghost,
268                        parent_is_sensor,
269                        parent_is_not_platform,
270                    )) = other_object_query.get(parent_entity)
271                    {
272                        if entity_kinematic_data.is_none() {
273                            entity_kinematic_data = parent_kinematic_data;
274                        }
275                        if entity_collision_layers.is_none() {
276                            entity_collision_layers = parent_collision_layers;
277                        }
278                        entity_is_ghost = entity_is_ghost || parent_is_ghost;
279                        entity_is_sensor = entity_is_sensor || parent_is_sensor;
280                        entity_is_not_platform = entity_is_not_platform || parent_is_not_platform;
281                    }
282                }
283
284                if entity_is_not_platform {
285                    return true;
286                }
287
288                let entity_linvel;
289                let entity_angvel;
290                if let Some((
291                    entity_position,
292                    entity_linear_velocity,
293                    entity_angular_velocity,
294                    rigid_body,
295                )) = entity_kinematic_data
296                {
297                    if rigid_body == Some(&RigidBody::Static) {
298                        entity_angvel = Vector3::ZERO;
299                        entity_linvel = Vector3::ZERO;
300                    } else {
301                        entity_angvel = entity_angular_velocity.0.adjust_precision();
302                        entity_linvel = entity_linear_velocity.0.adjust_precision()
303                            + if 0.0 < entity_angvel.length_squared() {
304                                let relative_point =
305                                    intersection_point - entity_position.adjust_precision();
306                                // NOTE: no need to project relative_point on the
307                                // rotation plane, it will not affect the cross
308                                // product.
309                                entity_angvel.cross(relative_point)
310                            } else {
311                                Vector3::ZERO
312                            };
313                    }
314                } else {
315                    entity_angvel = Vector3::ZERO;
316                    entity_linvel = Vector3::ZERO;
317                }
318                let sensor_output = TnuaProximitySensorOutput {
319                    entity,
320                    proximity,
321                    normal,
322                    entity_linvel,
323                    entity_angvel,
324                };
325
326                let excluded_by_collision_layers = || {
327                    let collision_layers = collision_layers.copied().unwrap_or_default();
328                    let entity_collision_layers =
329                        entity_collision_layers.copied().unwrap_or_default();
330                    !collision_layers.interacts_with(entity_collision_layers)
331                };
332
333                if entity_is_ghost {
334                    if let Some(ghost_sensor) = ghost_sensor.as_mut() {
335                        ghost_sensor.0.push(sensor_output);
336                    }
337                    true
338                } else if entity_is_sensor || excluded_by_collision_layers() {
339                    true
340                } else {
341                    final_sensor_output = Some(sensor_output);
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                        .dot(cast_direction.adjust_precision()),
355                );
356                spatial_query_pipeline.shape_hits_callback(
357                    shape,
358                    cast_origin,
359                    owner_rotation.mul_quat(sensor.cast_shape_rotation.adjust_precision()),
360                    cast_direction,
361                    &ShapeCastConfig {
362                        max_distance: sensor.cast_range,
363                        ignore_origin_penetration: true,
364                        ..default()
365                    },
366                    &query_filter,
367                    |shape_hit_data| {
368                        apply_cast(CastResult {
369                            entity: shape_hit_data.entity,
370                            proximity: shape_hit_data.distance,
371                            intersection_point: shape_hit_data.point1,
372                            normal: Dir3::new(shape_hit_data.normal1.f32())
373                                .unwrap_or_else(|_| -cast_direction),
374                        })
375                    },
376                );
377            } else {
378                spatial_query_pipeline.ray_hits_callback(
379                    cast_origin,
380                    cast_direction,
381                    sensor.cast_range,
382                    true,
383                    &query_filter,
384                    |ray_hit_data| {
385                        apply_cast(CastResult {
386                            entity: ray_hit_data.entity,
387                            proximity: ray_hit_data.distance,
388                            intersection_point: cast_origin
389                                + ray_hit_data.distance * cast_direction.adjust_precision(),
390                            normal: Dir3::new(ray_hit_data.normal.f32())
391                                .unwrap_or_else(|_| -cast_direction),
392                        })
393                    },
394                );
395            }
396            sensor.output = final_sensor_output;
397        },
398    );
399}
400
401fn update_obstacle_radars_system(
402    spatial_query_pipeline: Res<SpatialQueryPipeline>,
403    gravity: Res<Gravity>,
404    mut radars_query: Query<(Entity, &mut TnuaObstacleRadar, &Position)>,
405) {
406    if radars_query.is_empty() {
407        return;
408    }
409    for (radar_owner_entity, mut radar, radar_position) in radars_query.iter_mut() {
410        radar.pre_marking_update(
411            radar_owner_entity,
412            radar_position.0,
413            Dir3::new(gravity.0.f32()).unwrap_or(Dir3::Y),
414        );
415        spatial_query_pipeline.shape_intersections_callback(
416            &Collider::cylinder(radar.radius, radar.height),
417            radar_position.0,
418            Default::default(),
419            &SpatialQueryFilter::DEFAULT,
420            |obstacle_entity| {
421                if radar_owner_entity == obstacle_entity {
422                    return true;
423                }
424                radar.mark_seen(obstacle_entity);
425                true
426            },
427        );
428    }
429}
430
431#[allow(clippy::type_complexity)]
432fn apply_motors_system(
433    mut query: Query<(
434        &TnuaMotor,
435        &ComputedMass,
436        &ComputedAngularInertia,
437        Forces,
438        Option<&TnuaToggle>,
439        Option<&TnuaGravity>,
440    )>,
441) {
442    for (motor, mass, inertia, mut forces, tnua_toggle, tnua_gravity) in query.iter_mut() {
443        match tnua_toggle.copied().unwrap_or_default() {
444            TnuaToggle::Disabled | TnuaToggle::SenseOnly => {
445                // *external_force = Default::default();
446                return;
447            }
448            TnuaToggle::Enabled => {}
449        }
450        if motor.lin.boost.is_finite() {
451            forces.apply_linear_impulse(motor.lin.boost * mass.value());
452        }
453        if motor.lin.acceleration.is_finite() {
454            forces.apply_linear_acceleration(motor.lin.acceleration);
455        }
456        if motor.ang.boost.is_finite() {
457            forces.apply_angular_impulse(inertia.value() * motor.ang.boost);
458        }
459        if motor.ang.acceleration.is_finite() {
460            // NOTE: I did not actually verify that this is correct. Nothing uses angular
461            // acceleration yet - only angular impulses.
462            forces.apply_angular_acceleration(motor.ang.acceleration);
463        }
464        if let Some(gravity) = tnua_gravity {
465            forces.apply_force(gravity.0 * mass.value());
466        }
467    }
468}