Tainted\\Coders

Bevy Physics: Rapier

Bevy version: 0.14Last updated:

Bevy has no official physics engine. Other engines like Unreal, Godot, Unity, etc all usually have some default implementation but in Bevy we are left to choose for ourselves.

Physics simulations are common in games and the behaviour is quite generic. This makes it common to use a library instead of writing our own.

Although there are other options for physics such as bevy_xpbd the most complete choice for adding physics in a Bevy game currently is the bevy_rapier plugin.

This plugin is what helps you add the Rapier physics engine to your game in a way that will automatically synchronize with Bevy's ECS.

The overall idea of how Rapier works as a plugin for Bevy is that it will set up two separate worlds that bevy_rapier links together:

  1. An internal representation of the world in Rapier
  2. Which gets propagated to your game world in Bevy

How does Rapier work?

Rapier is an open source physics engine written in Rust. This is more convenient than calling out to a C library as each calculation can be done within the context of Rust.

The library itself is actually split into two crates:

  1. rapier2d for 2D physics
  2. rapier3d for 3D physics

Rapier is built on top of the nalgebra crate. It makes no assumptions about the kind of game engine you are using. It is not bound up into your choice of ECS or object oriented game development.

The primitives it uses to perform calculations are provided by nalgebra:

This is slightly unfortunate in that Bevy already uses glam. But we can convert primitives from either library into each other if needed.

Simulating physics without Bevy

To explore the rapier library we can start without any Bevy context. All we need to do is use the prelude and create our physical bodies:

use rapier2d::prelude::*;

fn main() {
  let mut rigid_body_set = RigidBodySet::new();

  let rigid_body = RigidBodyBuilder::dynamic()
    .translation(vector![0.0, 10.0])
    .build();

  let rigid_body_handle = rigid_body_set.insert(rigid_body);

  // ... rest of the code will go here
}

Then we need to create a PhysicsPipeline which will run a series of transformations to simulate our physics when we call step:

let mut physics_pipeline = PhysicsPipeline::new();

And we will need some supporting components for our simulation:

let gravity = vector![0.0, -9.81];
let integration_parameters = IntegrationParameters::default();
let mut collider_set = ColliderSet::new();
let mut island_manager = IslandManager::new();
let mut broad_phase = DefaultBroadPhase::new();
let mut narrow_phase = NarrowPhase::new();
let mut impulse_joint_set = ImpulseJointSet::new();
let mut multibody_joint_set = MultibodyJointSet::new();
let mut ccd_solver = CCDSolver::new();
let physics_hooks = ();
let event_handler = ();

Our PhysicsPipeline gives us an interface to run each step of our simulation. In this example we can simulate 200 ticks of our game:

// Run the game loop, stepping the simulation once per frame.
for _ in 0..200 {
  physics_pipeline.step(
    &gravity,
    &integration_parameters,
    &mut island_manager,
    &mut broad_phase,
    &mut narrow_phase,
    &mut rigid_body_set,
    &mut collider_set,
    &mut impulse_joint_set,
    &mut multibody_joint_set,
    &mut ccd_solver,
    None,
    &physics_hooks,
    &event_handler,
  );

  let ball_body = &rigid_body_set[ball_body_handle];
  println!("Ball altitude: {}", ball_body.translation().y);
}

We pass our physics components into our physics_pipeline which we then advance one step at a time.

The pipeline will transform the properties of our rigid bodies, like its translation, rotation, etc according to the rules of our physics simulation.

To understand a little more we dive deeper into the rapier internals.

Pipelines

In Rapier we use pipelines to group up the components we need and run our simulation.

There are 3 types of pipelines and each one handles a specific part of our simulation.

The physics pipeline

The physics pipeline is responsible for updating all our data structures and running our physics systems. Each step in our physics pipeline takes all our physics components:

