Skip to main content

Common recipes

This page provides code snippets that might be relevant for typical usages of the libraries in games.

Making a moving platform​

A moving platform must push the objects resting on it without being pushed back by them, which is exactly what a kinematic rigid-body does: it is moved by your own code, and the solver treats it as if it is immune to gravity and external forces.

A platform following a path is generally position-based: you give it the position it must reach at the end of the next timestep, and the engine derives the velocity needed to get there, so the objects on top of it are pushed with the right velocity:

let (platform_handle, _) = world.insert(
RigidBodyBuilder::kinematic_position_based().translation(Vector::new(0.0, 1.0, 0.0)),
ColliderBuilder::cuboid(2.0, 0.1, 2.0),
);

for step in 0..200 {
// Setting the next position of the platform, once per timestep.
let time = step as f32 * world.integration_parameters.dt;
let platform = &mut world.bodies[platform_handle];
platform.set_next_kinematic_translation(Vector::new(time.sin() * 2.0, 1.0, 0.0));
world.step();
}
fn setup_moving_platform(mut commands: Commands) {
commands.spawn((
RigidBody::KinematicPositionBased,
Transform::from_xyz(0.0, 1.0, 5.0),
Collider::cuboid(2.0, 0.1, 2.0),
MovingPlatform,
));
}

fn move_platform(time: Res<Time>, mut platforms: Query<&mut Transform, With<MovingPlatform>>) {
for mut transform in platforms.iter_mut() {
// Setting the next position of the platform: the physics engine derives the
// velocity needed to reach it at the end of the next timestep.
transform.translation.x = time.elapsed_secs().sin() * 2.0;
}
}
R3RigidBodyDesc platform_body = r3KinematicPositionBasedRigidBodyDesc();
platform_body.position.translation = r3Vector(0.0, 1.0, 0.0);
R3RigidBodyHandle platform_handle = r3InsertRigidBody(world, &platform_body);
R3ColliderDesc platform_collider_desc = r3CuboidColliderDesc(r3Vector(2.0, 0.1, 2.0));
r3InsertCollider(platform_handle, &platform_collider_desc);

for (int step = 0; step < 200; step++) {
/* Setting the next position of the platform, once per timestep. */
R3Real time = (R3Real)step * r3TimeStep(world);
r3RigidBody_SetNextKinematicTranslation(platform_handle, r3Vector(sinf(time) * 2.0f, 1.0, 0.0));
r3Step(world, NULL, NULL);
}
platform_handle = world.add_body(
rp.RigidBody.kinematic_position_based(translation=(0.0, 1.0, 0.0)),
colliders=[rp.Collider.cuboid(2.0, 0.1, 2.0)],
)

for step in range(200):
# Setting the next position of the platform, once per timestep.
time = step * world.integration_parameters.dt
platform = world.rigid_bodies[platform_handle]
platform.set_next_kinematic_translation((math.sin(time) * 2.0, 1.0, 0.0))
world.step()

Alternately, if the platform's rigid-body was created with the RigidBodyType::KinematicVelocityBasedRigidBody::KinematicVelocityBasedR3_KINEMATIC_VELOCITY_BASEDRigidBodyType.KINEMATIC_VELOCITY_BASED type, then it needs to be controlled by setting its velocity (with its Velocity component) (with r3RigidBody_SetLinvel and r3RigidBody_SetAngvel) (with its linvel and angvel properties) directly instead of a target position.

warning

Don't move a kinematic body by setting its position directly: this teleports it, so it goes through whatever is in the way instead of pushing it.Modifying the Transform of a position-based kinematic body sets its next kinematic position. However, modifying the Transform of a velocity-based kinematic body teleports it, so it goes through whatever is in the way instead of pushing it.Don't move a kinematic body by setting its position directly (e.g. with r3RigidBody_SetTranslation): this teleports it, so it goes through whatever is in the way instead of pushing it.Don't move a kinematic body by setting its position directly (e.g. with its translation property): this teleports it, so it goes through whatever is in the way instead of pushing it. Note as well that two kinematic bodies never collide with each other, and that a kinematic body pushing another kinematic body has no effect.

Making a one-way platform​

