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};
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
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 )
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 .insert_if_new(ColliderOf { body: child_of.0 });
110 }
111}
112
113#[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 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 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 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 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 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 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}