// https://github.com/dimforge/rapier/blob/e9ea2ca10b3058a6ac2d7f4b79d351ef18ad3c06/src/pipeline/physics_pipeline.rs#L404
pub fn step(
  &mut self,
  gravity: &Vector<Real>,
  integration_parameters: &IntegrationParameters,
  islands: &mut IslandManager,
  broad_phase: &mut BroadPhase,
  narrow_phase: &mut NarrowPhase,
  bodies: &mut RigidBodySet,
  colliders: &mut ColliderSet,
  impulse_joints: &mut ImpulseJointSet,
  multibody_joints: &mut MultibodyJointSet,
  ccd_solver: &mut CCDSolver,
  mut query_pipeline: Option<&mut QueryPipeline>,
  hooks: &dyn PhysicsHooks,
  events: &dyn EventHandler,
)

Each time we step through our physics pipeline Rapier is using a time-stepping scheme to divide our simulation into discrete time steps.

Then rapier will update the objects we pass in at each step based on the forces acting on them.

Our forces are calculated by two separate solvers:

  1. Velocity-based solver: This solver, based on the Projected Gauss-Seidel (PGS) algorithm, is responsible for computing forces for contact and joint constraints. It determines how objects should move and react to forces like collisions and joint limits, taking into account factors such as masses, velocities, and contact constraints.

  2. Position-based solver: This solver, based on a non-linear variant of PGS, is used for constraint stabilization. It corrects errors that may arise during the simulation, such as inter-penetrations (objects overlapping) or constraint drifts, to ensure that the objects remain in a physically plausible state. It performs adjustments to the positions of the objects based on the detected errors.

The overall sequence looks like this:

  1. Wake up our islands for objects that should be simulated
  2. Handle changes we made to any colliders
  3. Handle changes we made to any rigid bodies
  4. Handle joints
  5. Detect collisions
  6. Run any queries
  7. Perform any continuous collision detection (CCD)
  8. Integrate forces
  9. Update our objects

The collider pipeline

Collisions are a separate concern for the simulation. We can simulate physics without any collisions, and collisions are a separate stage, calculated after our initial physics pipeline.

let mut collision_pipeline = CollisionPipeline::new();

The step definition for this pipeline is much smaller:

// https://github.com/dimforge/rapier/blob/9e1113c5c7e3c3a042bc5979c158e752acfeb46a/src/pipeline/collision_pipeline.rs#L110C4-L120C5
pub fn step(
  &mut self,
  prediction_distance: Real,
  broad_phase: &mut dyn BroadPhase,
  narrow_phase: &mut NarrowPhase,
  bodies: &mut RigidBodySet,
  colliders: &mut ColliderSet,
  query_pipeline: Option<&mut QueryPipeline>,
  hooks: &dyn PhysicsHooks,
  events: &dyn EventHandler,
) {
  // ...
}

Running this will detect our collisions but not modify any dynamics like force computation or integration.

Instead, it will emit contact events which we can react to through query pipelines.

The query pipeline

Queries are how we ask about our colliders. These pipelines can be used to ask questions such as:

Did the bullet fired by the player hit someone?

When the bullet collided what direction should it ricochet to?

Which objects in my game are in a particular area?

These queries are separate from our simulation. They will not actually affect the simulation by moving rigid bodies or applying integration forces.

So we can run them separate from our physics pipeline, even multiple times in the same simulation frame if we need to.

We keep our query pipeline updated by running it after our simulation:

physics_pipeline.step(..)
query_pipeline.update(&collider_set);
// Now we can read the results of the physics simulation,
// and we can do advanced scene queries on the colliders.

After updating we can use our query pipeline to query. For example lets cast a ray and find its collision point if there is one:

let ray = Ray::new(point![1.0, 2.0], vector![0.0, 1.0]);
let max_time_of_impact = 4.0;
let solid = true;
let filter = QueryFilter::default();