A one-way platform lets the character pass through it from below and holds it from above. This can be done by looking at the contacts before they reach the solver, with the contact modification hook, and by discarding those whose normal isn't the one the platform accepts. Rapier provides a helper function update_as_oneway_platform for that (ContactModificationContextView::update_as_oneway_platform) (r3ContactModificationContext_UpdateAsOnewayPlatform) (ContactModificationContext.update_as_oneway_platform):

struct OneWayPlatform {
platform: ColliderHandle,
}

impl PhysicsHooks for OneWayPlatform {
fn modify_solver_contacts(&self, context: &mut ContactModificationContext) {
// Keep only the contacts pushing along the local +y axis of the platform; the other
// ones (the character arriving from below) are discarded. The normal is expressed in
// the frame of the first collider of the pair, hence the flip.
let allowed_local_n1 = if context.collider1 == self.platform {
Vector::Y
} else {
-Vector::Y
};
context.update_as_oneway_platform(allowed_local_n1, 0.1);
}
}
#[derive(Component)]
struct OneWayPlatform;

#[derive(SystemParam)]
struct OneWayPlatformHooks<'w, 's> {
platforms: Query<'w, 's, (), With<OneWayPlatform>>,
}

impl BevyPhysicsHooks for OneWayPlatformHooks<'_, '_> {
fn modify_solver_contacts(&self, mut context: ContactModificationContextView) {
// Keep only the contacts pushing along the local +y axis of the platform; the other
// ones (the character arriving from below) are discarded. The normal is expressed in
// the frame of the first collider of the pair, hence the flip.
let allowed_local_n1 = if self.platforms.contains(context.collider1()) {
Vec3::Y
} else if self.platforms.contains(context.collider2()) {
-Vec3::Y
} else {
return;
};
context.update_as_oneway_platform(allowed_local_n1, 0.1);
}
}
typedef struct OneWayPlatform {
R3ColliderHandle platform;
} OneWayPlatform;

static int same_collider(R3ColliderHandle a, R3ColliderHandle b) {
return a.world == b.world && a.index == b.index && a.generation == b.generation;
}

static void RAPIER_CALL one_way_platform(void *user_data, const R3ReadContext *read,
R3ColliderHandle collider1, R3ColliderHandle collider2,
R3ContactModificationContext *context) {
(void)read;
(void)collider2;
const OneWayPlatform *hook = user_data;
/* Keep only the contacts pushing along the local +y axis of the platform; the other
* ones (the character arriving from below) are discarded. The normal is expressed in
* the frame of the first collider of the pair, hence the flip. */
R3Vector allowed_local_n1 =
same_collider(collider1, hook->platform) ? r3Vector(0.0, 1.0, 0.0) : r3Vector(0.0, -1.0, 0.0);
r3ContactModificationContext_UpdateAsOnewayPlatform(context, allowed_local_n1, 0.1);
}
class OneWayPlatform:
def __init__(self, platform):
self.platform = platform

def modify_solver_contacts(self, context):
# Keep only the contacts pushing along the local +y axis of the platform; the other
# ones (the character arriving from below) are discarded. The normal is expressed in
# the frame of the first collider of the pair, hence the flip.
if context.collider1 == self.platform:
allowed_local_n1 = (0.0, 1.0, 0.0)
else:
allowed_local_n1 = (0.0, -1.0, 0.0)
context.update_as_oneway_platform(allowed_local_n1, 0.1)

The hooks are then given to the world at each timestepthe RapierPhysicsPlugin as its type parameterr3Step at each timestep, as the modify_solver_contacts_context callback of an R3PhysicsHooksthe world, by assigning them to its physics_hooks property, and the platform's collider is flagged as asking for them:

// The hooks are only called for the colliders asking for them.
let platform_collider = world.bodies[platform_handle].colliders()[0];
world.colliders[platform_collider].set_active_hooks(ActiveHooks::MODIFY_SOLVER_CONTACTS);

let hooks = OneWayPlatform {
platform: platform_collider,
};
world.step_with_events(&hooks, &());
.add_plugins((
DefaultPlugins,
// The hooks are given to the physics plugin.
RapierPhysicsPlugin::<OneWayPlatformHooks>::default(),
))
fn setup_one_way_platform(mut commands: Commands) {
// The hooks are only called for the colliders asking for them.
commands.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
Collider::cuboid(2.0, 0.1, 2.0),
ActiveHooks::MODIFY_SOLVER_CONTACTS,
OneWayPlatform,
));
}
/* The hooks are only called for the colliders asking for them. */
R3ColliderHandle platform_collider;
r3RigidBody_Colliders(platform_handle, &platform_collider, 1);
r3Collider_SetActiveHooks(platform_collider, R3_MODIFY_SOLVER_CONTACTS);

