Skip to main content

avian3d/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::{math::RVec3, prelude::*};
47/// use bevy::prelude::*;
48///
49/// # #[cfg(feature = "3d")]
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///         RVec3::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: RVector,
88
89    /// The global origin of the shape.
90    global_origin: RVector,
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    pub shape_rotation: Rot,
97
98    /// The global rotation of the shape.
99    global_shape_rotation: Rot,
100
101    /// The local direction of the shapecast relative to the [`Rotation`] of the shape caster entity or its parent.
102    ///
103    /// To get the global direction, use the `global_direction` method.
104    pub direction: Dir,
105
106    /// The global direction of the shapecast.
107    global_direction: Dir,
108
109    /// The maximum number of hits allowed. By default this is one and only the first hit is returned.
110    pub max_hits: u32,
111
112    /// The maximum distance the shape can travel.
113    ///
114    /// By default, this is infinite.
115    #[doc(alias = "max_time_of_impact")]
116    pub max_distance: f32,
117
118    /// The separation distance at which the shapes will be considered as impacting.
119    ///
120    /// If the shapes are separated by a distance smaller than `target_distance` at the origin of the cast,
121    /// the computed contact points and normals are only reliable if [`ShapeCaster::compute_contact_on_penetration`]
122    /// is set to `true`.
123    ///
124    /// By default, this is `0.0`, so the shapes will only be considered as impacting when they first touch.
125    pub target_distance: f32,
126
127    /// If `true`, contact points and normals will be calculated even when the cast distance is `0.0`.
128    ///
129    /// The default is `true`.
130    pub compute_contact_on_penetration: bool,
131
132    /// If `true` *and* the shape is travelling away from the object that was hit,
133    /// the cast will ignore any impact that happens at the cast origin.
134    ///
135    /// The default is `false`.
136    pub ignore_origin_penetration: bool,
137
138    /// If true, the shape caster ignores hits against its own [`Collider`]. This is the default.
139    pub ignore_self: bool,
140
141    /// Rules that determine which colliders are taken into account in the shape cast.
142    pub query_filter: SpatialQueryFilter,
143}
144
145impl Default for ShapeCaster {
146    fn default() -> Self {
147        Self {
148            enabled: true,
149            #[cfg(feature = "2d")]
150            shape: Collider::circle(0.0),
151            #[cfg(feature = "3d")]
152            shape: Collider::sphere(0.0),
153            origin: RVector::ZERO,
154            global_origin: RVector::ZERO,
155            shape_rotation: Rot::IDENTITY,
156            global_shape_rotation: Rot::IDENTITY,
157            direction: Dir::X,
158            global_direction: Dir::X,
159            max_hits: 1,
160            max_distance: f32::MAX,
161            target_distance: 0.0,
162            compute_contact_on_penetration: true,
163            ignore_origin_penetration: false,
164            ignore_self: true,
165            query_filter: SpatialQueryFilter::default(),
166        }
167    }
168}
169
170impl ShapeCaster {
171    /// Creates a new [`ShapeCaster`] with a given shape, origin, shape rotation and direction.
172    pub fn new(
173        shape: impl Into<Collider>,
174        origin: RVector,
175        shape_rotation: impl Into<Rot>,
176        direction: Dir,
177    ) -> Self {
178        Self {
179            shape: shape.into(),
180            origin,
181            shape_rotation: shape_rotation.into(),
182            direction,
183            ..default()
184        }
185    }
186
187    /// Sets the ray origin.
188    pub fn with_origin(mut self, origin: RVector) -> Self {
189        self.origin = origin;
190        self
191    }
192
193    /// Sets the ray direction.
194    pub fn with_direction(mut self, direction: Dir) -> Self {
195        self.direction = direction;
196        self
197    }
198
199    /// Sets the separation distance at which the shapes will be considered as impacting.
200    ///
201    /// If the shapes are separated by a distance smaller than `target_distance` at the origin of the cast,
202    /// the computed contact points and normals are only reliable if [`ShapeCaster::compute_contact_on_penetration`]
203    /// is set to `true`.
204    ///
205    /// By default, this is `0.0`, so the shapes will only be considered as impacting when they first touch.
206    pub fn with_target_distance(mut self, target_distance: f32) -> Self {
207        self.target_distance = target_distance;
208        self
209    }
210
211    /// Sets if contact points and normals should be calculated even when the cast distance is `0.0`.
212    ///
213    /// The default is `true`.
214    pub fn with_compute_contact_on_penetration(mut self, compute_contact: bool) -> Self {
215        self.compute_contact_on_penetration = compute_contact;
216        self
217    }
218
219    /// Controls how the shapecast behaves when the shape is already penetrating a [collider](Collider)
220    /// at the shape origin.
221    ///
222    /// If set to `true` **and** the shape is being cast in a direction where it will eventually stop penetrating,
223    /// the shapecast will not stop immediately, and will instead continue until another hit.\
224    /// If set to false, the shapecast will stop immediately and return the hit. This is the default.
225    pub fn with_ignore_origin_penetration(mut self, ignore: bool) -> Self {
226        self.ignore_origin_penetration = ignore;
227        self
228    }
229
230    /// Sets if the shape caster should ignore hits against its own [`Collider`].
231    ///
232    /// The default is `true`.
233    pub fn with_ignore_self(mut self, ignore: bool) -> Self {
234        self.ignore_self = ignore;
235        self
236    }
237
238    /// Sets the maximum distance the shape can travel.
239    pub fn with_max_distance(mut self, max_distance: f32) -> Self {
240        self.max_distance = max_distance;
241        self
242    }
243
244    /// Sets the maximum number of allowed hits.
245    pub fn with_max_hits(mut self, max_hits: u32) -> Self {
246        self.max_hits = max_hits;
247        self
248    }
249
250    /// Sets the shape caster's [query filter](SpatialQueryFilter) that controls which colliders
251    /// should be included or excluded by shapecasts.
252    pub fn with_query_filter(mut self, query_filter: SpatialQueryFilter) -> Self {
253        self.query_filter = query_filter;
254        self
255    }
256
257    /// Enables the [`ShapeCaster`].
258    pub fn enable(&mut self) {
259        self.enabled = true;
260    }
261
262    /// Disables the [`ShapeCaster`].
263    pub fn disable(&mut self) {
264        self.enabled = false;
265    }
266
267    /// Returns the global origin of the ray.
268    pub fn global_origin(&self) -> RVector {
269        self.global_origin
270    }
271
272    /// Returns the global rotation of the shape.
273    pub fn global_shape_rotation(&self) -> Rot {
274        self.global_shape_rotation
275    }
276
277    /// Returns the global direction of the ray.
278    pub fn global_direction(&self) -> Dir {
279        self.global_direction
280    }
281
282    /// Sets the global origin of the ray.
283    pub(crate) fn set_global_origin(&mut self, global_origin: RVector) {
284        self.global_origin = global_origin;
285    }
286
287    /// Sets the global rotation of the shape.
288    pub(crate) fn set_global_shape_rotation(&mut self, global_rotation: impl Into<Rot>) {
289        self.global_shape_rotation = global_rotation.into();
290    }
291
292    /// Sets the global direction of the ray.
293    pub(crate) fn set_global_direction(&mut self, global_direction: Dir) {
294        self.global_direction = global_direction;
295    }
296
297    pub(crate) fn cast(
298        &mut self,
299        caster_entity: Entity,
300        hits: &mut ShapeHits,
301        spatial_query: &SpatialQuery,
302    ) {
303        if self.ignore_self {
304            self.query_filter.excluded_entities.insert(caster_entity);
305        } else {
306            self.query_filter.excluded_entities.remove(&caster_entity);
307        }
308
309        hits.clear();
310
311        let config = ShapeCastConfig {
312            max_distance: self.max_distance,
313            target_distance: self.target_distance,
314            compute_contact_on_penetration: self.compute_contact_on_penetration,
315            ignore_origin_penetration: self.ignore_origin_penetration,
316        };
317
318        if self.max_hits == 1 {
319            let first_hit = spatial_query.cast_shape_predicate(
320                &self.shape,
321                self.global_origin,
322                self.global_shape_rotation,
323                self.global_direction,
324                &config,
325                &self.query_filter,
326                &|_| true,
327            );
328
329            if let Some(hit) = first_hit {
330                hits.push(hit);
331            }
332        } else {
333            spatial_query.shape_hits_callback(
334                &self.shape,
335                self.global_origin,
336                self.global_shape_rotation,
337                self.global_direction,
338                &config,
339                &self.query_filter,
340                |hit| {
341                    if hits.len() < self.max_hits as usize {
342                        hits.push(hit);
343                        true
344                    } else {
345                        false
346                    }
347                },
348            );
349        }
350    }
351}
352
353fn on_add_shape_caster(mut world: DeferredWorld, ctx: HookContext) {
354    let shape_caster = world.get::<ShapeCaster>(ctx.entity).unwrap();
355    let max_hits = if shape_caster.max_hits == u32::MAX {
356        10
357    } else {
358        shape_caster.max_hits as usize
359    };
360
361    // Initialize capacity for hits
362    world.get_mut::<ShapeHits>(ctx.entity).unwrap().0 = Vec::with_capacity(max_hits);
363}
364
365/// Configuration for a shape cast.
366#[derive(Clone, Debug, PartialEq, Reflect)]
367#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
368#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
369#[reflect(Debug, PartialEq)]
370pub struct ShapeCastConfig {
371    /// The maximum distance the shape can travel.
372    ///
373    /// By default, this is infinite.
374    #[doc(alias = "max_time_of_impact")]
375    pub max_distance: f32,
376
377    /// The separation distance at which the shapes will be considered as impacting.
378    ///
379    /// If the shapes are separated by a distance smaller than `target_distance` at the origin of the cast,
380    /// the computed contact points and normals are only reliable if [`ShapeCastConfig::compute_contact_on_penetration`]
381    /// is set to `true`.
382    ///
383    /// By default, this is `0.0`, so the shapes will only be considered as impacting when they first touch.
384    pub target_distance: f32,
385
386    /// If `true`, contact points and normals will be calculated even when the cast distance is `0.0`.
387    ///
388    /// The default is `true`.
389    pub compute_contact_on_penetration: bool,
390
391    /// If `true` *and* the shape is travelling away from the object that was hit,
392    /// the cast will ignore any impact that happens at the cast origin.
393    ///
394    /// The default is `false`.
395    pub ignore_origin_penetration: bool,
396}
397
398impl Default for ShapeCastConfig {
399    fn default() -> Self {
400        Self::DEFAULT
401    }
402}
403
404impl ShapeCastConfig {
405    /// The default [`ShapeCastConfig`] configuration.
406    pub const DEFAULT: Self = Self {
407        max_distance: f32::MAX,
408        target_distance: 0.0,
409        compute_contact_on_penetration: true,
410        ignore_origin_penetration: false,
411    };
412
413    /// Creates a new [`ShapeCastConfig`] with a given maximum distance the shape can travel.
414    #[inline]
415    pub const fn from_max_distance(max_distance: f32) -> Self {
416        Self {
417            max_distance,
418            target_distance: 0.0,
419            compute_contact_on_penetration: true,
420            ignore_origin_penetration: false,
421        }
422    }
423
424    /// Creates a new [`ShapeCastConfig`] with a given separation distance at which
425    /// the shapes will be considered as impacting.
426    #[inline]
427    pub const fn from_target_distance(target_distance: f32) -> Self {
428        Self {
429            max_distance: f32::MAX,
430            target_distance,
431            compute_contact_on_penetration: true,
432            ignore_origin_penetration: false,
433        }
434    }
435
436    /// Sets the maximum distance the shape can travel.
437    #[inline]
438    pub const fn with_max_distance(mut self, max_distance: f32) -> Self {
439        self.max_distance = max_distance;
440        self
441    }
442
443    /// Sets the separation distance at which the shapes will be considered as impacting.
444    #[inline]
445    pub const fn with_target_distance(mut self, target_distance: f32) -> Self {
446        self.target_distance = target_distance;
447        self
448    }
449}
450
451/// Contains the hits of a shape cast by a [`ShapeCaster`]. The hits are in the order of distance.
452///
453/// The maximum number of hits depends on the value of `max_hits` in [`ShapeCaster`]. By default only
454/// one hit is computed, as shapecasting for many results can be expensive.
455///
456/// # Order
457///
458/// By default, the order of the hits is not guaranteed.
459///
460/// You can iterate the hits in the order of distance with `iter_sorted`.
461/// Note that this will create and sort a new vector instead of iterating over the existing one.
462///
463/// **Note**: When there are more hits than `max_hits`, **some hits will be missed**.
464/// If you want to guarantee that the closest hit is included, set `max_hits` to one.
465///
466/// # Example
467///
468/// ```
469#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
470#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
471/// use bevy::prelude::*;
472///
473/// fn print_hits(query: Query<&ShapeHits, With<ShapeCaster>>) {
474///     for hits in &query {
475///         // For the faster iterator that isn't sorted, use `.iter()`.
476///         for hit in hits.iter_sorted() {
477///             println!("Hit entity {} with distance {}", hit.entity, hit.distance);
478///         }
479///     }
480/// }
481/// ```
482#[derive(Component, Clone, Debug, Default, Deref, DerefMut, PartialEq, Reflect)]
483#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
484#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
485#[reflect(Component, Debug, Default, PartialEq)]
486pub struct ShapeHits(pub Vec<ShapeHitData>);
487
488impl ShapeHits {
489    /// Returns an iterator over the hits, sorted in ascending order according to the distance.
490    ///
491    /// Note that this allocates a new vector. If you don't need the hits in order, use `iter`.
492    pub fn iter_sorted(&self) -> alloc::vec::IntoIter<ShapeHitData> {
493        let mut vector = self.as_slice().to_vec();
494        vector.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap());
495        vector.into_iter()
496    }
497}
498
499impl IntoIterator for ShapeHits {
500    type Item = ShapeHitData;
501    type IntoIter = alloc::vec::IntoIter<ShapeHitData>;
502
503    fn into_iter(self) -> Self::IntoIter {
504        self.0.into_iter()
505    }
506}
507
508impl<'a> IntoIterator for &'a ShapeHits {
509    type Item = &'a ShapeHitData;
510    type IntoIter = core::slice::Iter<'a, ShapeHitData>;
511
512    fn into_iter(self) -> Self::IntoIter {
513        self.0.iter()
514    }
515}
516
517impl<'a> IntoIterator for &'a mut ShapeHits {
518    type Item = &'a mut ShapeHitData;
519    type IntoIter = core::slice::IterMut<'a, ShapeHitData>;
520
521    fn into_iter(self) -> Self::IntoIter {
522        self.0.iter_mut()
523    }
524}
525
526impl MapEntities for ShapeHits {
527    fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
528        for hit in self {
529            hit.map_entities(entity_mapper);
530        }
531    }
532}
533
534/// Data related to a hit during a [shapecast](spatial_query#shapecasting).
535#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
536#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
537#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
538#[reflect(Debug, PartialEq)]
539pub struct ShapeHitData {
540    /// The entity of the collider that was hit by the shape.
541    pub entity: Entity,
542
543    /// How far the shape travelled before the initial hit.
544    #[doc(alias = "time_of_impact")]
545    pub distance: f32,
546
547    /// The closest point on the shape that was hit, expressed in world space.
548    ///
549    /// If the shapes are penetrating or the target distance is greater than zero,
550    /// this will be different from `point2`.
551    pub point1: RVector,
552
553    /// The closest point on the shape that was cast, expressed in world space.
554    ///
555    /// If the shapes are penetrating or the target distance is greater than zero,
556    /// this will be different from `point1`.
557    pub point2: RVector,
558
559    /// The outward surface normal on the hit shape at `point1`, expressed in world space.
560    pub normal1: Vector,
561
562    /// The outward surface normal on the cast shape at `point2`, expressed in world space.
563    pub normal2: Vector,
564}
565
566impl MapEntities for ShapeHitData {
567    fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
568        self.entity = entity_mapper.get_mapped(self.entity);
569    }
570}