// Find the first ray intersection
if let Some((handle, time_of_impact)) = query_pipeline.cast_ray(
  &rigid_body_set,
  &collider_set,
  &ray,
  max_time_of_impact,
  solid,
  filter,
) {
  // The first collider hit has the handle `handle` and it hit after
  // the ray travelled a distance equal to `ray.dir * time_of_impact`.
  let hit_point = ray.point_at(time_of_impact); // Same as: `ray.origin + ray.dir * time_of_impact`
  println!("Collider {:?} hit at point {}", handle, hit_point);
}

Or we could enumerate over all the collisions:

// Enumerate all ray intersections
query_pipeline.intersections_with_ray(
  &rigid_body_set,
  &collider_set,
  &ray,
  max_time_of_impact,
  solid,
  filter,
  |handle, intersection| {
    // Callback called on each collider hit by the ray.
    let hit_point = ray.point_at(intersection.time_of_impact);
    let hit_normal = intersection.normal;
    println!(
      "Collider {:?} hit at point {} with normal {}",
      handle, hit_point, hit_normal
    );
    true // Return `false` instead if we want to stop searching for other hits.
  },
);

Simulating physics in Bevy with Rapier

So now we know how the core of Rapier works in any game context we want.

However, Bevy is deeply integrated into its ECS. If we are passing around these rust structs to manipulate in an object oriented way, then how does it work with our components and systems?

The bevy_rapier crate handles this integration by providing us a plugin we can use to add Rapier to our Bevy app:

use bevy::prelude::*;
use bevy_rapier2d::prelude::*;


fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .add_plugins(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(100.0))
    .add_plugins(RapierDebugRenderPlugin::default())
    .run();
}

Underneath, bevy_rapier is using the pipelines we mentioned earlier to copy Rapiers internal representation of our physics simulation and project it back onto our game world.

This means that there are actually two worlds, Rapier's world and Bevy's world which have to be synced by propagating the data from Rapier to Bevy during the Writeback stage.

The bevy_rapier plugin is adding a series of stages that run during your game loop when you add the RapierPhysicsPlugin:

// https://github.com/dimforge/bevy_rapier/blob/f17bd5b04b3dc437152403c1d365cd56e75924ca/src/plugin/plugin.rs#L137
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
pub enum PhysicsSet {
  // This set runs the systems responsible for synchronizing (and
  // initializing) backend data structures with current component state.
  // These systems typically run at the after [`Update`].
  SyncBackend,
  // The systems responsible for advancing the physics simulation, and
  // updating the internal state for scene queries.
  // These systems typically run immediately after [`PhysicsSet::SyncBackend`].
  StepSimulation,
  // The systems responsible for updating
  // [`crate::geometry::CollidingEntities`] and writing
  // the result of the last simulation step into our `bevy_rapier`
  // components and the [`GlobalTransform`] component.
  // These systems typically run immediately after [`PhysicsSet::StepSimulation`].
  Writeback,
}

These sets are scheduled to be run right before Bevy's TransformSystem::TransformPropagate stage.

The SyncBackend step is our workhorse, it keeps track of changes we make in Bevy and propagating them to Rapier. This set pushes our input into Rapier which will then get used during the step simulation.

After we have synchronized Bevy with Rapier, we can run our simulation with StepSimulation set which then gets written back to bevy in the Writeback set.

Bevy keeps track of the state of our Rapier world using a resource RapierContext which is used as a system parameter in the bevy_rapier plugin.

The RapierContext acts as an API to the normal pipeline API we described before and is used in the systems in each PhysicsSet.

We can also query through the RapierContext resource directly in our systems:

// Cast a ray inside of a system.
fn cast_ray(rapier_context: Res<RapierContext>) {
  let ray_position = Vec2::new(1.0, 2.0);
  let ray_direction = Vec2::new(0.0, 1.0);
  let max_time_of_impact = 4.0;
  let solid = true;
  let filter = QueryFilter::default();

  if let Some((entity, time_of_impact)) = rapier_context.cast_ray(
    ray_position,
    ray_direction,
    max_time_of_impact,
    solid,
    filter,
  ) {
    // The first collider hit has the entity `entity` and it hit after
    // the ray travelled a distance equal to `ray_direction * time_of_impact`.
    let hit_point = ray_position + ray_direction * time_of_impact;
    println!("Entity {:?} hit at point {}", entity, hit_point);
  }
}

