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}