bevy_rapier3d/plugin/systems/
collider.rs

1use crate::dynamics::ReadMassProperties;
2use crate::geometry::Collider;
3use crate::plugin::context::systemparams::{RapierEntity, RAPIER_CONTEXT_EXPECT_ERROR};
4use crate::plugin::context::RapierContextEntityLink;
5use crate::plugin::{
6    context::{DefaultRapierContext, RapierContextColliders, RapierRigidBodySet},
7    RapierConfiguration,
8};
9use crate::prelude::{
10    ActiveCollisionTypes, ActiveEvents, ActiveHooks, ColliderDisabled, ColliderMassProperties,
11    ColliderScale, CollidingEntities, CollisionEvent, CollisionGroups, ContactForceEventThreshold,
12    ContactSkin, Friction, MassModifiedEvent, MassProperties, RapierColliderHandle,
13    RapierRigidBodyHandle, Restitution, Sensor, SolverGroups,
14};
15use crate::utils;
16use bevy::prelude::*;
17use rapier::dynamics::RigidBodyHandle;
18use rapier::geometry::ColliderBuilder;
19#[cfg(all(feature = "dim3", feature = "async-collider"))]
20use {
21    crate::prelude::{AsyncCollider, AsyncSceneCollider},
22    bevy::scene::SceneInstance,
23};
24
25#[cfg(feature = "dim2")]
26use bevy::math::Vec3Swizzles;
27
28/// Components related to colliders.
29pub type ColliderComponents<'a> = (
30    (Entity, Option<&'a RapierContextEntityLink>),
31    &'a Collider,
32    Option<&'a Sensor>,
33    Option<&'a ColliderMassProperties>,
34    Option<&'a ActiveEvents>,
35    Option<&'a ActiveHooks>,
36    Option<&'a ActiveCollisionTypes>,
37    Option<&'a Friction>,
38    Option<&'a Restitution>,
39    Option<&'a ContactSkin>,
40    Option<&'a CollisionGroups>,
41    Option<&'a SolverGroups>,
42    Option<&'a ContactForceEventThreshold>,
43    Option<&'a ColliderDisabled>,
44);
45
46/// System responsible for applying [`GlobalTransform`] scale and/or [`ColliderScale`] to
47/// colliders.
48pub fn apply_scale(
49    config: Query<&RapierConfiguration>,
50    mut changed_collider_scales: Query<
51        (
52            &mut Collider,
53            &RapierContextEntityLink,
54            &GlobalTransform,
55            Option<&ColliderScale>,
56        ),
57        Or<(
58            Changed<Collider>,
59            Changed<GlobalTransform>,
60            Changed<ColliderScale>,
61        )>,
62    >,
63) {
64    for (mut shape, link, transform, custom_scale) in changed_collider_scales.iter_mut() {
65        let config = config.get(link.0).unwrap();
66        #[cfg(feature = "dim2")]
67        let effective_scale = match custom_scale {
68            Some(ColliderScale::Absolute(scale)) => *scale,
69            Some(ColliderScale::Relative(scale)) => {
70                *scale * transform.compute_transform().scale.xy()
71            }
72            None => transform.compute_transform().scale.xy(),
73        };
74        #[cfg(feature = "dim3")]
75        let effective_scale = match custom_scale {
76            Some(ColliderScale::Absolute(scale)) => *scale,
77            Some(ColliderScale::Relative(scale)) => *scale * transform.compute_transform().scale,
78            None => transform.compute_transform().scale,
79        };
80
81        if shape.scale != crate::geometry::get_snapped_scale(effective_scale) {
82            shape.set_scale(effective_scale, config.scaled_shape_subdivision);
83        }
84    }
85}
86
87/// System responsible for applying changes the user made to a collider-related component.
88pub fn apply_collider_user_changes(
89    mut context: Query<(&RapierRigidBodySet, &mut RapierContextColliders)>,
90    config: Query<&RapierConfiguration>,
91    (changed_collider_transforms, child_of_query, transform_query): (
92        Query<
93            (RapierEntity, &RapierColliderHandle, &GlobalTransform),
94            (Without<RapierRigidBodyHandle>, Changed<GlobalTransform>),
95        >,
96        Query<&ChildOf>,
97        Query<&Transform>,
98    ),
99
100    changed_shapes: Query<(RapierEntity, &RapierColliderHandle, &Collider), Changed<Collider>>,
101    changed_active_events: Query<
102        (RapierEntity, &RapierColliderHandle, &ActiveEvents),
103        Changed<ActiveEvents>,
104    >,
105    changed_active_hooks: Query<
106        (RapierEntity, &RapierColliderHandle, &ActiveHooks),
107        Changed<ActiveHooks>,
108    >,
109    changed_active_collision_types: Query<
110        (RapierEntity, &RapierColliderHandle, &ActiveCollisionTypes),
111        Changed<ActiveCollisionTypes>,
112    >,
113    (changed_friction, changed_restitution, changed_contact_skin): (
114        Query<(RapierEntity, &RapierColliderHandle, &Friction), Changed<Friction>>,
115        Query<(RapierEntity, &RapierColliderHandle, &Restitution), Changed<Restitution>>,
116        Query<(RapierEntity, &RapierColliderHandle, &ContactSkin), Changed<ContactSkin>>,
117    ),
118    changed_collision_groups: Query<
119        (RapierEntity, &RapierColliderHandle, &CollisionGroups),
120        Changed<CollisionGroups>,
121    >,
122    changed_solver_groups: Query<
123        (RapierEntity, &RapierColliderHandle, &SolverGroups),
124        Changed<SolverGroups>,
125    >,
126    changed_sensors: Query<(RapierEntity, &RapierColliderHandle, &Sensor), Changed<Sensor>>,
127    changed_disabled: Query<
128        (RapierEntity, &RapierColliderHandle, &ColliderDisabled),
129        Changed<ColliderDisabled>,
130    >,
131    changed_contact_force_threshold: Query<
132        (
133            RapierEntity,
134            &RapierColliderHandle,
135            &ContactForceEventThreshold,
136        ),
137        Changed<ContactForceEventThreshold>,
138    >,
139    changed_collider_mass_props: Query<
140        (RapierEntity, &RapierColliderHandle, &ColliderMassProperties),
141        Changed<ColliderMassProperties>,
142    >,
143
144    mut mass_modified: MessageWriter<MassModifiedEvent>,
145) {
146    for (rapier_entity, handle, transform) in changed_collider_transforms.iter() {
147        let (rigidbody_set, mut context_colliders) = context
148            .get_mut(rapier_entity.rapier_context_link.0)
149            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
150        if context_colliders
151            .collider_parent(rigidbody_set, rapier_entity.entity)
152            .is_some()
153        {
154            let (_, collider_position) = collider_offset(
155                rapier_entity.entity,
156                rigidbody_set,
157                &child_of_query,
158                &transform_query,
159            );
160
161            if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
162                let new_pos = utils::transform_to_iso(&collider_position);
163
164                if co
165                    .position_wrt_parent()
166                    .map(|pos| *pos != new_pos)
167                    .unwrap_or(true)
168                {
169                    co.set_position_wrt_parent(new_pos);
170                }
171            }
172        } else if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
173            let new_pos = utils::transform_to_iso(&transform.compute_transform());
174
175            if *co.position() != new_pos {
176                co.set_position(utils::transform_to_iso(&transform.compute_transform()));
177            }
178        }
179    }
180
181    for (rapier_entity, handle, shape) in changed_shapes.iter() {
182        let (rigidbody_set, mut context_colliders) = context
183            .get_mut(rapier_entity.rapier_context_link.0)
184            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
185        let config = config.get(rapier_entity.rapier_context_link.0).unwrap();
186        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
187            let mut scaled_shape = shape.clone();
188            scaled_shape.set_scale(shape.scale, config.scaled_shape_subdivision);
189            co.set_shape(scaled_shape.raw.clone());
190
191            if let Some(body) = co.parent() {
192                if let Some(body_entity) = rigidbody_set.rigid_body_entity(body) {
193                    mass_modified.write(body_entity.into());
194                }
195            }
196        }
197    }
198
199    for (rapier_entity, handle, active_events) in changed_active_events.iter() {
200        let (_, mut context_colliders) = context
201            .get_mut(rapier_entity.rapier_context_link.0)
202            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
203        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
204            co.set_active_events((*active_events).into())
205        }
206    }
207
208    for (rapier_entity, handle, active_hooks) in changed_active_hooks.iter() {
209        let (_, mut context_colliders) = context
210            .get_mut(rapier_entity.rapier_context_link.0)
211            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
212        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
213            co.set_active_hooks((*active_hooks).into())
214        }
215    }
216
217    for (rapier_entity, handle, active_collision_types) in changed_active_collision_types.iter() {
218        let (_, mut context_colliders) = context
219            .get_mut(rapier_entity.rapier_context_link.0)
220            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
221        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
222            co.set_active_collision_types((*active_collision_types).into())
223        }
224    }
225
226    for (rapier_entity, handle, friction) in changed_friction.iter() {
227        let (_, mut context_colliders) = context
228            .get_mut(rapier_entity.rapier_context_link.0)
229            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
230        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
231            co.set_friction(friction.coefficient);
232            co.set_friction_combine_rule(friction.combine_rule.into());
233        }
234    }
235
236    for (rapier_entity, handle, restitution) in changed_restitution.iter() {
237        let (_, mut context_colliders) = context
238            .get_mut(rapier_entity.rapier_context_link.0)
239            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
240        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
241            co.set_restitution(restitution.coefficient);
242            co.set_restitution_combine_rule(restitution.combine_rule.into());
243        }
244    }
245
246    for (rapier_entity, handle, contact_skin) in changed_contact_skin.iter() {
247        let (_, mut context_colliders) = context
248            .get_mut(rapier_entity.rapier_context_link.0)
249            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
250        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
251            co.set_contact_skin(contact_skin.0);
252        }
253    }
254
255    for (rapier_entity, handle, collision_groups) in changed_collision_groups.iter() {
256        let (_, mut context_colliders) = context
257            .get_mut(rapier_entity.rapier_context_link.0)
258            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
259        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
260            co.set_collision_groups((*collision_groups).into());
261        }
262    }
263
264    for (rapier_entity, handle, solver_groups) in changed_solver_groups.iter() {
265        let (_, mut context_colliders) = context
266            .get_mut(rapier_entity.rapier_context_link.0)
267            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
268        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
269            co.set_solver_groups((*solver_groups).into());
270        }
271    }
272
273    for (rapier_entity, handle, _) in changed_sensors.iter() {
274        let (_, mut context_colliders) = context
275            .get_mut(rapier_entity.rapier_context_link.0)
276            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
277        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
278            co.set_sensor(true);
279        }
280    }
281
282    for (rapier_entity, handle, _) in changed_disabled.iter() {
283        let (_, mut context_colliders) = context
284            .get_mut(rapier_entity.rapier_context_link.0)
285            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
286        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
287            co.set_enabled(false);
288        }
289    }
290
291    for (rapier_entity, handle, threshold) in changed_contact_force_threshold.iter() {
292        let (_, mut context_colliders) = context
293            .get_mut(rapier_entity.rapier_context_link.0)
294            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
295        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
296            co.set_contact_force_event_threshold(threshold.0);
297        }
298    }
299
300    for (rapier_entity, handle, mprops) in changed_collider_mass_props.iter() {
301        let (rigidbody_set, mut context_colliders) = context
302            .get_mut(rapier_entity.rapier_context_link.0)
303            .expect(RAPIER_CONTEXT_EXPECT_ERROR);
304        if let Some(co) = context_colliders.colliders.get_mut(handle.0) {
305            match mprops {
306                ColliderMassProperties::Density(density) => co.set_density(*density),
307                ColliderMassProperties::Mass(mass) => co.set_mass(*mass),
308                ColliderMassProperties::MassProperties(mprops) => {
309                    co.set_mass_properties(mprops.into_rapier())
310                }
311            }
312
313            if let Some(body) = co.parent() {
314                if let Some(body_entity) = rigidbody_set.rigid_body_entity(body) {
315                    mass_modified.write(body_entity.into());
316                }
317            }
318        }
319    }
320}
321
322pub(crate) fn collider_offset(
323    entity: Entity,
324    rigidbody_set: &RapierRigidBodySet,
325    child_of_query: &Query<&ChildOf>,
326    transform_query: &Query<&Transform>,
327) -> (Option<RigidBodyHandle>, Transform) {
328    let mut body_entity = entity;
329    let mut body_handle = rigidbody_set.entity2body.get(&body_entity).copied();
330    let mut child_transform = Transform::default();
331    while body_handle.is_none() {
332        if let Ok(child_of) = child_of_query.get(body_entity) {
333            if let Ok(transform) = transform_query.get(body_entity) {
334                child_transform = *transform * child_transform;
335            }
336            body_entity = child_of.parent();
337        } else {
338            break;
339        }
340
341        body_handle = rigidbody_set.entity2body.get(&body_entity).copied();
342    }
343
344    if body_handle.is_some() {
345        if let Ok(transform) = transform_query.get(body_entity) {
346            let scale_transform = Transform {
347                scale: transform.scale,
348                ..default()
349            };
350
351            child_transform = scale_transform * child_transform;
352        }
353    }
354
355    (body_handle, child_transform)
356}
357
358/// System responsible for creating new Rapier colliders from the related `bevy_rapier` components.
359pub fn init_colliders(
360    mut commands: Commands,
361    config: Query<&RapierConfiguration>,
362    mut context_access: Query<(&mut RapierRigidBodySet, &mut RapierContextColliders)>,
363    default_context_access: Query<Entity, With<DefaultRapierContext>>,
364    colliders: Query<(ColliderComponents, Option<&GlobalTransform>), Without<RapierColliderHandle>>,
365    mut rigid_body_mprops: Query<&mut ReadMassProperties>,
366    child_of_query: Query<&ChildOf>,
367    transform_query: Query<&Transform>,
368) {
369    for (
370        (
371            (entity, entity_context_link),
372            shape,
373            sensor,
374            mprops,
375            active_events,
376            active_hooks,
377            active_collision_types,
378            friction,
379            restitution,
380            contact_skin,
381            collision_groups,
382            solver_groups,
383            contact_force_event_threshold,
384            disabled,
385        ),
386        global_transform,
387    ) in colliders.iter()
388    {
389        // Get rapier context from RapierContextEntityLink or insert its default value.
390        let context_entity = entity_context_link.map_or_else(
391            || {
392                let context_entity = default_context_access.single().ok()?;
393                commands
394                    .entity(entity)
395                    .insert(RapierContextEntityLink(context_entity));
396                Some(context_entity)
397            },
398            |link| Some(link.0),
399        );
400        let Some(context_entity) = context_entity else {
401            continue;
402        };
403
404        let config = config.get(context_entity).unwrap_or_else(|_| {
405            panic!("Failed to retrieve `RapierConfiguration` on entity {context_entity}.")
406        });
407
408        let Some(mut rigidbody_set_collider_set) = context_access.get_mut(context_entity).ok()
409        else {
410            log::error!("Could not find entity {context_entity} with rapier context while initializing {entity}");
411            continue;
412        };
413        let context_colliders = &mut *rigidbody_set_collider_set.1;
414        let rigidbody_set = &mut *rigidbody_set_collider_set.0;
415        let mut scaled_shape = shape.clone();
416        scaled_shape.set_scale(shape.scale, config.scaled_shape_subdivision);
417        let mut builder = ColliderBuilder::new(scaled_shape.raw.clone());
418
419        builder = builder.sensor(sensor.is_some());
420        builder = builder.enabled(disabled.is_none());
421
422        if let Some(mprops) = mprops {
423            builder = match mprops {
424                ColliderMassProperties::Density(density) => builder.density(*density),
425                ColliderMassProperties::Mass(mass) => builder.mass(*mass),
426                ColliderMassProperties::MassProperties(mprops) => {
427                    builder.mass_properties(mprops.into_rapier())
428                }
429            };
430        }
431
432        if let Some(active_events) = active_events {
433            builder = builder.active_events((*active_events).into());
434        }
435
436        if let Some(active_hooks) = active_hooks {
437            builder = builder.active_hooks((*active_hooks).into());
438        }
439
440        if let Some(active_collision_types) = active_collision_types {
441            builder = builder.active_collision_types((*active_collision_types).into());
442        }
443
444        if let Some(friction) = friction {
445            builder = builder
446                .friction(friction.coefficient)
447                .friction_combine_rule(friction.combine_rule.into());
448        }
449
450        if let Some(restitution) = restitution {
451            builder = builder
452                .restitution(restitution.coefficient)
453                .restitution_combine_rule(restitution.combine_rule.into());
454        }
455
456        if let Some(contact_skin) = contact_skin {
457            builder = builder.contact_skin(contact_skin.0);
458        }
459
460        if let Some(collision_groups) = collision_groups {
461            builder = builder.collision_groups((*collision_groups).into());
462        }
463
464        if let Some(solver_groups) = solver_groups {
465            builder = builder.solver_groups((*solver_groups).into());
466        }
467
468        if let Some(threshold) = contact_force_event_threshold {
469            builder = builder.contact_force_event_threshold(threshold.0);
470        }
471        let body_entity = entity;
472        let (body_handle, child_transform) =
473            collider_offset(entity, rigidbody_set, &child_of_query, &transform_query);
474
475        builder = builder.user_data(entity.to_bits() as u128);
476
477        let handle = if let Some(body_handle) = body_handle {
478            builder = builder.position(utils::transform_to_iso(&child_transform));
479            let handle = context_colliders.colliders.insert_with_parent(
480                builder,
481                body_handle,
482                &mut rigidbody_set.bodies,
483            );
484            if let Ok(mut mprops) = rigid_body_mprops.get_mut(body_entity) {
485                // Inserting the collider changed the rigid-body’s mass properties.
486                // Read them back from the engine.
487                if let Some(parent_body) = rigidbody_set.bodies.get(body_handle) {
488                    mprops.set(MassProperties::from_rapier(
489                        parent_body.mass_properties().local_mprops,
490                    ));
491                }
492            }
493            handle
494        } else {
495            let global_transform = global_transform.cloned().unwrap_or_default();
496            builder = builder.position(utils::transform_to_iso(
497                &global_transform.compute_transform(),
498            ));
499            context_colliders.colliders.insert(builder)
500        };
501
502        commands.entity(entity).insert(RapierColliderHandle(handle));
503        context_colliders.entity2collider.insert(entity, handle);
504    }
505}
506
507/// System responsible for creating `Collider` components from `AsyncCollider` components if the
508/// corresponding mesh has become available.
509#[cfg(all(feature = "dim3", feature = "async-collider"))]
510pub fn init_async_colliders(
511    mut commands: Commands,
512    meshes: Res<Assets<Mesh>>,
513    async_colliders: Query<(Entity, &Mesh3d, &AsyncCollider)>,
514) {
515    for (entity, mesh_handle, async_collider) in async_colliders.iter() {
516        if let Some(mesh) = meshes.get(mesh_handle) {
517            match Collider::from_bevy_mesh(mesh, &async_collider.0) {
518                Some(collider) => {
519                    commands
520                        .entity(entity)
521                        .insert(collider)
522                        .remove::<AsyncCollider>();
523                }
524                None => log::error!("Unable to generate collider from mesh {mesh:?}"),
525            }
526        }
527    }
528}
529
530/// System responsible for creating `Collider` components from `AsyncSceneCollider` components if the
531/// corresponding scene has become available.
532#[cfg(all(feature = "dim3", feature = "async-collider"))]
533pub fn init_async_scene_colliders(
534    mut commands: Commands,
535    meshes: Res<Assets<Mesh>>,
536    scene_spawner: Res<SceneSpawner>,
537    async_colliders: Query<(Entity, &SceneInstance, &AsyncSceneCollider)>,
538    children: Query<&Children>,
539    mesh_handles: Query<(&Name, &Mesh3d)>,
540) {
541    for (scene_entity, scene_instance, async_collider) in async_colliders.iter() {
542        if scene_spawner.instance_is_ready(**scene_instance) {
543            for child_entity in children.iter_descendants(scene_entity) {
544                if let Ok((name, handle)) = mesh_handles.get(child_entity) {
545                    let shape = async_collider
546                        .named_shapes
547                        .get(name.as_str())
548                        .unwrap_or(&async_collider.shape);
549                    if let Some(shape) = shape {
550                        let mesh = meshes.get(handle).unwrap(); // NOTE: Mesh is already loaded
551                        match Collider::from_bevy_mesh(mesh, shape) {
552                            Some(collider) => {
553                                commands.entity(child_entity).insert(collider);
554                            }
555                            None => log::error!(
556                                "Unable to generate collider from mesh {mesh:?} with name {name}"
557                            ),
558                        }
559                    }
560                }
561            }
562
563            commands.entity(scene_entity).remove::<AsyncSceneCollider>();
564        }
565    }
566}
567
568/// Adds entity to [`CollidingEntities`] on starting collision and removes from it when the
569/// collision ends.
570pub fn update_colliding_entities(
571    mut collision_events: MessageReader<CollisionEvent>,
572    mut colliding_entities: Query<&mut CollidingEntities>,
573) {
574    for event in collision_events.read() {
575        match event.to_owned() {
576            CollisionEvent::Started(entity1, entity2, _) => {
577                if let Ok(mut entities) = colliding_entities.get_mut(entity1) {
578                    entities.0.insert(entity2);
579                }
580                if let Ok(mut entities) = colliding_entities.get_mut(entity2) {
581                    entities.0.insert(entity1);
582                }
583            }
584            CollisionEvent::Stopped(entity1, entity2, _) => {
585                if let Ok(mut entities) = colliding_entities.get_mut(entity1) {
586                    entities.0.remove(&entity2);
587                }
588                if let Ok(mut entities) = colliding_entities.get_mut(entity2) {
589                    entities.0.remove(&entity1);
590                }
591            }
592        }
593    }
594}
595
596#[cfg(test)]
597#[allow(missing_docs)]
598pub mod test {
599    #[test]
600    #[cfg(all(feature = "dim3", feature = "async-collider"))]
601    fn async_collider_initializes() {
602        use super::*;
603        use bevy::{mesh::MeshPlugin, scene::ScenePlugin};
604
605        let mut app = App::new();
606        app.add_plugins((AssetPlugin::default(), MeshPlugin, ScenePlugin));
607        app.add_systems(Update, init_async_colliders);
608
609        app.finish();
610
611        let mut meshes = app.world_mut().resource_mut::<Assets<Mesh>>();
612        let cube = meshes.add(Cuboid::default());
613
614        let entity = app
615            .world_mut()
616            .spawn((Mesh3d(cube), AsyncCollider::default()))
617            .id();
618
619        app.update();
620
621        let entity = app.world().entity(entity);
622        assert!(
623            entity.get::<Collider>().is_some(),
624            "Collider component should be added"
625        );
626        assert!(
627            entity.get::<AsyncCollider>().is_none(),
628            "AsyncCollider component should be removed after Collider component creation"
629        );
630    }
631
632    #[test]
633    #[cfg(all(feature = "dim3", feature = "async-collider"))]
634    fn async_scene_collider_initializes() {
635        use super::*;
636        use bevy::{mesh::MeshPlugin, scene::ScenePlugin};
637
638        let mut app = App::new();
639        app.add_plugins((AssetPlugin::default(), MeshPlugin, ScenePlugin));
640        app.add_systems(PostUpdate, init_async_scene_colliders);
641
642        let mut meshes = app.world_mut().resource_mut::<Assets<Mesh>>();
643        let cube_handle = meshes.add(Cuboid::default());
644        let capsule_handle = meshes.add(Capsule3d::default());
645        let cube = app
646            .world_mut()
647            .spawn((Name::new("Cube"), Mesh3d(cube_handle)))
648            .id();
649        let capsule = app
650            .world_mut()
651            .spawn((Name::new("Capsule"), Mesh3d(capsule_handle)))
652            .id();
653
654        let mut scenes = app.world_mut().resource_mut::<Assets<Scene>>();
655        let scene = scenes.add(Scene::new(World::new()));
656
657        let mut named_shapes = bevy::platform::collections::HashMap::default();
658        named_shapes.insert("Capsule".to_string(), None);
659        let parent = app
660            .world_mut()
661            .spawn((
662                SceneRoot(scene),
663                AsyncSceneCollider {
664                    named_shapes,
665                    ..Default::default()
666                },
667            ))
668            .add_children(&[cube, capsule])
669            .id();
670
671        app.update();
672
673        assert!(
674            app.world().entity(cube).get::<Collider>().is_some(),
675            "Collider component should be added for cube"
676        );
677        assert!(
678            app.world().entity(capsule).get::<Collider>().is_none(),
679            "Collider component shouldn't be added for capsule"
680        );
681        assert!(
682            app.world().entity(parent).get::<AsyncCollider>().is_none(),
683            "AsyncSceneCollider component should be removed after Collider components creation"
684        );
685    }
686}