Configuring Rapier

During the plugins initialization it will create a RapierConfiguration resource using init_resource.

This means that we can override these settings by providing the resource ourselves.

The configuration resource looks like this:

// https://github.com/dimforge/bevy_rapier/blob/f17bd5b04b3dc437152403c1d365cd56e75924ca/src/plugin/configuration.rs#L58
#[derive(Resource, Copy, Clone, Debug)]
// A resource for specifying configuration information
// for the physics simulation
pub struct RapierConfiguration {
  // Specifying the gravity of the physics simulation.
  pub gravity: Vect,

  // Specifies if the physics simulation is active and update the physics world.
  pub physics_pipeline_active: bool,

  // Specifies if the query pipeline is active and update the query pipeline.
  pub query_pipeline_active: bool,

  // Specifies the way the timestep length should be adjusted at each frame.
  pub timestep_mode: TimestepMode,

  // Specifies the number of subdivisions along each axes a shape should be subdivided
  // if its scaled representation cannot be represented with the same shape type.
  //
  // For example, a ball subject to a non-uniform scaling cannot be represented as a ball
  // (it’s an ellipsoid). Thus, in order to be compatible with Rapier, the shape is automatically
  // discretized into a convex polyhedron, using `scaled_shape_subdivision` as the number of subdivisions
  // along each spherical coordinates angle.
  pub scaled_shape_subdivision: u32,

  // Specifies if backend sync should always accept transform changes, which may be from the writeback stage.
  pub force_update_from_transform_changes: bool,
}

The preferred way to modify this is to do so through a system. Eventually RapierConfiguration will move from being a resource to a component, so this is a good idea to future proof yourself.

fn setup_gravity(mut rapier_config: ResMut<RapierConfiguration>) {
  rapier_config.gravity = Vec2::new(0.0, -9.81);
}

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .add_plugins(RapierPhysicsPlugin::<NoUserData>::pixels_per_meter(100.0))
    .add_plugins(RapierDebugRenderPlugin::default())
    .add_systems(Update, cast_ray)
    .add_systems(Startup, set_gravity)
    .run();
}

Rigid bodies

A RigidBody is a non deformable solid in your game world. Forces are applied to these components which affect their position by propagating changes from Rapier to Bevy through your Transform and GlobalTransform components.

There are 4 kinds of RigidBody:

  1. RigidBody::Dynamic
  2. RigidBody::Fixed
  3. RigidBody::KinematicPositionBased
  4. RigidBody::KinematicVelocityBased

The most simple is our RigidBody::Fixed, which is an object that has an infinite mass and cannot move. They are not affected by any forces and will only collide with RigidBody::Dynamic. We can imagine these as our walls or ground of our game.

Next is the RigidBody::Dynamic which are the objects we want to simulate and apply forces to in our game. Think of these as balls being thrown, or boulders sliding down a mountain. They are not controlled by any player but they are interacting with the environment around them.

The RigidBody::KinematicPositionBased and RigidBody::KinematicVelocityBased are both moved independently of the simulation. Instead of the physics engine determining where to move these bodies we control them manually. This makes them great for moving around a player who will be sending input on where they want to go each frame.

The difference between position and velocity based options is in how you set where you want to move each frame. If you choose to set the Transform each frame then Rapier will calculate the Velocity for you. If you choose to control the movement by its Velocity then Rapier will calculate what it's Transform should be.

We add the RigidBody component of our choice to an entity:

fn spawn_rigidbody(
  mut commands: Commands
) {
  commands.spawn(RigidBody::Dynamic);
}

All this would do though is mark the entity as a RigidBody::Dynamic. If we want it to move about we need to add our own Transform and Velocity components:

