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}