1mod 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};
18use ordered_float::OrderedFloat;
19pub use spatial_ext::TnuaSpatialExtAvian3d;
20
21use bevy_tnua_physics_integration_layer::TnuaPipelineSystems;
22use bevy_tnua_physics_integration_layer::TnuaSystems;
23use bevy_tnua_physics_integration_layer::data_for_backends::TnuaGravity;
24use bevy_tnua_physics_integration_layer::data_for_backends::TnuaToggle;
25use bevy_tnua_physics_integration_layer::data_for_backends::{TnuaGhostPlatform, TnuaNotPlatform};
26use bevy_tnua_physics_integration_layer::data_for_backends::{TnuaGhostSensor, TnuaSensorOf};
27use bevy_tnua_physics_integration_layer::data_for_backends::{
28 TnuaMotor, TnuaProximitySensor, TnuaProximitySensorOutput, TnuaRigidBodyTracker,
29};
30use bevy_tnua_physics_integration_layer::obstacle_radar::TnuaObstacleRadar;
31
32pub mod prelude {
33 pub use crate::{TnuaAvian3dPlugin, TnuaAvian3dSensorShape, TnuaSpatialExtAvian3d};
34}
35
36pub 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 .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 .ambiguous_with(update_proximity_sensors_system),
83 )
84 .in_set(TnuaPipelineSystems::Sensors),
85 );
86 app.add_systems(
87 self.schedule,
88 apply_motors_system.in_set(TnuaPipelineSystems::Motors),
89 );
90 app.register_required_components_with::<TnuaGravity, GravityScale>(|| GravityScale(0.0));
91 }
92}
93
94#[derive(Component)]
96pub struct TnuaAvian3dSensorShape(pub Collider);
97
98#[allow(clippy::type_complexity)]
99fn update_rigid_body_trackers_system(
100 gravity: Res<Gravity>,
101 mut query: Query<(
102 &Position,
103 &Rotation,
104 &LinearVelocity,
105 &AngularVelocity,
106 &mut TnuaRigidBodyTracker,
107 Option<&TnuaToggle>,
108 Option<&TnuaGravity>,
109 )>,
110) {
111 for (
112 position,
113 rotation,
114 linaer_velocity,
115 angular_velocity,
116 mut tracker,
117 tnua_toggle,
118 tnua_gravity,
119 ) in query.iter_mut()
120 {
121 match tnua_toggle.copied().unwrap_or_default() {
122 TnuaToggle::Disabled => continue,
123 TnuaToggle::SenseOnly => {}
124 TnuaToggle::Enabled => {}
125 }
126 *tracker = TnuaRigidBodyTracker {
127 translation: position.adjust_precision(),
128 rotation: rotation.adjust_precision(),
129 velocity: linaer_velocity.0.adjust_precision(),
130 angvel: angular_velocity.0.adjust_precision(),
131 gravity: tnua_gravity.map(|g| g.0).unwrap_or(gravity.0),
132 };
133 }
134}
135
136#[allow(clippy::type_complexity)]
137fn update_proximity_sensors_system(
138 spatial_query: SpatialQuery,
139 mut sensor_query: Query<(
140 &mut TnuaProximitySensor,
141 &TnuaSensorOf,
142 Option<&TnuaAvian3dSensorShape>,
143 Option<&mut TnuaGhostSensor>,
144 )>,
145 owner_query: Query<(
146 &Position,
147 &Rotation,
148 Option<&Collider>,
149 Option<&TnuaAvian3dSensorShape>,
150 Option<&TnuaToggle>,
151 )>,
152 collision_layers_query: Query<&CollisionLayers>,
153 other_object_query: Query<(
154 Option<(
155 &Position,
156 &LinearVelocity,
157 &AngularVelocity,
158 Option<&RigidBody>,
159 )>,
160 Option<&CollisionLayers>,
161 Option<&ColliderOf>,
162 Has<TnuaGhostPlatform>,
163 Has<Sensor>,
164 Has<TnuaNotPlatform>,
165 )>,
166) {
167 sensor_query.par_iter_mut().for_each(
168 |(mut sensor, &TnuaSensorOf(owner_entity), shape, mut ghost_sensor)| {
169 let Ok((position, rotation, collider, owner_shape, tnua_toggle)) =
170 owner_query.get(owner_entity)
171 else {
172 return;
173 };
174 let shape = shape.or(owner_shape);
175 match tnua_toggle.copied().unwrap_or_default() {
176 TnuaToggle::Disabled => return,
177 TnuaToggle::SenseOnly => {}
178 TnuaToggle::Enabled => {}
179 }
180 let transform = Transform {
181 translation: position.0.f32(),
182 rotation: rotation.0.f32(),
183 scale: collider
184 .map(|collider| collider.scale().f32())
185 .unwrap_or(Vec3::ONE),
186 };
187
188 let cast_origin = transform
191 .transform_point(sensor.cast_origin.f32())
192 .adjust_precision();
193 let cast_direction = sensor.cast_direction;
194
195 struct CastResult {
196 entity: Entity,
197 proximity: Float,
198 intersection_point: Vector3,
199 normal: Dir3,
200 }
201
202 let collision_layers = collision_layers_query.get(owner_entity).ok();
203
204 let mut final_sensor_output: Option<TnuaProximitySensorOutput> = None;
205 if let Some(ghost_sensor) = ghost_sensor.as_mut() {
206 ghost_sensor.0.clear();
207 }
208 let mut apply_cast = |cast_result: CastResult| {
209 let CastResult {
210 entity,
211 proximity,
212 intersection_point,
213 normal,
214 } = cast_result;
215
216 let Ok((
217 mut entity_kinematic_data,
218 mut entity_collision_layers,
219 entity_collider_of,
220 mut entity_is_ghost,
221 mut entity_is_sensor,
222 mut entity_is_not_platform,
223 )) = other_object_query.get(entity)
224 else {
225 return false;
226 };
227
228 if let Some(collider_of) = entity_collider_of {
229 let parent_entity = collider_of.body;
230
231 if parent_entity == owner_entity {
233 return true;
234 }
235
236 if let Ok((
237 parent_kinematic_data,
238 parent_collision_layers,
239 _,
240 parent_is_ghost,
241 parent_is_sensor,
242 parent_is_not_platform,
243 )) = other_object_query.get(parent_entity)
244 {
245 if entity_kinematic_data.is_none() {
246 entity_kinematic_data = parent_kinematic_data;
247 }
248 if entity_collision_layers.is_none() {
249 entity_collision_layers = parent_collision_layers;
250 }
251 entity_is_ghost = entity_is_ghost || parent_is_ghost;
252 entity_is_sensor = entity_is_sensor || parent_is_sensor;
253 entity_is_not_platform = entity_is_not_platform || parent_is_not_platform;
254 }
255 }
256
257 if entity_is_not_platform {
258 return true;
259 }
260
261 let entity_linvel;
262 let entity_angvel;
263 if let Some((
264 entity_position,
265 entity_linear_velocity,
266 entity_angular_velocity,
267 rigid_body,
268 )) = entity_kinematic_data
269 {
270 if rigid_body == Some(&RigidBody::Static) {
271 entity_angvel = Vector3::ZERO;
272 entity_linvel = Vector3::ZERO;
273 } else {
274 entity_angvel = entity_angular_velocity.0.adjust_precision();
275 entity_linvel = entity_linear_velocity.0.adjust_precision()
276 + if 0.0 < entity_angvel.length_squared() {
277 let relative_point =
278 intersection_point - entity_position.adjust_precision();
279 entity_angvel.cross(relative_point)
283 } else {
284 Vector3::ZERO
285 };
286 }
287 } else {
288 entity_angvel = Vector3::ZERO;
289 entity_linvel = Vector3::ZERO;
290 }
291 let sensor_output = TnuaProximitySensorOutput {
292 entity,
293 proximity,
294 normal,
295 entity_linvel,
296 entity_angvel,
297 };
298
299 let excluded_by_collision_layers = || {
300 let collision_layers = collision_layers.copied().unwrap_or_default();
301 let entity_collision_layers =
302 entity_collision_layers.copied().unwrap_or_default();
303 !collision_layers.interacts_with(entity_collision_layers)
304 };
305
306 if entity_is_ghost {
307 if let Some(ghost_sensor) = ghost_sensor.as_mut() {
308 ghost_sensor.0.push(sensor_output);
309 }
310 true
311 } else if entity_is_sensor || excluded_by_collision_layers() {
312 true
313 } else {
314 if final_sensor_output.as_ref().is_none_or(|current_output| {
315 sensor_output.proximity < current_output.proximity
316 }) {
317 final_sensor_output = Some(sensor_output);
320 }
321 false
322 }
323 };
324
325 let query_filter = SpatialQueryFilter::from_excluded_entities([owner_entity]);
326 if let Some(TnuaAvian3dSensorShape(shape)) = shape {
327 let owner_rotation = Quaternion::from_axis_angle(
330 cast_direction.adjust_precision(),
331 rotation
332 .to_scaled_axis()
333 .dot(cast_direction.adjust_precision()),
334 );
335 spatial_query.shape_hits_callback(
336 shape,
337 cast_origin,
338 owner_rotation.mul_quat(sensor.cast_shape_rotation.adjust_precision()),
339 cast_direction,
340 &ShapeCastConfig {
341 max_distance: sensor.cast_range,
342 ignore_origin_penetration: true,
343 ..default()
344 },
345 &query_filter,
346 |shape_hit_data| {
347 apply_cast(CastResult {
348 entity: shape_hit_data.entity,
349 proximity: shape_hit_data.distance,
350 intersection_point: shape_hit_data.point1,
351 normal: Dir3::new(shape_hit_data.normal1.f32())
352 .unwrap_or_else(|_| -cast_direction),
353 })
354 },
355 );
356 } else {
357 spatial_query.ray_hits_callback(
358 cast_origin,
359 cast_direction,
360 sensor.cast_range,
361 true,
362 &query_filter,
363 |ray_hit_data| {
364 apply_cast(CastResult {
365 entity: ray_hit_data.entity,
366 proximity: ray_hit_data.distance,
367 intersection_point: cast_origin
368 + ray_hit_data.distance * cast_direction.adjust_precision(),
369 normal: Dir3::new(ray_hit_data.normal.f32())
370 .unwrap_or_else(|_| -cast_direction),
371 })
372 },
373 );
374 }
375 if let Some(ghost_sensor) = ghost_sensor.as_mut() {
376 if let Some(final_sensor_output) = final_sensor_output.as_ref() {
379 ghost_sensor
380 .0
381 .retain(|ghost_hit| ghost_hit.proximity < final_sensor_output.proximity);
382 }
383 ghost_sensor
384 .0
385 .sort_by_key(|ghost_hit| OrderedFloat(ghost_hit.proximity));
386 }
387 sensor.output = final_sensor_output;
388 },
389 );
390}
391
392fn update_obstacle_radars_system(
393 spatial_query: SpatialQuery,
394 gravity: Res<Gravity>,
395 mut radars_query: Query<(Entity, &mut TnuaObstacleRadar, &Position)>,
396) {
397 if radars_query.is_empty() {
398 return;
399 }
400 for (radar_owner_entity, mut radar, radar_position) in radars_query.iter_mut() {
401 radar.pre_marking_update(
402 radar_owner_entity,
403 radar_position.0,
404 Dir3::new(gravity.0.f32()).unwrap_or(Dir3::Y),
405 );
406 spatial_query.shape_intersections_callback(
407 &Collider::cylinder(radar.radius, radar.height),
408 radar_position.0,
409 Default::default(),
410 &SpatialQueryFilter::DEFAULT,
411 |obstacle_entity| {
412 if radar_owner_entity == obstacle_entity {
413 return true;
414 }
415 radar.mark_seen(obstacle_entity);
416 true
417 },
418 );
419 }
420}
421
422#[allow(clippy::type_complexity)]
423fn apply_motors_system(
424 mut query: Query<(
425 &TnuaMotor,
426 &ComputedMass,
427 &ComputedAngularInertia,
428 Forces,
429 Option<&TnuaToggle>,
430 Option<&TnuaGravity>,
431 )>,
432) {
433 for (motor, mass, inertia, mut forces, tnua_toggle, tnua_gravity) in query.iter_mut() {
434 match tnua_toggle.copied().unwrap_or_default() {
435 TnuaToggle::Disabled | TnuaToggle::SenseOnly => {
436 return;
438 }
439 TnuaToggle::Enabled => {}
440 }
441 if motor.lin.boost.is_finite() {
442 forces.apply_linear_impulse(motor.lin.boost * mass.value());
443 }
444 if motor.lin.acceleration.is_finite() {
445 forces.apply_linear_acceleration(motor.lin.acceleration);
446 }
447 if motor.ang.boost.is_finite() {
448 forces.apply_angular_impulse(inertia.value() * motor.ang.boost);
449 }
450 if motor.ang.acceleration.is_finite() {
451 forces.apply_angular_acceleration(motor.ang.acceleration);
454 }
455 if let Some(gravity) = tnua_gravity {
456 forces.apply_force(gravity.0 * mass.value());
457 }
458 }
459}