fn spawn_rigidbody(mut commands: Commands) {
  commands
    .spawn(RigidBody::Dynamic)
    // Adds Transform and GlobalTransform components
    .insert(TransformBundle::from(Transform::from_xyz(0.0, 5.0, 0.0)))
    // Adds movement to our dynamic body
    .insert(Velocity {
      linvel: Vec2::new(1.0, 2.0),
      angvel: 0.4,
    })
    // Changes the scale of our gravity for this entity
    .insert(GravityScale(2.0));
}

By adding both a Transform and a Velocity the systems included by the bevy_rapier plugin will query and update these entities according to rapier's simulation.

Moving a rigid body

When we want to move our RigidBody::Dynamic, like we made above, we can do so by querying and modifying our components from within a system:

#[derive(Component)]
struct Ball;

fn move_balls(mut velocities: Query<&mut Velocity, With<Ball>>) {
  for mut ball_velocity in &mut velocities {
    ball_velocity.linvel = Vec2::new(1.0, 2.0);
    ball_velocity.angvel = 0.4;
  }
}

This works fine because our RigidBody is dynamic.

If we modify the Transform to teleport our dynamic body somewhere, it would have strange behaviour when dealing with collisions. So in most cases its preferred to adjust our RigidBody::Dynamic through the Velocity instead of its Transform.

Controlling a character

What if we instead spawned a RigidBody::KinematicVelocityBased body and tried moving it like in the system above?

We would find it doesn't actually interact with any other bodies it runs into, even though all dynamic bodies work just fine.

This is because a kinematic body's purpose is to give us full control over its trajectory. They are immune to any forces or impulses like gravity.

When we want to use a kinematic based body and we need it to also interact with the rest of our dynamic and fixed bodies we should use a KinematicCharacterController component:

fn spawn_player(mut commands: Commands) {
  commands
    .spawn(RigidBody::KinematicPositionBased)
    .insert(TransformBundle::default())
    .insert(Collider::ball(0.5))
    .insert(KinematicCharacterController::default());
}

Then we can read and write to this component to control our characters:

fn change_character_position(
  mut controllers: Query<&mut KinematicCharacterController>,
) {
  for mut controller in &mut controllers {
    controller.translation = Some(Vec2::new(0., -1.));
  }
}

To read the results of our movements we can query for KinematicCharacterControllerOutput in one of our systems:

fn print_entity_movement(
  controllers: Query<(Entity, &KinematicCharacterControllerOutput)>,
) {
  for (entity, output) in &controllers {
    info!(
      "Entity {:?} moved by {:?} and touches the ground: {:?}",
      entity, output.effective_translation, output.grounded
    );
  }
}

The controller works by emitting ray-casts and shape-casts to adjust our movement based on the obstacles around us.

We could choose to implement this ourselves manually using scene queries with the QueryPipeline, but unless we need something custom the controller gets us everything we need.

When we set the controllers translation, during the next physics step it will be checked and the results are updated on the KinematicCharacterControllerOutput.

This component gets added automatically when we add the KinematicCharacterController as long as we set its translation to something other than None.

The controller itself has many options we can tweak to control our characters interaction with our environment:

// https://github.com/dimforge/rapier/blob/cf74150763dd575bc0399087e845e9be62aba56f/src/control/character_controller.rs#L121
pub struct KinematicCharacterController {
    // The direction that goes "up". Used to determine where the floor is, and the floor’s angle.
    pub up: UnitVector<Real>,

    // A small gap to preserve between the character and its surroundings.
    //
    // This value should not be too large to avoid visual artifacts, but shouldn’t be too small
    // (must not be zero) to improve numerical stability of the character controller.
    pub offset: CharacterLength,

    // Should the character try to slide against the floor if it hits it?
    pub slide: bool,

    // Should the character automatically step over small obstacles? (disabled by default)
    //
    // Note that autostepping is currently a very computationally expensive feature, so it
    // is disabled by default.
    pub autostep: Option<CharacterAutostep>,

