use std::marker::PhantomData;
use bevy::ecs::event::Events;
use bevy::ecs::prelude::*;
use bevy::ecs::system::SystemParam;
use bevy::log::prelude::*;
use bevy::math::Quat;
use bevy::math::Vec3;
use crossbeam::channel::{Receiver, Sender};
use heron_core::{
CollisionData, CollisionEvent, CollisionLayers, CollisionShape, Gravity, PhysicsStepDuration,
PhysicsSteps, PhysicsTime,
};
pub use physics_world::PhysicsWorld;
use crate::convert::{IntoBevy, IntoRapier};
use crate::rapier::{
self,
prelude::{
BroadPhase, CCDSolver, ColliderHandle, ColliderSet, ContactPair, ImpulseJointSet,
IntegrationParameters, InteractionGroups, IslandManager, MultibodyJointSet, NarrowPhase,
RigidBodySet,
},
};
use crate::rapier::parry::query::{Ray, TOIStatus};
use crate::rapier::pipeline::{EventHandler, PhysicsPipeline, QueryPipeline};
use crate::shape::ColliderFactory;
#[allow(missing_docs)]
mod physics_world {
#[allow(clippy::wildcard_imports)]
use super::*;
#[derive(SystemParam)]
pub struct PhysicsWorld<'w, 's> {
query_pipeline: ResMut<'w, QueryPipeline>,
colliders: ResMut<'w, ColliderSet>,
#[system_param(ignore)]
marker: PhantomData<&'s usize>,
}
impl<'w, 's> PhysicsWorld<'w, 's> {
#[must_use]
pub fn ray_cast(&self, start: Vec3, ray: Vec3, solid: bool) -> Option<RayCastInfo> {
self.ray_cast_internal(start, ray, solid, CollisionLayers::default(), None)
}
#[must_use]
pub fn ray_cast_with_filter<F>(
&self,
start: Vec3,
ray: Vec3,
solid: bool,
layers: CollisionLayers,
filter: F,
) -> Option<RayCastInfo>
where
F: Fn(Entity) -> bool,
{
self.ray_cast_internal(start, ray, solid, layers, Some(&filter))
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
fn ray_cast_internal(
&self,
start: Vec3,
ray: Vec3,
solid: bool,
layers: CollisionLayers,
filter: Option<&dyn Fn(Entity) -> bool>,
) -> Option<RayCastInfo> {
let direction = ray.try_normalize()?;
let rapier_ray = Ray::new(start.into_rapier(), direction.into_rapier());
let result = self.query_pipeline.cast_ray_and_get_normal(
&*self.colliders,
&rapier_ray,
ray.length(),
solid,
InteractionGroups {
memberships: layers.groups_bits(),
filter: layers.masks_bits(),
},
filter
.map(|filter| {
move |handle: ColliderHandle| -> bool {
self.colliders
.get(handle)
.map(|collider| Entity::from_bits(collider.user_data as u64))
.map_or(false, filter)
}
})
.as_ref()
.map(|x| x as &dyn Fn(ColliderHandle) -> bool),
);
result.map(|(collider_handle, intersection)| {
Some(RayCastInfo {
collision_point: start + direction * intersection.toi,
entity: self
.colliders
.get(collider_handle)
.map(|collider| Entity::from_bits(collider.user_data as u64))?,
normal: intersection.normal.into_bevy(),
})
})?
}
#[must_use]
pub fn shape_cast(
&self,
shape: &CollisionShape,
start_position: Vec3,
start_rotation: Quat,
ray: Vec3,
) -> Option<ShapeCastInfo> {
self.shape_cast_internal(
shape,
start_position,
start_rotation,
ray,
CollisionLayers::default(),
None,
)
}
pub fn shape_cast_with_filter<F>(
&self,
shape: &CollisionShape,
start_position: Vec3,
start_rotation: Quat,
ray: Vec3,
layers: CollisionLayers,
filter: F,
) -> Option<ShapeCastInfo>
where
F: Fn(Entity) -> bool,
{
self.shape_cast_internal(
shape,
start_position,
start_rotation,
ray,
layers,
Some(&filter),
)
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
fn shape_cast_internal(
&self,
shape: &CollisionShape,
start_position: Vec3,
start_rotation: Quat,
ray: Vec3,
layers: CollisionLayers,
filter: Option<&dyn Fn(Entity) -> bool>,
) -> Option<ShapeCastInfo> {
let direction = ray.try_normalize()?;
let collider = shape.collider_builder().build();
let result = self.query_pipeline.cast_shape(
&*self.colliders,
&(start_position, start_rotation).into_rapier(),
&direction.into_rapier(),
collider.shape(),
ray.length(),
InteractionGroups {
memberships: layers.groups_bits(),
filter: layers.masks_bits(),
},
filter
.map(|filter| {
move |handle: ColliderHandle| -> bool {
self.colliders
.get(handle)
.map(|collider| Entity::from_bits(collider.user_data as u64))
.map_or(false, filter)
}
})
.as_ref()
.map(|x| x as &dyn Fn(ColliderHandle) -> bool),
);
result.map(|(collider_handle, toi)| {
let collision_type = match toi.status {
TOIStatus::OutOfIterations | TOIStatus::Converged | TOIStatus::Failed => {
let self_end_position = start_position + direction * toi.toi;
let self_point = toi.witness1.into_bevy();
#[cfg(dim2)]
let self_point = self_point.extend(0.);
let self_normal = toi.normal1.into_bevy();
let other_point = toi.witness2.into_bevy();
#[cfg(dim2)]
let other_point = other_point.extend(0.);
let other_normal = toi.normal2.into_bevy();
ShapeCastCollisionType::Collided(ShapeCastCollisionInfo {
self_end_position,
self_point,
self_normal,
other_point,
other_normal,
})
}
TOIStatus::Penetrating => ShapeCastCollisionType::AlreadyPenetrating,
};
Some(ShapeCastInfo {
entity: self
.colliders
.get(collider_handle)
.map(|collider| Entity::from_bits(collider.user_data as u64))?,
collision_type,
})
})?
}
}
}
#[derive(Clone, Debug)]
pub struct RayCastInfo {
pub collision_point: Vec3,
pub entity: Entity,
pub normal: Vec3,
}
#[derive(Clone, Debug)]
pub struct ShapeCastInfo {
pub entity: Entity,
pub collision_type: ShapeCastCollisionType,
}
#[derive(Clone, Debug)]
pub enum ShapeCastCollisionType {
AlreadyPenetrating,
Collided(ShapeCastCollisionInfo),
}
#[derive(Clone, Debug)]
pub struct ShapeCastCollisionInfo {
pub self_end_position: Vec3,
pub self_point: Vec3,
pub self_normal: Vec3,
pub other_point: Vec3,
pub other_normal: Vec3,
}
pub(crate) fn update_integration_parameters(
physics_steps: Res<'_, PhysicsSteps>,
physics_time: Res<'_, PhysicsTime>,
bevy_time: Res<'_, bevy::time::Time>,
mut integration_parameters: ResMut<'_, IntegrationParameters>,
) {
if matches!(
physics_steps.duration(),
PhysicsStepDuration::MaxDeltaTime(_)
) || physics_steps.is_changed()
|| physics_time.is_changed()
{
integration_parameters.dt = physics_steps
.duration()
.exact(bevy_time.delta())
.as_secs_f32()
* physics_time.scale();
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn step(
mut pipeline: ResMut<'_, PhysicsPipeline>,
mut query_pipeline: ResMut<'_, QueryPipeline>,
gravity: Res<'_, Gravity>,
integration_parameters: Res<'_, IntegrationParameters>,
mut islands: ResMut<'_, IslandManager>,
mut broad_phase: ResMut<'_, BroadPhase>,
mut narrow_phase: ResMut<'_, NarrowPhase>,
mut bodies: ResMut<'_, RigidBodySet>,
mut colliders: ResMut<'_, ColliderSet>,
mut impulse_joints: ResMut<'_, ImpulseJointSet>,
mut multibody_joints: ResMut<'_, MultibodyJointSet>,
mut ccd_solver: ResMut<'_, CCDSolver>,
event_manager: Local<'_, EventManager>,
mut events: ResMut<'_, Events<CollisionEvent>>,
) {
let gravity = Vec3::from(*gravity).into_rapier();
pipeline.step(
&gravity,
&integration_parameters,
&mut islands,
&mut broad_phase,
&mut narrow_phase,
&mut bodies,
&mut colliders,
&mut impulse_joints,
&mut multibody_joints,
&mut ccd_solver,
&(),
&*event_manager,
);
query_pipeline.update(&islands, &bodies, &colliders);
event_manager.fire_events(&narrow_phase, &bodies, &colliders, &mut events);
}
pub(crate) struct EventManager {
recv: Receiver<rapier::prelude::CollisionEvent>,
send: Sender<rapier::prelude::CollisionEvent>,
}
impl EventHandler for EventManager {
fn handle_collision_event(
&self,
_: &RigidBodySet,
_: &ColliderSet,
event: rapier::prelude::CollisionEvent,
_: Option<&ContactPair>,
) {
if let Err(err) = self.send.send(event) {
error!("Failed to handle collision even ({})", err);
}
}
}
impl Default for EventManager {
fn default() -> Self {
let (send, recv) = crossbeam::channel::unbounded();
Self { recv, send }
}
}
impl EventManager {
fn fire_events(
&self,
narrow_phase: &NarrowPhase,
bodies: &RigidBodySet,
colliders: &ColliderSet,
events: &mut Events<CollisionEvent>,
) {
while let Ok(event) = self.recv.try_recv() {
match event {
rapier::prelude::CollisionEvent::Started(h1, h2, _) => {
if let Some((e1, e2)) = Self::data(narrow_phase, bodies, colliders, h1, h2) {
events.send(CollisionEvent::Started(e1, e2));
}
}
rapier::prelude::CollisionEvent::Stopped(h1, h2, _) => {
if let Some((e1, e2)) = Self::data(narrow_phase, bodies, colliders, h1, h2) {
events.send(CollisionEvent::Stopped(e1, e2));
}
}
}
}
}
#[allow(clippy::cast_possible_truncation)]
fn data(
narrow_phase: &NarrowPhase,
bodies: &RigidBodySet,
colliders: &ColliderSet,
h1: ColliderHandle,
h2: ColliderHandle,
) -> Option<(CollisionData, CollisionData)> {
if let (Some(collider1), Some(collider2)) = (colliders.get(h1), colliders.get(h2)) {
if let (Some(rb1), Some(rb2)) = (
collider1.parent().and_then(|parent| bodies.get(parent)),
collider2.parent().and_then(|parent| bodies.get(parent)),
) {
let normals1 =
narrow_phase
.contact_pair(h1, h2)
.into_iter()
.flat_map(|contact_pair| {
contact_pair.manifolds.iter().map(|manifold| {
#[cfg(dim2)]
let z = 0.0;
#[cfg(not(dim2))]
let z = manifold.data.normal.z;
Vec3::new(manifold.data.normal.x, manifold.data.normal.y, z)
})
});
let normals2 =
narrow_phase
.contact_pair(h2, h1)
.into_iter()
.flat_map(|contact_pair| {
contact_pair.manifolds.iter().map(|manifold| {
#[cfg(dim2)]
let z = 0.0;
#[cfg(not(dim2))]
let z = manifold.data.normal.z;
Vec3::new(manifold.data.normal.x, manifold.data.normal.y, z)
})
});
let d1 = CollisionData::new(
Entity::from_bits(rb1.user_data as u64),
Entity::from_bits(collider1.user_data as u64),
collider1.collision_groups().into_bevy(),
normals1,
);
let d2 = CollisionData::new(
Entity::from_bits(rb2.user_data as u64),
Entity::from_bits(collider2.user_data as u64),
collider2.collision_groups().into_bevy(),
normals2,
);
Some(
if Entity::from_bits(rb1.user_data as u64)
< Entity::from_bits(rb2.user_data as u64)
{
(d1, d2)
} else {
(d2, d1)
},
)
} else {
None
}
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use bevy::prelude::*;
use bevy::MinimalPlugins;
use heron_core::CollisionLayers;
use heron_core::RigidBody;
use crate::pipeline::EventManager;
use crate::rapier::prelude::{
ColliderBuilder, ColliderHandle, CollisionEventFlags, RigidBodyBuilder,
};
use crate::RapierPlugin;
use super::*;
struct TestContext {
narrow_phase: NarrowPhase,
bodies: RigidBodySet,
colliders: ColliderSet,
rb_entity_1: Entity,
rb_entity_2: Entity,
collider_entity_1: Entity,
collider_entity_2: Entity,
layers_1: CollisionLayers,
layers_2: CollisionLayers,
handle1: ColliderHandle,
handle2: ColliderHandle,
}
impl Default for TestContext {
fn default() -> Self {
let narrow_phase = NarrowPhase::new();
let mut bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
let rb_entity_1 = Entity::from_bits(0);
let rb_entity_2 = Entity::from_bits(1);
let collider_entity_1 = Entity::from_bits(2);
let collider_entity_2 = Entity::from_bits(3);
let layers_1 = CollisionLayers::from_bits(1, 2);
let layers_2 = CollisionLayers::from_bits(3, 4);
let body1 = bodies.insert(
RigidBodyBuilder::dynamic()
.user_data(rb_entity_1.to_bits().into())
.build(),
);
let body2 = bodies.insert(
RigidBodyBuilder::dynamic()
.user_data(rb_entity_2.to_bits().into())
.build(),
);
let handle1 = colliders.insert_with_parent(
ColliderBuilder::ball(1.0)
.user_data(collider_entity_1.to_bits().into())
.collision_groups(layers_1.into_rapier())
.build(),
body1,
&mut bodies,
);
let handle2 = colliders.insert_with_parent(
ColliderBuilder::ball(1.0)
.user_data(collider_entity_2.to_bits().into())
.collision_groups(layers_2.into_rapier())
.build(),
body2,
&mut bodies,
);
Self {
narrow_phase,
bodies,
colliders,
rb_entity_1,
rb_entity_2,
collider_entity_1,
collider_entity_2,
layers_1,
layers_2,
handle1,
handle2,
}
}
}
#[test]
fn contact_started_fires_collision_started() {
let manager = EventManager::default();
let context = TestContext::default();
manager
.send
.send(rapier::prelude::CollisionEvent::Started(
context.handle1,
context.handle2,
CollisionEventFlags::all(),
))
.unwrap();
let mut events = Events::<CollisionEvent>::default();
manager.fire_events(
&context.narrow_phase,
&context.bodies,
&context.colliders,
&mut events,
);
let events: Vec<CollisionEvent> = events.get_reader().iter(&events).cloned().collect();
assert_eq!(events.len(), 1);
let event = &events[0];
assert!(matches!(event, CollisionEvent::Started(_, _)));
assert_eq!(
event.collision_shape_entities(),
(context.collider_entity_1, context.collider_entity_2)
);
}
#[test]
fn contact_stopped_fires_collision_stopped() {
let manager = EventManager::default();
let context = TestContext::default();
manager
.send
.send(rapier::prelude::CollisionEvent::Stopped(
context.handle1,
context.handle2,
CollisionEventFlags::all(),
))
.unwrap();
let mut events = Events::<CollisionEvent>::default();
manager.fire_events(
&context.narrow_phase,
&context.bodies,
&context.colliders,
&mut events,
);
let events: Vec<CollisionEvent> = events.get_reader().iter(&events).cloned().collect();
assert_eq!(events.len(), 1);
let event = &events[0];
assert!(matches!(event, CollisionEvent::Stopped(_, _)));
assert_eq!(
event.collision_shape_entities(),
(context.collider_entity_1, context.collider_entity_2)
);
}
#[test]
fn contains_rigid_body_entities() {
let manager = EventManager::default();
let context = TestContext::default();
manager
.send
.send(rapier::prelude::CollisionEvent::Started(
context.handle1,
context.handle2,
CollisionEventFlags::all(),
))
.unwrap();
let mut events = Events::<CollisionEvent>::default();
manager.fire_events(
&context.narrow_phase,
&context.bodies,
&context.colliders,
&mut events,
);
assert_eq!(
events
.get_reader()
.iter(&events)
.next()
.unwrap()
.rigid_body_entities(),
(context.rb_entity_1, context.rb_entity_2)
);
}
#[test]
fn contains_collision_layers() {
let manager = EventManager::default();
let context = TestContext::default();
manager
.send
.send(rapier::prelude::CollisionEvent::Started(
context.handle1,
context.handle2,
CollisionEventFlags::all(),
))
.unwrap();
let mut events = Events::<CollisionEvent>::default();
manager.fire_events(
&context.narrow_phase,
&context.bodies,
&context.colliders,
&mut events,
);
assert_eq!(
events
.get_reader()
.iter(&events)
.next()
.unwrap()
.collision_layers(),
(context.layers_1, context.layers_2)
);
}
#[derive(Component)]
struct RayCastTestCollider;
fn setup_ray_cast_test_app() -> App {
fn setup(mut commands: Commands<'_, '_>) {
commands.spawn_bundle((
CollisionShape::Cuboid {
half_extends: Vec3::new(10., 10., 10.),
border_radius: None,
},
RigidBody::Static,
Transform::from_xyz(0., 100., 0.),
GlobalTransform::default(),
RayCastTestCollider,
));
}
let mut app = App::new();
app.add_plugins(MinimalPlugins)
.add_plugin(RapierPlugin)
.add_startup_system(setup);
app
}
#[test]
fn ray_cast_hit() {
fn ray_cast(
mut runs: Local<'_, i32>,
physics_world: PhysicsWorld<'_, '_>,
test_colliders: Query<'_, '_, (), With<RayCastTestCollider>>,
) {
if *runs == 0 {
*runs += 1;
return;
}
let result = physics_world.ray_cast(Vec3::default(), Vec3::new(0., 200., 0.), true);
if let Some(info) = result {
assert!(info.collision_point.distance(Vec3::new(0., 90., 0.)) < 0.1);
assert!(test_colliders.get(info.entity).is_ok());
} else {
panic!("Ray cast did not collide when we expected it to");
}
}
let mut app = setup_ray_cast_test_app();
app.add_system(ray_cast);
app.update();
app.update();
}
#[test]
fn ray_cast_miss() {
fn ray_cast(mut runs: Local<'_, i32>, physics_world: PhysicsWorld<'_, '_>) {
if *runs == 0 {
*runs += 1;
return;
}
let result = physics_world.ray_cast(Vec3::default(), Vec3::new(0., -200., 0.), true);
assert!(result.is_none());
}
let mut app = setup_ray_cast_test_app();
app.add_system(ray_cast);
app.update();
app.update();
}
#[test]
fn shape_cast_hit() {
fn ray_cast(
mut runs: Local<'_, i32>,
physics_world: PhysicsWorld<'_, '_>,
test_colliders: Query<'_, '_, (), With<RayCastTestCollider>>,
) {
if *runs == 0 {
*runs += 1;
return;
}
let result = physics_world.shape_cast(
&CollisionShape::Cuboid {
half_extends: Vec3::new(10., 10., 10.),
border_radius: None,
},
Vec3::default(),
Quat::default(),
Vec3::new(0., 200., 0.),
);
if let Some(info) = result {
assert!(test_colliders.get(info.entity).is_ok());
if let ShapeCastCollisionType::Collided(info) = info.collision_type {
assert!(info.self_end_position.distance(Vec3::new(0., 80., 0.)) < 0.1);
} else {
panic!("Shape cast did not collide the way we thought it would");
}
} else {
panic!("Shape cast did not collide when we expected it to");
}
}
let mut app = setup_ray_cast_test_app();
app.add_system(ray_cast);
app.update();
app.update();
}
#[test]
fn shape_cast_miss() {
fn ray_cast(mut runs: Local<'_, i32>, physics_world: PhysicsWorld<'_, '_>) {
if *runs == 0 {
*runs += 1;
return;
}
let result = physics_world.shape_cast(
&CollisionShape::Cuboid {
half_extends: Vec3::new(10., 10., 10.),
border_radius: None,
},
Vec3::default(),
Quat::default(),
Vec3::new(0., -200., 0.),
);
assert!(result.is_none());
}
let mut app = setup_ray_cast_test_app();
app.add_system(ray_cast);
app.update();
app.update();
}
}