avian3d/spatial_query/shape_caster.rs
1use crate::prelude::*;
2use bevy::{
3 ecs::{
4 component::HookContext,
5 entity::{EntityMapper, MapEntities},
6 world::DeferredWorld,
7 },
8 prelude::*,
9};
10use parry::query::{details::TOICompositeShapeShapeBestFirstVisitor, ShapeCastOptions};
11
12/// A component used for [shapecasting](spatial_query#shapecasting).
13///
14/// **Shapecasting** is a type of [spatial query](spatial_query) where a shape travels along a straight
15/// line and computes hits with colliders. This is often used to determine how far an object can move
16/// in a direction before it hits something.
17///
18/// Each shapecast is defined by a `shape` (a [`Collider`]), its local `shape_rotation`, a local `origin` and
19/// a local `direction`. The [`ShapeCaster`] will find each hit and add them to the [`ShapeHits`] component in
20/// the order of distance.
21///
22/// Computing lots of hits can be expensive, especially against complex geometry, so the maximum number of hits
23/// is one by default. This can be configured through the `max_hits` property.
24///
25/// The [`ShapeCaster`] is the easiest way to handle simple shapecasting. If you want more control and don't want
26/// to perform shapecasts on every frame, consider using the [`SpatialQuery`] system parameter.
27///
28/// # Example
29///
30/// ```
31/// # #[cfg(feature = "2d")]
32/// # use avian2d::prelude::*;
33/// # #[cfg(feature = "3d")]
34/// use avian3d::prelude::*;
35/// use bevy::prelude::*;
36///
37/// # #[cfg(all(feature = "3d", feature = "f32"))]
38/// fn setup(mut commands: Commands) {
39/// // Spawn a shape caster with a ball shape moving right starting from the origin
40/// commands.spawn(ShapeCaster::new(
41#[cfg_attr(feature = "2d", doc = " Collider::circle(0.5),")]
42#[cfg_attr(feature = "3d", doc = " Collider::sphere(0.5),")]
43/// Vec3::ZERO,
44/// Quat::default(),
45/// Dir3::X,
46/// ));
47/// }
48///
49/// fn print_hits(query: Query<(&ShapeCaster, &ShapeHits)>) {
50/// for (shape_caster, hits) in &query {
51/// for hit in hits.iter() {
52/// println!("Hit entity {}", hit.entity);
53/// }
54/// }
55/// }
56/// ```
57#[derive(Component, Clone, Debug, Reflect)]
58#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
59#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
60#[reflect(Debug, Component)]
61#[component(on_add = on_add_shape_caster)]
62#[require(ShapeHits)]
63pub struct ShapeCaster {
64 /// Controls if the shape caster is enabled.
65 pub enabled: bool,
66
67 /// The shape being cast represented as a [`Collider`].
68 #[reflect(ignore)]
69 pub shape: Collider,
70
71 /// The local origin of the shape relative to the [`Position`] and [`Rotation`]
72 /// of the shape caster entity or its parent.
73 ///
74 /// To get the global origin, use the `global_origin` method.
75 pub origin: Vector,
76
77 /// The global origin of the shape.
78 global_origin: Vector,
79
80 /// The local rotation of the shape being cast relative to the [`Rotation`]
81 /// of the shape caster entity or its parent. Expressed in radians.
82 ///
83 /// To get the global shape rotation, use the `global_shape_rotation` method.
84 #[cfg(feature = "2d")]
85 pub shape_rotation: Scalar,
86
87 /// The local rotation of the shape being cast relative to the [`Rotation`]
88 /// of the shape caster entity or its parent.
89 ///
90 /// To get the global shape rotation, use the `global_shape_rotation` method.
91 #[cfg(feature = "3d")]
92 pub shape_rotation: Quaternion,
93
94 /// The global rotation of the shape.
95 #[cfg(feature = "2d")]
96 global_shape_rotation: Scalar,
97
98 /// The global rotation of the shape.
99 #[cfg(feature = "3d")]
100 global_shape_rotation: Quaternion,
101
102 /// The local direction of the shapecast relative to the [`Rotation`] of the shape caster entity or its parent.
103 ///
104 /// To get the global direction, use the `global_direction` method.
105 pub direction: Dir,
106
107 /// The global direction of the shapecast.
108 global_direction: Dir,
109
110 /// The maximum number of hits allowed. By default this is one and only the first hit is returned.
111 pub max_hits: u32,
112
113 /// The maximum distance the shape can travel.
114 ///
115 /// By default, this is infinite.
116 #[doc(alias = "max_time_of_impact")]
117 pub max_distance: Scalar,
118
119 /// The separation distance at which the shapes will be considered as impacting.
120 ///
121 /// If the shapes are separated by a distance smaller than `target_distance` at the origin of the cast,
122 /// the computed contact points and normals are only reliable if [`ShapeCaster::compute_contact_on_penetration`]
123 /// is set to `true`.
124 ///
125 /// By default, this is `0.0`, so the shapes will only be considered as impacting when they first touch.
126 pub target_distance: Scalar,
127
128 /// If `true`, contact points and normals will be calculated even when the cast distance is `0.0`.
129 ///
130 /// The default is `true`.
131 pub compute_contact_on_penetration: bool,
132
133 /// If `true` *and* the shape is travelling away from the object that was hit,
134 /// the cast will ignore any impact that happens at the cast origin.
135 ///
136 /// The default is `false`.
137 pub ignore_origin_penetration: bool,
138
139 /// If true, the shape caster ignores hits against its own [`Collider`]. This is the default.
140 pub ignore_self: bool,
141
142 /// Rules that determine which colliders are taken into account in the shape cast.
143 pub query_filter: SpatialQueryFilter,
144}
145
146impl Default for ShapeCaster {
147 fn default() -> Self {
148 Self {
149 enabled: true,
150 #[cfg(feature = "2d")]
151 shape: Collider::circle(0.0),
152 #[cfg(feature = "3d")]
153 shape: Collider::sphere(0.0),
154 origin: Vector::ZERO,
155 global_origin: Vector::ZERO,
156 #[cfg(feature = "2d")]
157 shape_rotation: 0.0,
158 #[cfg(feature = "3d")]
159 shape_rotation: Quaternion::IDENTITY,
160 #[cfg(feature = "2d")]
161 global_shape_rotation: 0.0,
162 #[cfg(feature = "3d")]
163 global_shape_rotation: Quaternion::IDENTITY,
164 direction: Dir::X,
165 global_direction: Dir::X,
166 max_hits: 1,
167 max_distance: Scalar::MAX,
168 target_distance: 0.0,
169 compute_contact_on_penetration: true,
170 ignore_origin_penetration: false,
171 ignore_self: true,
172 query_filter: SpatialQueryFilter::default(),
173 }
174 }
175}
176
177impl ShapeCaster {
178 /// Creates a new [`ShapeCaster`] with a given shape, origin, shape rotation and direction.
179 #[cfg(feature = "2d")]
180 pub fn new(
181 shape: impl Into<Collider>,
182 origin: Vector,
183 shape_rotation: Scalar,
184 direction: Dir,
185 ) -> Self {
186 Self {
187 shape: shape.into(),
188 origin,
189 shape_rotation,
190 direction,
191 ..default()
192 }
193 }
194 #[cfg(feature = "3d")]
195 /// Creates a new [`ShapeCaster`] with a given shape, origin, shape rotation and direction.
196 pub fn new(
197 shape: impl Into<Collider>,
198 origin: Vector,
199 shape_rotation: Quaternion,
200 direction: Dir,
201 ) -> Self {
202 Self {
203 shape: shape.into(),
204 origin,
205 shape_rotation,
206 direction,
207 ..default()
208 }
209 }
210
211 /// Sets the ray origin.
212 pub fn with_origin(mut self, origin: Vector) -> Self {
213 self.origin = origin;
214 self
215 }
216
217 /// Sets the ray direction.
218 pub fn with_direction(mut self, direction: Dir) -> Self {
219 self.direction = direction;
220 self
221 }
222
223 /// Sets the separation distance at which the shapes will be considered as impacting.
224 ///
225 /// If the shapes are separated by a distance smaller than `target_distance` at the origin of the cast,
226 /// the computed contact points and normals are only reliable if [`ShapeCaster::compute_contact_on_penetration`]
227 /// is set to `true`.
228 ///
229 /// By default, this is `0.0`, so the shapes will only be considered as impacting when they first touch.
230 pub fn with_target_distance(mut self, target_distance: Scalar) -> Self {
231 self.target_distance = target_distance;
232 self
233 }
234
235 /// Sets if contact points and normals should be calculated even when the cast distance is `0.0`.
236 ///
237 /// The default is `true`.
238 pub fn with_compute_contact_on_penetration(mut self, compute_contact: bool) -> Self {
239 self.compute_contact_on_penetration = compute_contact;
240 self
241 }
242
243 /// Controls how the shapecast behaves when the shape is already penetrating a [collider](Collider)
244 /// at the shape origin.
245 ///
246 /// If set to `true` **and** the shape is being cast in a direction where it will eventually stop penetrating,
247 /// the shapecast will not stop immediately, and will instead continue until another hit.\
248 /// If set to false, the shapecast will stop immediately and return the hit. This is the default.
249 pub fn with_ignore_origin_penetration(mut self, ignore: bool) -> Self {
250 self.ignore_origin_penetration = ignore;
251 self
252 }
253
254 /// Sets if the shape caster should ignore hits against its own [`Collider`].
255 ///
256 /// The default is `true`.
257 pub fn with_ignore_self(mut self, ignore: bool) -> Self {
258 self.ignore_self = ignore;
259 self
260 }
261
262 /// Sets the maximum distance the shape can travel.
263 pub fn with_max_distance(mut self, max_distance: Scalar) -> Self {
264 self.max_distance = max_distance;
265 self
266 }
267
268 /// Sets the maximum number of allowed hits.
269 pub fn with_max_hits(mut self, max_hits: u32) -> Self {
270 self.max_hits = max_hits;
271 self
272 }
273
274 /// Sets the shape caster's [query filter](SpatialQueryFilter) that controls which colliders
275 /// should be included or excluded by shapecasts.
276 pub fn with_query_filter(mut self, query_filter: SpatialQueryFilter) -> Self {
277 self.query_filter = query_filter;
278 self
279 }
280
281 /// Enables the [`ShapeCaster`].
282 pub fn enable(&mut self) {
283 self.enabled = true;
284 }
285
286 /// Disables the [`ShapeCaster`].
287 pub fn disable(&mut self) {
288 self.enabled = false;
289 }
290
291 /// Returns the global origin of the ray.
292 pub fn global_origin(&self) -> Vector {
293 self.global_origin
294 }
295
296 /// Returns the global rotation of the shape.
297 #[cfg(feature = "2d")]
298 pub fn global_shape_rotation(&self) -> Scalar {
299 self.global_shape_rotation
300 }
301
302 /// Returns the global rotation of the shape.
303 #[cfg(feature = "3d")]
304 pub fn global_shape_rotation(&self) -> Quaternion {
305 self.global_shape_rotation
306 }
307
308 /// Returns the global direction of the ray.
309 pub fn global_direction(&self) -> Dir {
310 self.global_direction
311 }
312
313 /// Sets the global origin of the ray.
314 pub(crate) fn set_global_origin(&mut self, global_origin: Vector) {
315 self.global_origin = global_origin;
316 }
317
318 /// Sets the global rotation of the shape.
319 #[cfg(feature = "2d")]
320 pub(crate) fn set_global_shape_rotation(&mut self, global_rotation: Scalar) {
321 self.global_shape_rotation = global_rotation;
322 }
323
324 /// Sets the global rotation of the shape.
325 #[cfg(feature = "3d")]
326 pub(crate) fn set_global_shape_rotation(&mut self, global_rotation: Quaternion) {
327 self.global_shape_rotation = global_rotation;
328 }
329
330 /// Sets the global direction of the ray.
331 pub(crate) fn set_global_direction(&mut self, global_direction: Dir) {
332 self.global_direction = global_direction;
333 }
334
335 pub(crate) fn cast(
336 &self,
337 caster_entity: Entity,
338 hits: &mut ShapeHits,
339 query_pipeline: &SpatialQueryPipeline,
340 ) {
341 // TODO: This clone is here so that the excluded entities in the original `query_filter` aren't modified.
342 // We could remove this if shapecasting could compute multiple hits without just doing casts in a loop.
343 // See https://github.com/Jondolf/avian/issues/403.
344 let mut query_filter = self.query_filter.clone();
345
346 if self.ignore_self {
347 query_filter.excluded_entities.insert(caster_entity);
348 }
349
350 hits.count = 0;
351
352 let shape_rotation: Rotation;
353 #[cfg(feature = "2d")]
354 {
355 shape_rotation = Rotation::radians(self.global_shape_rotation());
356 }
357 #[cfg(feature = "3d")]
358 {
359 shape_rotation = Rotation::from(self.global_shape_rotation());
360 }
361
362 let shape_isometry = make_isometry(self.global_origin(), shape_rotation);
363 let shape_direction = self.global_direction().adjust_precision().into();
364
365 while hits.count < self.max_hits {
366 let pipeline_shape = query_pipeline.as_composite_shape(&query_filter);
367 let mut visitor = TOICompositeShapeShapeBestFirstVisitor::new(
368 &*query_pipeline.dispatcher,
369 &shape_isometry,
370 &shape_direction,
371 &pipeline_shape,
372 &**self.shape.shape_scaled(),
373 ShapeCastOptions {
374 max_time_of_impact: self.max_distance,
375 stop_at_penetration: !self.ignore_origin_penetration,
376 ..default()
377 },
378 );
379
380 if let Some(hit) =
381 query_pipeline
382 .qbvh
383 .traverse_best_first(&mut visitor)
384 .map(|(_, (index, hit))| ShapeHitData {
385 entity: query_pipeline.proxies[index as usize].entity,
386 distance: hit.time_of_impact,
387 point1: hit.witness1.into(),
388 point2: hit.witness2.into(),
389 normal1: hit.normal1.into(),
390 normal2: hit.normal2.into(),
391 })
392 {
393 if (hits.vector.len() as u32) < hits.count + 1 {
394 hits.vector.push(hit);
395 } else {
396 hits.vector[hits.count as usize] = hit;
397 }
398
399 hits.count += 1;
400 query_filter.excluded_entities.insert(hit.entity);
401 } else {
402 return;
403 }
404 }
405 }
406}
407
408fn on_add_shape_caster(mut world: DeferredWorld, ctx: HookContext) {
409 let shape_caster = world.get::<ShapeCaster>(ctx.entity).unwrap();
410 let max_hits = if shape_caster.max_hits == u32::MAX {
411 10
412 } else {
413 shape_caster.max_hits as usize
414 };
415
416 // Initialize capacity for hits
417 world.get_mut::<ShapeHits>(ctx.entity).unwrap().vector = Vec::with_capacity(max_hits);
418}
419
420/// Configuration for a shape cast.
421#[derive(Clone, Debug, PartialEq, Reflect)]
422#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
423#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
424#[reflect(Debug, PartialEq)]
425pub struct ShapeCastConfig {
426 /// The maximum distance the shape can travel.
427 ///
428 /// By default, this is infinite.
429 #[doc(alias = "max_time_of_impact")]
430 pub max_distance: Scalar,
431
432 /// The separation distance at which the shapes will be considered as impacting.
433 ///
434 /// If the shapes are separated by a distance smaller than `target_distance` at the origin of the cast,
435 /// the computed contact points and normals are only reliable if [`ShapeCastConfig::compute_contact_on_penetration`]
436 /// is set to `true`.
437 ///
438 /// By default, this is `0.0`, so the shapes will only be considered as impacting when they first touch.
439 pub target_distance: Scalar,
440
441 /// If `true`, contact points and normals will be calculated even when the cast distance is `0.0`.
442 ///
443 /// The default is `true`.
444 pub compute_contact_on_penetration: bool,
445
446 /// If `true` *and* the shape is travelling away from the object that was hit,
447 /// the cast will ignore any impact that happens at the cast origin.
448 ///
449 /// The default is `false`.
450 pub ignore_origin_penetration: bool,
451}
452
453impl Default for ShapeCastConfig {
454 fn default() -> Self {
455 Self::DEFAULT
456 }
457}
458
459impl ShapeCastConfig {
460 /// The default [`ShapeCastConfig`] configuration.
461 pub const DEFAULT: Self = Self {
462 max_distance: Scalar::MAX,
463 target_distance: 0.0,
464 compute_contact_on_penetration: true,
465 ignore_origin_penetration: false,
466 };
467
468 /// Creates a new [`ShapeCastConfig`] with a given maximum distance the shape can travel.
469 #[inline]
470 pub const fn from_max_distance(max_distance: Scalar) -> Self {
471 Self {
472 max_distance,
473 target_distance: 0.0,
474 compute_contact_on_penetration: true,
475 ignore_origin_penetration: false,
476 }
477 }
478
479 /// Creates a new [`ShapeCastConfig`] with a given separation distance at which
480 /// the shapes will be considered as impacting.
481 #[inline]
482 pub const fn from_target_distance(target_distance: Scalar) -> Self {
483 Self {
484 max_distance: Scalar::MAX,
485 target_distance,
486 compute_contact_on_penetration: true,
487 ignore_origin_penetration: false,
488 }
489 }
490
491 /// Sets the maximum distance the shape can travel.
492 #[inline]
493 pub const fn with_max_distance(mut self, max_distance: Scalar) -> Self {
494 self.max_distance = max_distance;
495 self
496 }
497
498 /// Sets the separation distance at which the shapes will be considered as impacting.
499 #[inline]
500 pub const fn with_target_distance(mut self, target_distance: Scalar) -> Self {
501 self.target_distance = target_distance;
502 self
503 }
504}
505
506/// Contains the hits of a shape cast by a [`ShapeCaster`]. The hits are in the order of distance.
507///
508/// The maximum number of hits depends on the value of `max_hits` in [`ShapeCaster`]. By default only
509/// one hit is computed, as shapecasting for many results can be expensive.
510///
511/// # Example
512///
513/// ```
514/// # #[cfg(feature = "2d")]
515/// # use avian2d::prelude::*;
516/// # #[cfg(feature = "3d")]
517/// use avian3d::prelude::*;
518/// use bevy::prelude::*;
519///
520/// fn print_hits(query: Query<&ShapeHits, With<ShapeCaster>>) {
521/// for hits in &query {
522/// for hit in hits.iter() {
523/// println!("Hit entity {} with distance {}", hit.entity, hit.distance);
524/// }
525/// }
526/// }
527/// ```
528#[derive(Component, Clone, Debug, Default, Reflect, PartialEq)]
529#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
530#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
531#[reflect(Debug, Component, PartialEq)]
532pub struct ShapeHits {
533 pub(crate) vector: Vec<ShapeHitData>,
534 pub(crate) count: u32,
535}
536
537impl ShapeHits {
538 /// Returns a slice over the shapecast hits.
539 pub fn as_slice(&self) -> &[ShapeHitData] {
540 &self.vector[0..self.count as usize]
541 }
542
543 /// Returns the number of hits.
544 #[doc(alias = "count")]
545 pub fn len(&self) -> usize {
546 self.count as usize
547 }
548
549 /// Returns true if the number of hits is 0.
550 pub fn is_empty(&self) -> bool {
551 self.count == 0
552 }
553
554 /// Clears the hits.
555 pub fn clear(&mut self) {
556 self.vector.clear();
557 self.count = 0;
558 }
559
560 /// Returns an iterator over the hits in the order of distance.
561 pub fn iter(&self) -> core::slice::Iter<ShapeHitData> {
562 self.as_slice().iter()
563 }
564}
565
566impl MapEntities for ShapeHits {
567 fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
568 for hit in &mut self.vector {
569 hit.map_entities(entity_mapper);
570 }
571 }
572}
573
574/// Data related to a hit during a [shapecast](spatial_query#shapecasting).
575#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
576#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
577#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
578#[reflect(Debug, PartialEq)]
579pub struct ShapeHitData {
580 /// The entity of the collider that was hit by the shape.
581 pub entity: Entity,
582
583 /// How far the shape travelled before the initial hit.
584 #[doc(alias = "time_of_impact")]
585 pub distance: Scalar,
586
587 /// The closest point on the shape that was hit, expressed in world space.
588 ///
589 /// If the shapes are penetrating or the target distance is greater than zero,
590 /// this will be different from `point2`.
591 pub point1: Vector,
592
593 /// The closest point on the shape that was cast, expressed in world space.
594 ///
595 /// If the shapes are penetrating or the target distance is greater than zero,
596 /// this will be different from `point1`.
597 pub point2: Vector,
598
599 /// The outward surface normal on the hit shape at `point1`, expressed in world space.
600 pub normal1: Vector,
601
602 /// The outward surface normal on the cast shape at `point2`, expressed in world space.
603 pub normal2: Vector,
604}
605
606impl MapEntities for ShapeHitData {
607 fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
608 self.entity = entity_mapper.get_mapped(self.entity);
609 }
610}