1mod 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
32pub 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 .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 .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#[derive(Component)]
92pub struct TnuaAvian3dSensorShape(pub Collider);
93
94impl FromTemplate for TnuaAvian3dSensorShape {
95 type Template = TnuaAvian3dSensorShapeTemplate;
96}
97
98#[derive(Default)]
99pub struct TnuaAvian3dSensorShapeTemplate(pub Option<Collider>);
100
101impl Template for TnuaAvian3dSensorShapeTemplate {
102 type Output = TnuaAvian3dSensorShape;
103
104 fn build_template(&self, _: &mut bevy::ecs::template::TemplateContext) -> Result<Self::Output> {
105 Ok(TnuaAvian3dSensorShape(self.0.clone().ok_or_else(|| {
106 BevyError::error(
107 "TnuaAvian3dSensorShape does not have a default value. Must provide shape",
108 )
109 })?))
110 }
111
112 fn clone_template(&self) -> Self {
113 Self(self.0.clone())
114 }
115}
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
151 .map(|g| g.0)
152 .unwrap_or(gravity.0.adjust_precision()),
153 };
154 }
155}
156
157#[allow(clippy::type_complexity)]
158fn update_proximity_sensors_system(
159 spatial_query: SpatialQuery,
160 mut sensor_query: Query<(
161 &mut TnuaProximitySensor,
162 &TnuaSensorOf,
163 Option<&TnuaAvian3dSensorShape>,
164 Option<&mut TnuaGhostSensor>,
165 )>,
166 owner_query: Query<(
167 &Position,
168 &Rotation,
169 Option<&Collider>,
170 Option<&TnuaAvian3dSensorShape>,
171 Option<&TnuaToggle>,
172 )>,
173 collision_layers_query: Query<&CollisionLayers>,
174 other_object_query: Query<(
175 Option<(
176 &Position,
177 &LinearVelocity,
178 &AngularVelocity,
179 Option<&RigidBody>,
180 )>,
181 Option<&CollisionLayers>,
182 Option<&ColliderOf>,
183 Has<TnuaGhostPlatform>,
184 Has<Sensor>,
185 Has<TnuaNotPlatform>,
186 )>,
187) {
188 sensor_query.par_iter_mut().for_each(
189 |(mut sensor, &TnuaSensorOf(owner_entity), shape, mut ghost_sensor)| {
190 let Ok((position, rotation, collider, owner_shape, tnua_toggle)) =
191 owner_query.get(owner_entity)
192 else {
193 return;
194 };
195 let shape = shape.or(owner_shape);
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 collision_layers = collision_layers_query.get(owner_entity).ok();
224
225 let mut final_sensor_output: Option<TnuaProximitySensorOutput> = None;
226 if let Some(ghost_sensor) = ghost_sensor.as_mut() {
227 ghost_sensor.0.clear();
228 }
229 let mut apply_cast = |cast_result: CastResult| {
230 let CastResult {
231 entity,
232 proximity,
233 intersection_point,
234 normal,
235 } = cast_result;
236
237 let Ok((
238 mut entity_kinematic_data,
239 mut entity_collision_layers,
240 entity_collider_of,
241 mut entity_is_ghost,
242 mut entity_is_sensor,
243 mut entity_is_not_platform,
244 )) = other_object_query.get(entity)
245 else {
246 return false;
247 };
248
249 if let Some(collider_of) = entity_collider_of {
250 let parent_entity = collider_of.body;
251
252 if parent_entity == owner_entity {
254 return true;
255 }
256
257 if let Ok((
258 parent_kinematic_data,
259 parent_collision_layers,
260 _,
261 parent_is_ghost,
262 parent_is_sensor,
263 parent_is_not_platform,
264 )) = other_object_query.get(parent_entity)
265 {
266 if entity_kinematic_data.is_none() {
267 entity_kinematic_data = parent_kinematic_data;
268 }
269 if entity_collision_layers.is_none() {
270 entity_collision_layers = parent_collision_layers;
271 }
272 entity_is_ghost = entity_is_ghost || parent_is_ghost;
273 entity_is_sensor = entity_is_sensor || parent_is_sensor;
274 entity_is_not_platform = entity_is_not_platform || parent_is_not_platform;
275 }
276 }
277
278 if entity_is_not_platform {
279 return true;
280 }
281
282 let entity_linvel;
283 let entity_angvel;
284 if let Some((
285 entity_position,
286 entity_linear_velocity,
287 entity_angular_velocity,
288 rigid_body,
289 )) = entity_kinematic_data
290 {
291 if rigid_body == Some(&RigidBody::Static) {
292 entity_angvel = Vector3::ZERO;
293 entity_linvel = Vector3::ZERO;
294 } else {
295 entity_angvel = entity_angular_velocity.0.adjust_precision();
296 entity_linvel = entity_linear_velocity.0.adjust_precision()
297 + if 0.0 < entity_angvel.length_squared() {
298 let relative_point =
299 intersection_point - entity_position.adjust_precision();
300 entity_angvel.cross(relative_point)
304 } else {
305 Vector3::ZERO
306 };
307 }
308 } else {
309 entity_angvel = Vector3::ZERO;
310 entity_linvel = Vector3::ZERO;
311 }
312 let sensor_output = TnuaProximitySensorOutput {
313 entity,
314 proximity,
315 normal,
316 entity_linvel,
317 entity_angvel,
318 };
319
320 let excluded_by_collision_layers = || {
321 let collision_layers = collision_layers.copied().unwrap_or_default();
322 let entity_collision_layers =
323 entity_collision_layers.copied().unwrap_or_default();
324 !collision_layers.interacts_with(entity_collision_layers)
325 };
326
327 if entity_is_ghost {
328 if let Some(ghost_sensor) = ghost_sensor.as_mut() {
329 ghost_sensor.0.push(sensor_output);
330 }
331 true
332 } else if entity_is_sensor || excluded_by_collision_layers() {
333 true
334 } else {
335 if final_sensor_output.as_ref().is_none_or(|current_output| {
336 sensor_output.proximity < current_output.proximity
337 }) {
338 final_sensor_output = Some(sensor_output);
341 }
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 .adjust_precision()
355 .dot(cast_direction.adjust_precision()),
356 );
357 spatial_query.shape_hits_callback(
358 shape,
359 cast_origin,
360 owner_rotation
361 .mul_quat(sensor.cast_shape_rotation.adjust_precision())
362 .f32(),
363 cast_direction,
364 &ShapeCastConfig {
365 max_distance: sensor.cast_range.f32(),
366 ignore_origin_penetration: true,
367 ..default()
368 },
369 &query_filter,
370 |shape_hit_data| {
371 apply_cast(CastResult {
372 entity: shape_hit_data.entity,
373 proximity: shape_hit_data.distance.adjust_precision(),
374 intersection_point: shape_hit_data.point1,
375 normal: Dir3::new(shape_hit_data.normal1.f32())
376 .unwrap_or_else(|_| -cast_direction),
377 })
378 },
379 );
380 } else {
381 spatial_query.ray_hits_callback(
382 cast_origin,
383 cast_direction,
384 sensor.cast_range.f32(),
385 true,
386 &query_filter,
387 |ray_hit_data| {
388 apply_cast(CastResult {
389 entity: ray_hit_data.entity,
390 proximity: ray_hit_data.distance.adjust_precision(),
391 intersection_point: cast_origin
392 + ray_hit_data.distance.adjust_precision()
393 * cast_direction.adjust_precision(),
394 normal: Dir3::new(ray_hit_data.normal.f32())
395 .unwrap_or_else(|_| -cast_direction),
396 })
397 },
398 );
399 }
400 if let Some(ghost_sensor) = ghost_sensor.as_mut() {
401 if let Some(final_sensor_output) = final_sensor_output.as_ref() {
404 ghost_sensor
405 .0
406 .retain(|ghost_hit| ghost_hit.proximity < final_sensor_output.proximity);
407 }
408 ghost_sensor
409 .0
410 .sort_by_key(|ghost_hit| OrderedFloat(ghost_hit.proximity));
411 }
412 sensor.output = final_sensor_output;
413 },
414 );
415}
416
417fn update_obstacle_radars_system(
418 spatial_query: SpatialQuery,
419 gravity: Res<Gravity>,
420 mut radars_query: Query<(Entity, &mut TnuaObstacleRadar, &Position)>,
421) {
422 if radars_query.is_empty() {
423 return;
424 }
425 for (radar_owner_entity, mut radar, radar_position) in radars_query.iter_mut() {
426 radar.pre_marking_update(
427 radar_owner_entity,
428 radar_position.0,
429 Dir3::new(gravity.0.f32()).unwrap_or(Dir3::Y),
430 );
431 spatial_query.shape_intersections_callback(
432 &Collider::cylinder(radar.radius.f32(), radar.height.f32()),
433 radar_position.0,
434 Rotation::default(),
435 &SpatialQueryFilter::DEFAULT,
436 |obstacle_entity| {
437 if radar_owner_entity == obstacle_entity {
438 return true;
439 }
440 radar.mark_seen(obstacle_entity);
441 true
442 },
443 );
444 }
445}
446
447#[allow(clippy::type_complexity)]
448fn apply_motors_system(
449 mut query: Query<(
450 &TnuaMotor,
451 Forces,
452 Option<&TnuaToggle>,
453 Option<&TnuaGravity>,
454 )>,
455) {
456 for (motor, mut forces, tnua_toggle, tnua_gravity) in query.iter_mut() {
457 match tnua_toggle.copied().unwrap_or_default() {
458 TnuaToggle::Disabled | TnuaToggle::SenseOnly => {
459 return;
460 }
461 TnuaToggle::Enabled => {}
462 }
463
464 if motor.lin.boost.is_finite() {
465 *forces.linear_velocity_mut() += motor.lin.boost.f32();
466 }
467 if motor.lin.acceleration.is_finite() {
468 forces.apply_linear_acceleration(motor.lin.acceleration.f32());
469 }
470 if motor.ang.boost.is_finite() {
471 *forces.angular_velocity_mut() += motor.ang.boost.f32();
472 }
473 if motor.ang.acceleration.is_finite() {
474 forces.apply_torque(motor.ang.acceleration.f32());
475 }
476 if let Some(gravity) = tnua_gravity {
477 forces.apply_linear_acceleration(gravity.0.f32());
478 }
479 }
480}