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::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
35pub 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 .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#[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 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 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 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 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 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 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}