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