diff --git a/bevy_rapier3d/examples/ray_casting3.rs b/bevy_rapier3d/examples/ray_casting3.rs index 50a27a4c..c6068117 100644 --- a/bevy_rapier3d/examples/ray_casting3.rs +++ b/bevy_rapier3d/examples/ray_casting3.rs @@ -80,12 +80,16 @@ fn cast_ray( ) { let window = windows.single(); - let Some(cursor_position) = window.cursor_position() else { return; }; + let Some(cursor_position) = window.cursor_position() else { + return; + }; // We will color in read the colliders hovered by the mouse. for (camera, camera_transform) in &cameras { // First, compute a ray from the mouse position. - let Some(ray) = camera.viewport_to_world(camera_transform, cursor_position) else { return; }; + let Some(ray) = camera.viewport_to_world(camera_transform, cursor_position) else { + return; + }; // Then cast the ray. let hit = rapier_context.cast_ray( diff --git a/src/geometry/collider.rs b/src/geometry/collider.rs index a7dd2384..52a7a696 100644 --- a/src/geometry/collider.rs +++ b/src/geometry/collider.rs @@ -3,9 +3,15 @@ use std::fmt; #[cfg(all(feature = "dim3", feature = "async-collider"))] use {crate::geometry::VHACDParameters, bevy::utils::HashMap}; -use bevy::prelude::*; +use bevy::{ + ecs::{ + entity::{EntityMapper, MapEntities}, + reflect::ReflectMapEntities, + }, + prelude::*, + utils::HashSet, +}; -use bevy::utils::HashSet; use rapier::geometry::Shape; use rapier::prelude::{ColliderHandle, InteractionGroups, SharedShape}; @@ -503,6 +509,44 @@ impl CollidingEntities { #[reflect(Component, PartialEq)] pub struct ColliderDisabled; +/// Rigid body parent of the collider. +/// +/// This is not meant to be set directly, this is controlled by bevy's hierarchy. +/// +/// To change a colliders parent, set the [`Parent`] of the entity to a +/// different rigid body or remove the parent to leave the collider unattached. +#[derive(Component, Debug, Eq, PartialEq, Reflect)] +#[reflect(Component, MapEntities, PartialEq)] +pub struct ColliderParent(pub(crate) Entity); + +impl ColliderParent { + /// Gets the [`Entity`] ID of the rigid-body parent this collider is attached to. + /// + /// This may be the same entity as the collider. + pub fn get(&self) -> Entity { + self.0 + } +} + +impl FromWorld for ColliderParent { + fn from_world(_world: &mut World) -> Self { + Self(Entity::PLACEHOLDER) + } +} + +impl MapEntities for ColliderParent { + fn map_entities(&mut self, entity_mapper: &mut EntityMapper) { + self.0 = entity_mapper.get_or_reserve(self.0); + } +} + +impl std::ops::Deref for ColliderParent { + type Target = Entity; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + /// We restrict the scaling increment to 1.0e-4, to avoid numerical jitter /// due to the extraction of scaling factor from the GlobalTransform matrix. pub fn get_snapped_scale(scale: Vect) -> Vect { diff --git a/src/geometry/collider_impl.rs b/src/geometry/collider_impl.rs index f526ecd8..212a77f7 100644 --- a/src/geometry/collider_impl.rs +++ b/src/geometry/collider_impl.rs @@ -172,7 +172,9 @@ impl Collider { /// Returns `None` if the index buffer or vertex buffer of the mesh are in an incompatible format. #[cfg(all(feature = "dim3", feature = "async-collider"))] pub fn from_bevy_mesh(mesh: &Mesh, collider_shape: &ComputedColliderShape) -> Option<Self> { - let Some((vtx, idx)) = extract_mesh_vertices_indices(mesh) else { return None; }; + let Some((vtx, idx)) = extract_mesh_vertices_indices(mesh) else { + return None; + }; match collider_shape { ComputedColliderShape::TriMesh => Some( SharedShape::trimesh_with_flags(vtx, idx, TriMeshFlags::MERGE_DUPLICATE_VERTICES) diff --git a/src/plugin/plugin.rs b/src/plugin/plugin.rs index fc5aa295..0ca2e088 100644 --- a/src/plugin/plugin.rs +++ b/src/plugin/plugin.rs @@ -77,6 +77,13 @@ where /// See [`PhysicsSet`] for a description of these systems. pub fn get_systems(set: PhysicsSet) -> SystemConfigs { match set { + PhysicsSet::EveryFrame => ( + systems::collect_collider_hierarchy_changes, + apply_deferred, + systems::sync_removals, + ) + .chain() + .into_configs(), PhysicsSet::SyncBackend => ( // Run the character controller before the manual transform propagation. systems::update_character_controls, @@ -94,9 +101,10 @@ where systems::init_rigid_bodies, systems::init_colliders, systems::init_joints, - systems::sync_removals, - // Run this here so the folowwing systems do not have a 1 frame delay. + systems::collect_collider_hierarchy_changes, + // Run this here so the following systems do not have a 1 frame delay. apply_deferred, + systems::sync_removals, systems::apply_scale, systems::apply_collider_user_changes, systems::apply_rigid_body_user_changes, @@ -159,6 +167,9 @@ pub enum PhysicsSet { /// components and the [`GlobalTransform`] component. /// These systems typically run immediately after [`PhysicsSet::StepSimulation`]. Writeback, + /// The systems responsible for responding to state like `Events` that get + /// cleared every 2 main schedule runs. + EveryFrame, } impl<PhysicsHooks> Plugin for RapierPhysicsPlugin<PhysicsHooks> @@ -172,6 +183,10 @@ where .register_type::<Velocity>() .register_type::<AdditionalMassProperties>() .register_type::<MassProperties>() + .register_type::<ReadMassProperties>() + .register_type::<ColliderMassProperties>() + .register_type::<ColliderDisabled>() + .register_type::<RigidBodyDisabled>() .register_type::<LockedAxes>() .register_type::<ExternalForce>() .register_type::<ExternalImpulse>() @@ -185,6 +200,7 @@ where .register_type::<Friction>() .register_type::<Restitution>() .register_type::<CollisionGroups>() + .register_type::<ColliderParent>() .register_type::<SolverGroups>() .register_type::<ContactForceEventThreshold>() .register_type::<Group>(); @@ -204,6 +220,7 @@ where // Add each set as necessary if self.default_system_setup { + app.configure_sets(PostUpdate, (PhysicsSet::EveryFrame,)); app.configure_sets( self.schedule.clone(), ( @@ -216,7 +233,10 @@ where ); // These *must* be in the main schedule currently so that they do not miss events. - app.add_systems(PostUpdate, (systems::sync_removals,)); + app.add_systems( + PostUpdate, + Self::get_systems(PhysicsSet::EveryFrame).in_set(PhysicsSet::EveryFrame), + ); app.add_systems( self.schedule.clone(), diff --git a/src/plugin/systems.rs b/src/plugin/systems.rs index aadbf662..943c2fe0 100644 --- a/src/plugin/systems.rs +++ b/src/plugin/systems.rs @@ -15,11 +15,13 @@ use crate::pipeline::{CollisionEvent, ContactForceEvent}; use crate::plugin::configuration::{SimulationToRenderTime, TimestepMode}; use crate::plugin::{RapierConfiguration, RapierContext}; use crate::prelude::{ - BevyPhysicsHooks, BevyPhysicsHooksAdapter, CollidingEntities, KinematicCharacterController, - KinematicCharacterControllerOutput, MassModifiedEvent, RigidBodyDisabled, + BevyPhysicsHooks, BevyPhysicsHooksAdapter, ColliderParent, CollidingEntities, + KinematicCharacterController, KinematicCharacterControllerOutput, MassModifiedEvent, + RigidBodyDisabled, }; use crate::utils; -use bevy::ecs::system::{StaticSystemParam, SystemParamItem}; +use bevy::ecs::system::{StaticSystemParam, SystemParam, SystemParamItem}; +use bevy::hierarchy::HierarchyEvent; use bevy::prelude::*; use rapier::prelude::*; use std::collections::HashMap; @@ -117,47 +119,217 @@ pub fn apply_scale( } } -/// System responsible for applying changes the user made to a collider-related component. -pub fn apply_collider_user_changes( - mut context: ResMut<RapierContext>, - config: Res<RapierConfiguration>, - (changed_collider_transforms, parent_query, transform_query): ( - Query< - (Entity, &RapierColliderHandle, &GlobalTransform), - (Without<RapierRigidBodyHandle>, Changed<GlobalTransform>), - >, - Query<&Parent>, - Query<&Transform>, - ), - - changed_shapes: Query<(&RapierColliderHandle, &Collider), Changed<Collider>>, - changed_active_events: Query<(&RapierColliderHandle, &ActiveEvents), Changed<ActiveEvents>>, - changed_active_hooks: Query<(&RapierColliderHandle, &ActiveHooks), Changed<ActiveHooks>>, +fn find_child_colliders( + base_entity: Entity, + colliders: &Query<&Collider>, + rigid_bodies: &Query<&RigidBody>, + childrens: &Query<&Children>, + + found: &mut Vec<Entity>, + possibilities: &mut Vec<Entity>, +) { + found.clear(); + possibilities.clear(); + possibilities.push(base_entity); + while let Some(entity) = possibilities.pop() { + if rigid_bodies.contains(entity) { + continue; + } + + if colliders.contains(entity) { + found.push(entity); + } + + if let Ok(children) = childrens.get(entity) { + possibilities.extend(children.iter()); + } else { + continue; + }; + } +} + +/// System responsible for detecting changes in the hierarchy that would +/// affect the collider's parent rigid body. +pub fn collect_collider_hierarchy_changes( + mut commands: Commands, + + mut hierarchy_events: EventReader<HierarchyEvent>, + parents: Query<&Parent>, + childrens: Query<&Children>, + rigid_bodies: Query<&RigidBody>, + colliders: Query<&Collider>, + mut collider_parents: Query<&mut ColliderParent>, + + mut found: Local<Vec<Entity>>, + mut possibilities: Local<Vec<Entity>>, +) { + let parent_rigid_body = |mut entity: Entity| -> Option<Entity> { + loop { + if rigid_bodies.contains(entity) { + return Some(entity); + } + + if let Ok(parent) = parents.get(entity) { + entity = parent.get(); + } else { + return None; + } + } + }; + + for event in hierarchy_events.iter() { + match event { + HierarchyEvent::ChildAdded { child, .. } | HierarchyEvent::ChildMoved { child, .. } => { + let Some(rigid_body) = parent_rigid_body(*child) else { + continue; + }; + find_child_colliders( + *child, + &colliders, + &rigid_bodies, + &childrens, + &mut found, + &mut possibilities, + ); + + for collider in &found { + let new_collider_parent = ColliderParent(rigid_body); + if let Ok(mut collider_parent) = collider_parents.get_mut(*collider) { + *collider_parent = new_collider_parent; + } else { + commands.entity(*collider).insert(new_collider_parent); + } + } + } + HierarchyEvent::ChildRemoved { child, .. } => { + find_child_colliders( + *child, + &colliders, + &rigid_bodies, + &childrens, + &mut found, + &mut possibilities, + ); + for collider in &found { + if collider_parents.contains(*collider) { + commands.entity(*collider).remove::<ColliderParent>(); + } + } + } + } + } +} + +/// Collection of change queries for colliders. +/// +/// Mainly used because bevy doesn't impl more than +/// 16 parameters for a system. +#[derive(SystemParam)] +pub struct ColliderChanges<'w, 's> { + changed_collider_transforms: Query< + 'w, + 's, + ( + Entity, + &'static RapierColliderHandle, + &'static GlobalTransform, + ), + (Without<RapierRigidBodyHandle>, Changed<GlobalTransform>), + >, + changed_collider_parents: Query< + 'w, + 's, + (&'static RapierColliderHandle, &'static ColliderParent), + Changed<ColliderParent>, + >, + changed_shapes: + Query<'w, 's, (&'static RapierColliderHandle, &'static Collider), Changed<Collider>>, + changed_active_events: Query< + 'w, + 's, + (&'static RapierColliderHandle, &'static ActiveEvents), + Changed<ActiveEvents>, + >, + changed_active_hooks: + Query<'w, 's, (&'static RapierColliderHandle, &'static ActiveHooks), Changed<ActiveHooks>>, changed_active_collision_types: Query< - (&RapierColliderHandle, &ActiveCollisionTypes), + 'w, + 's, + (&'static RapierColliderHandle, &'static ActiveCollisionTypes), Changed<ActiveCollisionTypes>, >, - changed_friction: Query<(&RapierColliderHandle, &Friction), Changed<Friction>>, - changed_restitution: Query<(&RapierColliderHandle, &Restitution), Changed<Restitution>>, + changed_friction: + Query<'w, 's, (&'static RapierColliderHandle, &'static Friction), Changed<Friction>>, + changed_restitution: + Query<'w, 's, (&'static RapierColliderHandle, &'static Restitution), Changed<Restitution>>, changed_collision_groups: Query< - (&RapierColliderHandle, &CollisionGroups), + 'w, + 's, + (&'static RapierColliderHandle, &'static CollisionGroups), Changed<CollisionGroups>, >, - changed_solver_groups: Query<(&RapierColliderHandle, &SolverGroups), Changed<SolverGroups>>, - changed_sensors: Query<(&RapierColliderHandle, &Sensor), Changed<Sensor>>, - changed_disabled: Query<(&RapierColliderHandle, &ColliderDisabled), Changed<ColliderDisabled>>, + changed_solver_groups: Query< + 'w, + 's, + (&'static RapierColliderHandle, &'static SolverGroups), + Changed<SolverGroups>, + >, + changed_sensors: + Query<'w, 's, (&'static RapierColliderHandle, &'static Sensor), Changed<Sensor>>, + changed_disabled: Query< + 'w, + 's, + (&'static RapierColliderHandle, &'static ColliderDisabled), + Changed<ColliderDisabled>, + >, changed_contact_force_threshold: Query< - (&RapierColliderHandle, &ContactForceEventThreshold), + 'w, + 's, + ( + &'static RapierColliderHandle, + &'static ContactForceEventThreshold, + ), Changed<ContactForceEventThreshold>, >, changed_collider_mass_props: Query< - (&RapierColliderHandle, &ColliderMassProperties), + 'w, + 's, + ( + &'static RapierColliderHandle, + &'static ColliderMassProperties, + ), Changed<ColliderMassProperties>, >, +} + +/// System responsible for applying changes the user made to a collider-related component. +pub fn apply_collider_user_changes( + mut context: ResMut<RapierContext>, + config: Res<RapierConfiguration>, + + collider_changes: ColliderChanges, + parent_query: Query<&Parent>, + transform_query: Query<&Transform>, mut mass_modified: EventWriter<MassModifiedEvent>, ) { let scale = context.physics_scale; + let ColliderChanges { + changed_collider_transforms, + changed_collider_parents, + changed_shapes, + changed_active_events, + changed_active_hooks, + changed_active_collision_types, + changed_friction, + changed_restitution, + changed_collision_groups, + changed_solver_groups, + changed_sensors, + changed_disabled, + changed_contact_force_threshold, + changed_collider_mass_props, + } = collider_changes; for (entity, handle, transform) in changed_collider_transforms.iter() { if context.collider_parent(entity).is_some() { @@ -175,6 +347,17 @@ pub fn apply_collider_user_changes( } } + for (handle, collider_parent) in changed_collider_parents.iter() { + if let Some(body_handle) = context.entity2body.get(&collider_parent.0).copied() { + let RapierContext { + ref mut colliders, + ref mut bodies, + .. + } = *context; + colliders.set_parent(handle.0, Some(body_handle), bodies); + } + } + for (handle, shape) in changed_shapes.iter() { if let Some(co) = context.colliders.get_mut(handle.0) { let mut scaled_shape = shape.clone(); @@ -745,9 +928,10 @@ pub fn step_simulation<Hooks>( #[cfg(all(feature = "dim3", feature = "async-collider"))] pub fn init_async_colliders( mut commands: Commands, - meshes: Res<Assets<Mesh>>, + meshes: Option<Res<Assets<Mesh>>>, async_colliders: Query<(Entity, &Handle<Mesh>, &AsyncCollider)>, ) { + let Some(meshes) = meshes else { return }; for (entity, mesh_handle, async_collider) in async_colliders.iter() { if let Some(mesh) = meshes.get(mesh_handle) { match Collider::from_bevy_mesh(mesh, &async_collider.0) { @@ -768,12 +952,16 @@ pub fn init_async_colliders( #[cfg(all(feature = "dim3", feature = "async-collider"))] pub fn init_async_scene_colliders( mut commands: Commands, - meshes: Res<Assets<Mesh>>, - scene_spawner: Res<SceneSpawner>, + meshes: Option<Res<Assets<Mesh>>>, + scene_spawner: Option<Res<SceneSpawner>>, async_colliders: Query<(Entity, &SceneInstance, &AsyncSceneCollider)>, children: Query<&Children>, mesh_handles: Query<(&Name, &Handle<Mesh>)>, ) { + let Some(meshes) = meshes else { return }; + let Some(scene_spawner) = scene_spawner else { + return; + }; for (scene_entity, scene_instance, async_collider) in async_colliders.iter() { if scene_spawner.instance_is_ready(**scene_instance) { for child_entity in children.iter_descendants(scene_entity) { @@ -935,6 +1123,8 @@ pub fn init_colliders( context .colliders .insert_with_parent(builder, body_handle, &mut context.bodies); + commands.entity(entity).insert(ColliderParent(body_entity)); + if let Ok(mut mprops) = rigid_body_mprops.get_mut(body_entity) { // Inserting the collider changed the rigid-body’s mass properties. // Read them back from the engine. @@ -956,6 +1146,7 @@ pub fn init_colliders( }; commands.entity(entity).insert(RapierColliderHandle(handle)); + context.entity2collider.insert(entity, handle); } } @@ -1180,6 +1371,7 @@ pub fn sync_removals( mut removed_sensors: RemovedComponents<Sensor>, mut removed_rigid_body_disabled: RemovedComponents<RigidBodyDisabled>, mut removed_colliders_disabled: RemovedComponents<ColliderDisabled>, + mut removed_collider_parents: RemovedComponents<ColliderParent>, mut mass_modified: EventWriter<MassModifiedEvent>, ) { @@ -1308,6 +1500,17 @@ pub fn sync_removals( } } + for entity in removed_collider_parents.iter() { + if let Some(handle) = context.entity2collider.get(&entity) { + let RapierContext { + ref mut colliders, + ref mut bodies, + .. + } = *context; + colliders.set_parent(*handle, None, bodies); + } + } + // TODO: what about removing forces? } @@ -1684,6 +1887,7 @@ mod tests { app.add_plugins(( HeadlessRenderPlugin, TransformPlugin, + HierarchyPlugin, TimePlugin, RapierPhysicsPlugin::<NoUserData>::default(), )); @@ -1741,6 +1945,7 @@ mod tests { app.add_plugins(( HeadlessRenderPlugin, TransformPlugin, + HierarchyPlugin, TimePlugin, RapierPhysicsPlugin::<NoUserData>::default(), )); @@ -1815,6 +2020,149 @@ mod tests { } } + #[test] + fn collider_parent() { + let mut app = App::new(); + app.add_plugins(( + TransformPlugin, + HierarchyPlugin, + TimePlugin, + RapierPhysicsPlugin::<NoUserData>::default(), + )); + + fn verify_collider_parent( + ctx: Res<RapierContext>, + colliders: Query<(Entity, Option<&ColliderParent>), With<Collider>>, + parents: Query<&Parent>, + bodies: Query<(), With<RigidBody>>, + names: Query<&Name>, + ) { + let row_length = 60; + let column_length = row_length / 4; + let column = |collider: &str, rapier: &str, component: &str, hierarchal: &str| { + println!( + "{:<column_length$}| {:<column_length$}| {:<column_length$}| {:<column_length$}", + collider, rapier, component, hierarchal, + ); + }; + + column("collider", "rapier", "component", "hierarchal"); + println!("{}", "-".repeat(row_length)); + for (collider_entity, collider_parent) in &colliders { + let rapier_parent = ctx.collider_parent(collider_entity); + let collider_parent = collider_parent.map(|parent| parent.get()); + + let mut entity = collider_entity; + let mut hierarchal_parent = None; + loop { + if bodies.contains(entity) { + hierarchal_parent = Some(entity); + break; + } + + let Ok(parent) = parents.get(entity) else { + break; + }; + entity = parent.get(); + } + + let named = |entity: Option<Entity>| -> String { + entity + .map(|entity| { + names + .get(entity) + .map(|name| name.as_str().to_owned()) + .unwrap_or(format!("{:?}", entity)) + }) + .unwrap_or("None".to_owned()) + }; + column( + &named(Some(collider_entity)), + &named(rapier_parent), + &named(collider_parent), + &named(hierarchal_parent), + ); + + assert_eq!(rapier_parent, collider_parent); + assert_eq!(rapier_parent, hierarchal_parent); + } + } + + fn frame(mut frame: Local<u32>) { + *frame += 1; + println!("-- frame {} -----------", *frame); + println!(); + } + + app.add_systems(Last, (frame, verify_collider_parent).chain()); + + let self_parented = app + .world + .spawn(( + Name::new("Self-parented"), + TransformBundle::default(), + RigidBody::Dynamic, + Collider::default(), + )) + .id(); + + let parent1 = app + .world + .spawn(( + Name::new("Parent 1"), + TransformBundle::default(), + RigidBody::Dynamic, + )) + .id(); + + let parent2 = app + .world + .spawn(( + Name::new("Parent 2"), + TransformBundle::default(), + RigidBody::Dynamic, + )) + .id(); + + let inbetween = app + .world + .spawn((Name::new("Inbetween"), TransformBundle::default())) + .id(); + + let child = app + .world + .spawn(( + Name::new("Child collider"), + TransformBundle::default(), + Collider::default(), + )) + .id(); + + // Unnested + app.update(); + app.world.entity_mut(self_parented).despawn_recursive(); + app.world.entity_mut(child).set_parent(parent1); + app.update(); + app.world.entity_mut(child).set_parent(parent2); + app.update(); + app.world.entity_mut(child).remove_parent(); + app.update(); + + // Nested + app.world.entity_mut(child).set_parent(inbetween); + app.update(); + app.world.entity_mut(inbetween).set_parent(parent1); + app.update(); + app.world.entity_mut(inbetween).set_parent(parent2); + app.update(); + app.world.entity_mut(inbetween).remove_parent(); + app.update(); + + app.world.entity_mut(inbetween).set_parent(parent1); + app.world.entity_mut(child).remove_parent(); + app.update(); + } + // Allows run tests for systems containing rendering related things without GPU struct HeadlessRenderPlugin;