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