avian2d/spatial_query/
shape_caster.rs

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