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}