    // The maximum angle (radians) between the floor’s normal and the `up` vector that the
    // character is able to climb.
    pub max_slope_climb_angle: Real,

    // The minimum angle (radians) between the floor’s normal and the `up` vector before the
    // character starts to slide down automatically.
    pub min_slope_slide_angle: Real,

    // Should the character be automatically snapped to the ground if the distance between
    // the ground and its feed are smaller than the specified threshold?
    pub snap_to_ground: Option<CharacterLength>,

    // Increase this number if your character appears to get stuck when sliding against surfaces.
    //
    // This is a small distance applied to the movement toward the contact normals of shapes hit
    // by the character controller. This helps shape-casting not getting stuck in an always-penetrating
    // state during the sliding calculation.
    //
    // This value should remain fairly small since it can introduce artificial "bumps" when sliding
    // along a flat surface.
    pub normal_nudge_factor: Real,
}

We should be careful with our character controllers and make sure that any systems that depend on KinimaticCharacterControllerOutput run before anything that modifies our KinematicCharacterController. Doing this prevents issues when detecting the ground with a ray-cast, especially in 2D.

Other gotchas include expecting physical forces like Friction to affect our character movement. It will affect any dynamic bodies that interact with us, but will not modify the movement of our characters.

Collisions

A RigidBody on its own won't actually collide with anything on its own, to do so we need to attach a Collider to the same entity.

fn spawn_ball(mut commands: Commands) {
  // Create a simple solid ball
  commands
    .spawn(RigidBody::Dynamic)
    .insert(Collider::ball(0.5));
}

But in Bevy's ECS each entity can only have a single instance of a component type. So how can we add two colliders to the same entity?

We can spawn many colliders that attach to child entities using Bevy's hierarchy capabilities:

fn spawn_multiple_colliders(mut commands: Commands) {
  commands
    .spawn(RigidBody::Dynamic)
    .with_children(|children| {
      children
        .spawn(Collider::ball(0.5))
        // Position the collider relative to the rigid-body.
        .insert(TransformBundle::default());
      children
        .spawn(Collider::ball(0.5))
        // Position the collider relative to the rigid-body.
        .insert(TransformBundle::from(Transform::from_xyz(0., 0., 1.)));
    });
}

Now our dynamic rigid body will collide with other rigid bodies that have Collider components.

Colliders can also have mass properties which effect the kind of contacts they produce with other colliders.

By default adding a Collider will automatically set its ColliderMassProperties::Density to 1.0 which will cause Rapier to automatically calculate the other mass properties based on its shape. This gives it some default physical behaviour that matches our intuition (bigger things smash harder).

There are other components we can add to affect our colliders:

Sensors

If we instead wanted to detect collisions but not actually be effected by the impacts then we could add a Sensor component:

fn spawn_sensor(mut commands: Commands) {
  commands.spawn(Collider::ball(0.5)).insert(Sensor);
}

A Sensor is different in the way it detects collision events.

Sensors are not going to emit contact events like normal colliders. They only emit intersection events when something first enters and leaves our boundary.

This makes them great for figuring out when our rigid bodies enter or leave a particular area of our game as defined by the Collider.

How collisions are detected

The way collision detection works in Rapier is in two phases:

  1. The broad phase: finds anything contacting or intersecting and flags them.
  2. The narrow phase: iterates over items found in the broad phase and calculates the collision events

Then the two solvers mentioned at the start of this article go to work on these events to generate the impacts and forces to apply to our entities.

The PhysicsPipeline running inside Rapier updates the contact graph and the intersection graph that the broad phase generates and its passed to the narrow phase which emits events.

Intersections are produced by colliders with a Sensor component, while contacts are produced when any two colliders touch.

The narrow phase emits both CollisionEvent and ContactForceEvent events we can read in our systems.

Filtering collisions

Filtering is split up into two groups:

  1. Collision groups: determines what contacts should be reported
  2. Solver groups: determines what contact forces should be computed

Usually you will be thinking about collision groups.

