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};
10use parry::{query::ShapeCastOptions, shape::CompositeShapeRef};
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.clear();
351
352 let shape_cast_options = ShapeCastOptions {
353 max_time_of_impact: self.max_distance,
354 target_distance: self.target_distance,
355 stop_at_penetration: !self.ignore_origin_penetration,
356 compute_impact_geometry_on_penetration: self.compute_contact_on_penetration,
357 };
358
359 let shape_rotation: Rotation;
360 #[cfg(feature = "2d")]
361 {
362 shape_rotation = Rotation::radians(self.global_shape_rotation());
363 }
364 #[cfg(feature = "3d")]
365 {
366 shape_rotation = Rotation::from(self.global_shape_rotation());
367 }
368
369 let shape_isometry = make_isometry(self.global_origin(), shape_rotation);
370 let shape_direction = self.global_direction().adjust_precision().into();
371
372 while hits.len() < self.max_hits as usize {
373 let composite = query_pipeline.as_composite_shape_internal(&query_filter);
374 let pipeline_shape = CompositeShapeRef(&composite);
375
376 let hit = pipeline_shape
377 .cast_shape(
378 query_pipeline.dispatcher.as_ref(),
379 &shape_isometry,
380 &shape_direction,
381 self.shape.shape_scaled().as_ref(),
382 shape_cast_options,
383 )
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 let Some(hit) = hit {
394 query_filter.excluded_entities.insert(hit.entity);
395 hits.push(hit);
396 } else {
397 return;
398 }
399 }
400 }
401}
402
403fn on_add_shape_caster(mut world: DeferredWorld, ctx: HookContext) {
404 let shape_caster = world.get::<ShapeCaster>(ctx.entity).unwrap();
405 let max_hits = if shape_caster.max_hits == u32::MAX {
406 10
407 } else {
408 shape_caster.max_hits as usize
409 };
410
411 // Initialize capacity for hits
412 world.get_mut::<ShapeHits>(ctx.entity).unwrap().0 = Vec::with_capacity(max_hits);
413}
414
415/// Configuration for a shape cast.
416#[derive(Clone, Debug, PartialEq, Reflect)]
417#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
418#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
419#[reflect(Debug, PartialEq)]
420pub struct ShapeCastConfig {
421 /// The maximum distance the shape can travel.
422 ///
423 /// By default, this is infinite.
424 #[doc(alias = "max_time_of_impact")]
425 pub max_distance: Scalar,
426
427 /// The separation distance at which the shapes will be considered as impacting.
428 ///
429 /// If the shapes are separated by a distance smaller than `target_distance` at the origin of the cast,
430 /// the computed contact points and normals are only reliable if [`ShapeCastConfig::compute_contact_on_penetration`]
431 /// is set to `true`.
432 ///
433 /// By default, this is `0.0`, so the shapes will only be considered as impacting when they first touch.
434 pub target_distance: Scalar,
435
436 /// If `true`, contact points and normals will be calculated even when the cast distance is `0.0`.
437 ///
438 /// The default is `true`.
439 pub compute_contact_on_penetration: bool,
440
441 /// If `true` *and* the shape is travelling away from the object that was hit,
442 /// the cast will ignore any impact that happens at the cast origin.
443 ///
444 /// The default is `false`.
445 pub ignore_origin_penetration: bool,
446}
447
448impl Default for ShapeCastConfig {
449 fn default() -> Self {
450 Self::DEFAULT
451 }
452}
453
454impl ShapeCastConfig {
455 /// The default [`ShapeCastConfig`] configuration.
456 pub const DEFAULT: Self = Self {
457 max_distance: Scalar::MAX,
458 target_distance: 0.0,
459 compute_contact_on_penetration: true,
460 ignore_origin_penetration: false,
461 };
462
463 /// Creates a new [`ShapeCastConfig`] with a given maximum distance the shape can travel.
464 #[inline]
465 pub const fn from_max_distance(max_distance: Scalar) -> Self {
466 Self {
467 max_distance,
468 target_distance: 0.0,
469 compute_contact_on_penetration: true,
470 ignore_origin_penetration: false,
471 }
472 }
473
474 /// Creates a new [`ShapeCastConfig`] with a given separation distance at which
475 /// the shapes will be considered as impacting.
476 #[inline]
477 pub const fn from_target_distance(target_distance: Scalar) -> Self {
478 Self {
479 max_distance: Scalar::MAX,
480 target_distance,
481 compute_contact_on_penetration: true,
482 ignore_origin_penetration: false,
483 }
484 }
485
486 /// Sets the maximum distance the shape can travel.
487 #[inline]
488 pub const fn with_max_distance(mut self, max_distance: Scalar) -> Self {
489 self.max_distance = max_distance;
490 self
491 }
492
493 /// Sets the separation distance at which the shapes will be considered as impacting.
494 #[inline]
495 pub const fn with_target_distance(mut self, target_distance: Scalar) -> Self {
496 self.target_distance = target_distance;
497 self
498 }
499}
500
501/// Contains the hits of a shape cast by a [`ShapeCaster`]. The hits are in the order of distance.
502///
503/// The maximum number of hits depends on the value of `max_hits` in [`ShapeCaster`]. By default only
504/// one hit is computed, as shapecasting for many results can be expensive.
505///
506/// # Example
507///
508/// ```
509#[cfg_attr(feature = "2d", doc = "use avian2d::prelude::*;")]
510#[cfg_attr(feature = "3d", doc = "use avian3d::prelude::*;")]
511/// use bevy::prelude::*;
512///
513/// fn print_hits(query: Query<&ShapeHits, With<ShapeCaster>>) {
514/// for hits in &query {
515/// for hit in hits {
516/// println!("Hit entity {} with distance {}", hit.entity, hit.distance);
517/// }
518/// }
519/// }
520/// ```
521#[derive(Component, Clone, Debug, Default, Deref, DerefMut, PartialEq, Reflect)]
522#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
523#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
524#[reflect(Component, Debug, Default, PartialEq)]
525pub struct ShapeHits(pub Vec<ShapeHitData>);
526
527impl IntoIterator for ShapeHits {
528 type Item = ShapeHitData;
529 type IntoIter = alloc::vec::IntoIter<ShapeHitData>;
530
531 fn into_iter(self) -> Self::IntoIter {
532 self.0.into_iter()
533 }
534}
535
536impl<'a> IntoIterator for &'a ShapeHits {
537 type Item = &'a ShapeHitData;
538 type IntoIter = core::slice::Iter<'a, ShapeHitData>;
539
540 fn into_iter(self) -> Self::IntoIter {
541 self.0.iter()
542 }
543}
544
545impl<'a> IntoIterator for &'a mut ShapeHits {
546 type Item = &'a mut ShapeHitData;
547 type IntoIter = core::slice::IterMut<'a, ShapeHitData>;
548
549 fn into_iter(self) -> Self::IntoIter {
550 self.0.iter_mut()
551 }
552}
553
554impl MapEntities for ShapeHits {
555 fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
556 for hit in self {
557 hit.map_entities(entity_mapper);
558 }
559 }
560}
561
562/// Data related to a hit during a [shapecast](spatial_query#shapecasting).
563#[derive(Clone, Copy, Debug, PartialEq, Reflect)]
564#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
565#[cfg_attr(feature = "serialize", reflect(Serialize, Deserialize))]
566#[reflect(Debug, PartialEq)]
567pub struct ShapeHitData {
568 /// The entity of the collider that was hit by the shape.
569 pub entity: Entity,
570
571 /// How far the shape travelled before the initial hit.
572 #[doc(alias = "time_of_impact")]
573 pub distance: Scalar,
574
575 /// The closest point on the shape that was hit, expressed in world space.
576 ///
577 /// If the shapes are penetrating or the target distance is greater than zero,
578 /// this will be different from `point2`.
579 pub point1: Vector,
580
581 /// The closest point on the shape that was cast, expressed in world space.
582 ///
583 /// If the shapes are penetrating or the target distance is greater than zero,
584 /// this will be different from `point1`.
585 pub point2: Vector,
586
587 /// The outward surface normal on the hit shape at `point1`, expressed in world space.
588 pub normal1: Vector,
589
590 /// The outward surface normal on the cast shape at `point2`, expressed in world space.
591 pub normal2: Vector,
592}
593
594impl MapEntities for ShapeHitData {
595 fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
596 self.entity = entity_mapper.get_mapped(self.entity);
597 }
598}