diff --git a/CHANGELOG.md b/CHANGELOG.md index fe983358..07e6e957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,12 @@ In the future it will be extended to define more physics properties, like the fr Since the restitution is now defined in `PhysicMaterial`, the `Restitution` component has been removed. +### ⚠ Kinematic bodies + +There is a new variant to `BodyType`: `Kinematic`. That makes possible to create "kinematic" bodies. +A kinematic body is controlled programmatically (usually by updating the transform) and affect the other bodies normally, +bot is not affected by them. + ### ⚠ Dependency requirements updated (breaking) diff --git a/README.md b/README.md index 3fec4c99..92b41b16 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ fn spawn(commands: &mut Commands) { .with(Body::Sphere { radius: 10.0 }) // Optionally define a type (if absent, the body will be *dynamic*) - .with(BodyType::Static) + .with(BodyType::Kinematic) // Optionally define the velocity (works only with dynamic and kinematic bodies) .with(Velocity::from(Vec2::unit_x() * 2.0)); diff --git a/core/src/lib.rs b/core/src/lib.rs index 0fb6653c..3d5f940e 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -151,20 +151,35 @@ impl Default for Body { /// ``` #[derive(Debug, Copy, Clone, Eq, PartialEq, Reflect)] pub enum BodyType { - /// A dynamic body is normally affected by physic forces and affect the other bodies too. + /// A dynamic body is normally affected by physic forces and affect the other bodies normally too. /// /// This is the most "natural" type in the sense that, in the real life, everything is dynamic. + /// + /// It is the default type. Dynamic, /// A static body is not affected by physic forces and doesn't move. But it does affect the other bodies. /// /// This effectively behaves like a dynamic body with infinite mass and zero velocity. /// - /// It is especially useful to model terrain and static obstacles. + /// It is well suited for terrain and static obstacles. Static, + /// A kinematic body is not moved by the physics engine. But it can have user-defined velocity. + /// + /// It affects the other bodies normally, but is not affected by them. + /// + /// If the transform is updated, then a velocity will be automatically calculated, producing + /// realistic interaction with other bodies. + /// + /// It can also have a velocity be applied. + /// + /// It is well suited fo moving-platforms. + Kinematic, + /// A sensor is not affected by physics forces and doesn't affect other bodies either. - /// Other bodies will be able to penetrate the sensor. + /// + /// Other bodies will be able to penetrate the sensor. But it still participate in collision events. /// /// A sensor is useful when we are only interested in collision events. /// One may for example add a sensor to detect when the player reach a certain area. @@ -177,6 +192,17 @@ impl Default for BodyType { } } +impl BodyType { + /// Returns true if this body type can be moved by [`Velocity`] + #[must_use] + pub fn can_have_velocity(self) -> bool { + match self { + BodyType::Dynamic | BodyType::Kinematic => true, + BodyType::Static | BodyType::Sensor => false, + } + } +} + /// An event fired when the collision state between two entities changed /// /// # Example diff --git a/core/src/velocity.rs b/core/src/velocity.rs index 045de4b4..839c2803 100644 --- a/core/src/velocity.rs +++ b/core/src/velocity.rs @@ -2,6 +2,7 @@ use bevy::math::prelude::*; use bevy::reflect::prelude::*; use crate::utils::NearZero; +use std::ops::{Mul, MulAssign}; /// Component that defines the linear and angular velocity. /// @@ -133,6 +134,30 @@ impl NearZero for Velocity { } } +impl MulAssign for AxisAngle { + fn mul_assign(&mut self, rhs: f32) { + self.0 = self.0 * rhs; + } +} + +impl Mul for AxisAngle { + type Output = Self; + + fn mul(mut self, rhs: f32) -> Self::Output { + self *= rhs; + self + } +} + +impl Mul for f32 { + type Output = AxisAngle; + + fn mul(self, mut rhs: AxisAngle) -> Self::Output { + rhs *= self; + rhs + } +} + impl AxisAngle { /// Create a new axis-angle #[inline] diff --git a/examples/demo.rs b/examples/demo.rs index e1bd2064..1e5adc8d 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -55,7 +55,8 @@ fn spawn(commands: &mut Commands, mut materials: ResMut>) }) // Add an initial velocity. (it is also possible to read/mutate this component later) .with( - Velocity::from(Vec2::unit_x() * 300.0).with_angular(AxisAngle::new(Vec3::unit_z(), PI)), + Velocity::from(Vec2::unit_x() * 300.0) + .with_angular(AxisAngle::new(Vec3::unit_z(), -PI)), ) // Define restitution (so that it bounces) .with(PhysicMaterial { diff --git a/rapier/src/body.rs b/rapier/src/body.rs index 039adbbc..3219b271 100644 --- a/rapier/src/body.rs +++ b/rapier/src/body.rs @@ -134,10 +134,12 @@ pub(crate) fn update_rapier_position( ) { for (transform, handle) in query.iter() { if let Some(body) = bodies.get_mut(handle.rigid_body) { - body.set_position( - (transform.translation, transform.rotation).into_rapier(), - true, - ); + let isometry = (transform.translation, transform.rotation).into_rapier(); + if body.is_kinematic() { + body.set_next_kinematic_position(isometry); + } else { + body.set_position(isometry, true); + } } } } @@ -155,7 +157,7 @@ pub(crate) fn update_bevy_transform( >, ) { for (mut local, mut global, handle, body_type) in query.iter_mut() { - if !matches!(body_type.cloned().unwrap_or_default(), BodyType::Dynamic) { + if !body_type.cloned().unwrap_or_default().can_have_velocity() { continue; } @@ -245,6 +247,7 @@ fn body_status(body_type: BodyType) -> BodyStatus { match body_type { BodyType::Dynamic => BodyStatus::Dynamic, BodyType::Static | BodyType::Sensor => BodyStatus::Static, + BodyType::Kinematic => BodyStatus::Kinematic, } } diff --git a/rapier/src/lib.rs b/rapier/src/lib.rs index 185dc190..10e8352a 100644 --- a/rapier/src/lib.rs +++ b/rapier/src/lib.rs @@ -116,7 +116,7 @@ impl Plugin for RapierPlugin { .stage(heron_core::stage::ROOT, |schedule: &mut Schedule| { schedule .add_stage( - "heron-step", + "heron-pre-step", SystemStage::serial() .with_system( bevy::transform::transform_propagate_system::transform_propagate_system @@ -125,9 +125,14 @@ impl Plugin for RapierPlugin { .with_system(body::remove.system()) .with_system(body::recreate_collider.system()) .with_system(body::update_rapier_position.system()) - .with_system(body::update_rapier_status.system()) .with_system(velocity::update_rapier_velocity.system()) - .with_system(body::create.system()) + .with_system(body::update_rapier_status.system()) + .with_system(body::create.system()), + ) + .add_stage( + "heron-step", + SystemStage::serial() + .with_system(velocity::apply_velocity_to_kinematic_bodies.system()) .with_system(pipeline::step.system()), ) .add_stage( diff --git a/rapier/src/velocity.rs b/rapier/src/velocity.rs index 7bc05ddf..5aa7f16a 100644 --- a/rapier/src/velocity.rs +++ b/rapier/src/velocity.rs @@ -1,25 +1,50 @@ use bevy::ecs::prelude::*; +use bevy::math::prelude::*; use heron_core::utils::NearZero; -use heron_core::Velocity; +use heron_core::{BodyType, Velocity}; use crate::convert::{IntoBevy, IntoRapier}; -use crate::rapier::dynamics::RigidBodySet; +use crate::rapier::dynamics::{IntegrationParameters, RigidBodySet}; use crate::BodyHandle; pub(crate) fn update_rapier_velocity( mut bodies: ResMut<'_, RigidBodySet>, - velocities: Query<'_, (&BodyHandle, &Velocity), Changed>, + query: Query<'_, (&BodyHandle, Option<&BodyType>, &Velocity), Changed>, ) { - for (handle, velocity) in velocities.iter() { + let dynamic_bodies = query.iter().filter(|(_, body_type, _)| { + matches!(body_type.cloned().unwrap_or_default(), BodyType::Dynamic) + }); + + for (handle, _, velocity) in dynamic_bodies { if let Some(body) = bodies.get_mut(handle.rigid_body) { - let wake_up = velocity.is_near_zero(); + let wake_up = !velocity.is_near_zero(); body.set_linvel(velocity.linear.into_rapier(), wake_up); body.set_angvel(velocity.angular.into_rapier(), wake_up); } } } +pub(crate) fn apply_velocity_to_kinematic_bodies( + mut bodies: ResMut<'_, RigidBodySet>, + integration_parameters: Res<'_, IntegrationParameters>, + query: Query<'_, (&BodyHandle, &BodyType, &Velocity)>, +) { + let delta_time = integration_parameters.dt; + let kinematic_bodies = query + .iter() + .filter(|(_, body_type, _)| matches!(body_type, BodyType::Kinematic)); + + for (handle, _, velocity) in kinematic_bodies { + if let Some(body) = bodies.get_mut(handle.rigid_body) { + let (mut translation, mut rotation) = body.position().into_bevy(); + translation += velocity.linear * delta_time; + rotation *= Quat::from(velocity.angular * delta_time); + body.set_next_kinematic_position((translation, rotation).into_rapier()) + } + } +} + pub(crate) fn update_velocity_component( bodies: Res<'_, RigidBodySet>, mut velocities: Query<'_, (&BodyHandle, &mut Velocity)>, diff --git a/rapier/tests/bodies.rs b/rapier/tests/bodies.rs index 3883125b..d1d612f9 100644 --- a/rapier/tests/bodies.rs +++ b/rapier/tests/bodies.rs @@ -10,12 +10,14 @@ use bevy::core::CorePlugin; use bevy::prelude::*; use bevy::reflect::TypeRegistryArc; -use heron_core::Body; +use heron_core::{Body, BodyType}; use heron_rapier::convert::{IntoBevy, IntoRapier}; use heron_rapier::rapier::dynamics::{IntegrationParameters, RigidBodySet}; use heron_rapier::rapier::geometry::ColliderSet; use heron_rapier::{BodyHandle, RapierPlugin}; +use rstest::rstest; + fn test_app() -> App { let mut builder = App::build(); builder @@ -200,8 +202,13 @@ fn despawn_body_entity() { assert_eq!(colliders.len(), 0); } -#[test] -fn update_bevy_transform() { +#[rstest( + body_type, + case(Some(BodyType::Dynamic)), + case(Some(BodyType::Kinematic)), + case(None) +)] +fn update_bevy_transform(body_type: Option) { let mut app = test_app(); let entity = app.world.spawn(( @@ -210,6 +217,10 @@ fn update_bevy_transform() { GlobalTransform::default(), )); + if let Some(body_type) = body_type { + app.world.insert_one(entity, body_type).unwrap(); + } + let translation = Vec3::new(1.0, 2.0, 3.0); let rotation = Quat::from_axis_angle(Vec3::unit_z(), PI / 2.0); diff --git a/rapier/tests/body_types.rs b/rapier/tests/body_types.rs index 428fecf8..120648b7 100644 --- a/rapier/tests/body_types.rs +++ b/rapier/tests/body_types.rs @@ -64,6 +64,26 @@ fn create_static_body() { assert!(body.is_static()) } +#[test] +fn create_kinematic_body() { + let mut app = test_app(); + + let entity = app.world.spawn(( + GlobalTransform::default(), + Body::Sphere { radius: 10.0 }, + BodyType::Kinematic, + )); + + app.update(); + + let bodies = app.resources.get::().unwrap(); + let body = bodies + .get(app.world.get::(entity).unwrap().rigid_body()) + .unwrap(); + + assert!(body.is_kinematic()) +} + #[test] fn create_sensor_body() { let mut app = test_app(); diff --git a/rapier/tests/velocity.rs b/rapier/tests/velocity.rs index 3d62df49..f0557990 100644 --- a/rapier/tests/velocity.rs +++ b/rapier/tests/velocity.rs @@ -145,3 +145,40 @@ fn velocity_is_updated_to_reflect_rapier_world() { assert_eq!(angular, velocity.angular.into()); } + +#[test] +fn velocity_can_move_kinematic_bodies() { + let mut app = test_app(); + + let linear = Vec3::new(1.0, 2.0, 3.0); + let angular = AxisAngle::new(Vec3::unit_z(), PI * 0.5); + + let entity = app.world.spawn(( + GlobalTransform::from_rotation(Quat::from_axis_angle(Vec3::unit_z(), 0.0)), + Body::Sphere { radius: 1.0 }, + BodyType::Kinematic, + Velocity::from_linear(linear).with_angular(angular), + )); + + app.update(); + + let bodies = app.resources.get::().unwrap(); + let body = bodies + .get(app.world.get::(entity).unwrap().rigid_body()) + .unwrap(); + + let position = body.position().translation.into_bevy(); + let rotation = body.position().rotation.into_bevy(); + + assert_eq!(position.x, linear.x); + assert_eq!(position.y, linear.y); + + #[cfg(feature = "3d")] + assert_eq!(position.z, linear.z); + + let angular: Quat = angular.into(); + assert!((rotation.x - angular.x).abs() < 0.00001); + assert!((rotation.y - angular.y).abs() < 0.00001); + assert!((rotation.z - angular.z).abs() < 0.00001); + assert!((rotation.w - angular.w).abs() < 0.00001); +} diff --git a/src/lib.rs b/src/lib.rs index 6d96c2e7..33ac12a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,31 +63,68 @@ //! .with(Body::Sphere { radius: 10.0 }) //! //! // Optionally define a type (if absent, the body will be *dynamic*) -//! .with(BodyType::Static) +//! .with(BodyType::Kinematic) //! //! // Optionally define the velocity (works only with dynamic and kinematic bodies) //! .with(Velocity::from(Vec2::unit_x() * 2.0)); //! } //! ``` //! +//! ## Run systems synchronously with the physics step +//! +//! The physics step runs at fixed rate (60 times per second by default) and is out of sync of the +//! bevy frame. +//! +//! But modifying any physics component (such as the transform or velocity) **must** be done synchronously with +//! the physics step. +//! +//! The simplest way is to add these systems with `add_physics_system`: +//! +//! ```no_run +//! # use bevy::prelude::*; +//! # use heron::prelude::*; +//! App::build() +//! .add_plugins(DefaultPlugins) +//! .add_plugin(PhysicsPlugin::default()) +//! +//! // This system should NOT update transforms, velocities and other physics components +//! // In other game engines this would be the "update" function +//! .add_system(cannot_update_physics.system()) +//! +//! // This system can update transforms, velocities and other physics components +//! // In other game engines this would be the "physics update" function +//! .add_physics_system(update_velocities.system()) +//! # .run(); +//! # fn cannot_update_physics() {} +//! # fn update_velocities() {} +//! ``` +//! //! ## Control the position //! //! When creating games, it is often useful to interact with the physics engine and move bodies programatically. -//! For this, you have two options: Updating the `Transform` or applying a [`Velocity`] +//! For this, you have two options: Updating the `Transform` or applying a [`Velocity`]. +//! +//! ### Option 1: Update the Transform //! -//! ### Option 1: Update the Transform (teleport) +//! For kinematic bodies ([`BodyType::Kinematic`]), if the transform is udpated, +//! the body is moved and get an automatically calculated velocity. Physics rules will be applied normally. +//! Updating the transform is a good way to move a kinematic body. //! -//! If the `GlobalTransform` is modified (generally as an effect of modifying the `Transform` component), -//! then the rigid body will be *teleported* to the new position/rotation, **ignoring physic rules**. +//! For other type of bodies, if the transform is updated, +//! the rigid body will be *teleported* to the new position/rotation, **ignoring physic rules**. //! //! ### Option 2: Use the Velocity component //! -//! For [`BodyType::Dynamic`] bodies **only**, one can add a [`Velocity`] component to the entity, +//! For [`BodyType::Dynamic`] and [`BodyType::Kinematic`] bodies **only**, one can add a [`Velocity`] component to the entity, //! that will move the body over time. Physics rules will be applied normally. //! +//! Note that the velocity component is updated by heron to always reflects the current velocity. +//! +//! Defining/updating the velocity is a good way to interact with dynamic bodies. +//! //! ## See also //! -//! * The different [`BodyType`] (dynamic, static or sensor) +//! * The different [`BodyType`] (dynamic, static, kinematic or sensor) //! * How to define the world's [`Gravity`] //! * How to define the [`PhysicMaterial`] //! * How to listen to [`CollisionEvent`]