OneWayPlatform platform = {platform_collider};
R3PhysicsHooks hooks = {0};
hooks.user_data = &platform;
hooks.modify_solver_contacts_context = one_way_platform;
r3Step(world, &hooks, NULL);
# The hooks are only called for the colliders asking for them.
platform_collider = world.rigid_bodies[platform_handle].colliders[0]
world.colliders[platform_collider].active_hooks = rp.ActiveHooks.MODIFY_SOLVER_CONTACTS

world.physics_hooks = OneWayPlatform(platform_collider)
world.step()
info

The normal given to update_as_oneway_platform is expressed in the local frame of the first collider of the pair, therefore it must be flipped when the platform happens to be the second one. Don't forget to give the platform's collider the ActiveHooks::MODIFY_SOLVER_CONTACTSR3_MODIFY_SOLVER_CONTACTS active hooks, otherwise the hook is never called for it.

info

The normal given to update_as_oneway_platform is expressed in the local frame of the first collider of the pair, therefore it must be flipped when the platform happens to be the second one. Don't forget to give the platform's collider the ActiveHooks.MODIFY_SOLVER_CONTACTS active hooks, otherwise the hook is never called for it.

Simulating a conveyor belt​

A conveyor belt is a surface that drags what rests on it without moving itself. This is modeled by an artificial surface velocity, which is set on the solver contacts using a contact modification hook (with r3ContactModificationContext_SetTangentVelocity) (with ContactModificationContext.set_tangent_velocity).

struct ConveyorBelt;

impl PhysicsHooks for ConveyorBelt {
fn modify_solver_contacts(&self, context: &mut ContactModificationContext) {
if let Some(rigid) = context.rigid_mut() {
for contact in rigid.solver_contacts.iter_mut() {
// The belt drags the objects along the world-space z axis at 12 m/s.
contact.tangent_velocity.z = 12.0;
}
}
}
}
#[derive(SystemParam)]
struct ConveyorBeltHooks<'w, 's> {
belts: Query<'w, 's, (), With<ConveyorBelt>>,
}

#[derive(Component)]
struct ConveyorBelt;

impl BevyPhysicsHooks for ConveyorBeltHooks<'_, '_> {
fn modify_solver_contacts(&self, mut context: ContactModificationContextView) {
if !self.belts.contains(context.collider1()) && !self.belts.contains(context.collider2())
{
return;
}
if let Some(contacts) = context.solver_contacts_mut() {
for contact in contacts.iter_mut() {
// The belt drags the objects along the world-space z axis at 12 m/s.
contact.tangent_velocity.z = 12.0;
}
}
}
}
static void RAPIER_CALL conveyor_belt(void *user_data, const R3ReadContext *read,
R3ColliderHandle collider1, R3ColliderHandle collider2,
R3ContactModificationContext *context) {
(void)user_data;
(void)read;
(void)collider1;
(void)collider2;
/* The belt drags the objects along the world-space z axis at 12 m/s. */
r3ContactModificationContext_SetTangentVelocity(context, r3Vector(0.0, 0.0, 12.0));
}
class ConveyorBelt:
def __init__(self, belt):
self.belt = belt

def modify_solver_contacts(self, context):
# The belt drags the objects along the world-space z axis at 12 m/s. The tangent
# velocity is the one of the surface of the second collider relative to the first
# one, hence the flip when the belt is the second collider.
if context.collider1 == self.belt:
context.set_tangent_velocity((0.0, 0.0, 12.0))
else:
context.set_tangent_velocity((0.0, 0.0, -12.0))

The tangent velocity given to set_tangent_velocity is the velocity of the surface of the second collider of the pair relative to the surface of the first one, therefore it must be flipped when the belt happens to be the second one. Like the one-way platform, the belt's collider must be given the ActiveHooks.MODIFY_SOLVER_CONTACTS active hooks. Keep in mind as well that the hooks aren't called for the contacts of sleeping rigid-bodies: an object put to sleep on the belt before it started moving must be woken up, e.g., with RigidBody.wake_up.