The only time you'll use solver groups is to apply your own custom forces when a particular collision happens.

To set a collision group we add the CollisionGroups component to something with a Collider:

fn spawn_filtered_collider(mut commands: Commands) {
  commands
    .spawn(Collider::ball(0.5))
    // First argument is the membership, second is the filter
    .insert(CollisionGroups::new(
      // We can use constants:
      Group::GROUP_1,
      // Or we can use names:
      Group::from_name("A").unwrap(),
    ));
}

The membership parameter determines who you are. It is how others reference you. If you say you are part of group 1 then everyone who filters for group 1 will be able to interact with you.

The filter parameter determines who you can interact with. If you say you can interact with members of group 1 then any entity assigned to group 1 will produce a collision if we make contact.

We set both our membership and filter using a Group which holds an integer representing its bitmask. We could have created our group using these more primitive types:

fn spawn_filtered_collider(mut commands: Commands) {
  commands
    .spawn(Collider::ball(0.5))
    .insert(CollisionGroups::new(
      Group::from_bits(0b1101).unwrap(), // (memberships)
      Group::from_bits(0b1101).unwrap(), // (filters)
    ));
}

Here, the two arguments we passed to CollisionGroups look weird. The 0b prefix indicates that the number is written in binary format.

Each digit in the binary number corresponds to a specific group, and the position of the digit determines the group index.

The type is Group(u32) so there are 32 possible groups. By default all bits are set to 1, so the default is to interact with everything else in the physics simulation.

In the case above, the binary number we gave (1011) has four digits. Reading from right to left, each digit represents the inclusion or exclusion of the collider in a specific group.

A 1 in a digit's position indicates that the collider is included in that group, while a 0 indicates exclusion.

For example, let's say you have four groups:

  1. Objects that can collide with walls.
  2. Objects that can collide with other objects of the same group.
  3. Objects that can collide with ground.
  4. Objects that can collide with the ceiling.

If you want your collider to be part of groups 0, 2, and 3, you would assign binary digits as follows:

  1. 1 (collide with walls)
  2. 0 (do not collide with objects of the same group)
  3. 1 (collide with ground)
  4. 1 (collide with ceiling)

Combining these binary digits, you get 0b1101, which represents the groups to which your collider belongs.

If instead we wanted to produce the contact events but not apply the forces of the collision (so we could apply our own) then we could do the same thing with SolverGroups:

fn spawn_filtered_solver(mut commands: Commands) {
  commands
    .spawn(Collider::ball(0.5))
    // First argument is the membership, second is the filter
    .insert(SolverGroups::new(
      // We can use constants:
      Group::GROUP_1,
      // Or we can use names:
      Group::from_name("A").unwrap(),
    ));
}

One gotcha is that by default your kinematic rigid bodies won't collide with your fixed bodies. This is because they are not listening for the right contact events. To enable this behaviour we need to add a ActiveCollisionTypes component:

fn spawn_fixed_ball(mut commands: Commands) {
  commands
    .spawn(RigidBody::Fixed)
    .insert(Collider::ball(0.5))
    .insert(ActiveCollisionTypes::all());
}

Events

The narrow phase will produce events like CollisionEvent and ContactForceEvent but not by default.

To enable this at least one of the contacted entities needs to have a ActiveEvents::COLLISION_EVENTS or ActiveEvents::CONTACT_FORCE_EVENTS component to trigger them.

fn spawn_emitting_ball(mut commands: Commands) {
  commands
    .spawn(Collider::ball(0.5))
    .insert(ActiveEvents::COLLISION_EVENTS);
}

Then we can read these events in our other systems:

fn display_events(
  mut collision_events: EventReader<CollisionEvent>,
  mut contact_force_events: EventReader<ContactForceEvent>,
) {
  for collision_event in collision_events.read() {
    info!("Received collision event: {:?}", collision_event);
  }

  for contact_force_event in contact_force_events.read() {
    info!("Received contact force event: {:?}", contact_force_event);
  }
}