DEV Community

Sébastien Belzile
Sébastien Belzile

Posted on • Updated on

Making Games in Rust - Part 3 - Floors and Gravity

Now that we have a window, and a character drawn on the screen, it's time to make our character fall. This article explains how to add gravity to pull our player towards the ground when it is in the air, and make it stop when it meets the ground.

Theory

Our end goal is to have realistic movements. This is a problem more complex than it looks. We need a way to:

  1. Detect that our player is in contact with a surface (our floor)
  2. Make the player fall down when it does not touch the ground.

To facilitate our implementation (physics can be hard) we are going to bring in a physics library: Rapier. Rapier will allow us to easily simulate gravity, and will handle collision detection (detection of when our player is in the air or in contact with a surface) for us.

Adding Gravity

Step 1: Add bevy_rapier2d to the cargo.toml file:

[dependencies]
bevy = "0.5"
bevy_rapier2d = "0.11.0"
Enter fullscreen mode Exit fullscreen mode

Step 2: Register Rapier plugin with our application:

// main.rs
use bevy_rapier2d::prelude::*;

// ...
    App::build()
        // ...
        .add_plugin(RapierPhysicsPlugin::<NoUserData>::default())
Enter fullscreen mode Exit fullscreen mode

Step 3: Add collider and rigid body components to the player:

fn spawn_player(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
    let rigid_body = RigidBodyBundle {
        mass_properties: RigidBodyMassPropsFlags::ROTATION_LOCKED.into(),
        activation: RigidBodyActivation::cannot_sleep(),
        ccd: RigidBodyCcd { ccd_enabled: true, ..Default::default() },
        ..Default::default()
    };
    let collider = ColliderBundle {
        shape: ColliderShape::cuboid(0.5, 0.5),
        flags: ColliderFlags {
            active_events: ActiveEvents::CONTACT_EVENTS,
            ..Default::default()
        },
        ..Default::default()
    };
    commands
        .spawn_bundle(SpriteBundle {
            material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
            sprite: Sprite::new(Vec2::new(1.0, 1.0)),
            ..Default::default()
        })
        .insert_bundle(rigid_body)
        .insert_bundle(collider)
        .insert(RigidBodyPositionSync::Discrete)
        .insert(Player);
}
Enter fullscreen mode Exit fullscreen mode

There is a lot going on here, so let's explain what is going on.

A rigid body is responsible for dynamics and kinematics (motion and or motion resulting from forces) of our entities. In this case, we created a basic RigidBodyBundle with these properties:

  • mass_properties: the ROTATION_LOCKED property prevents Rapier from applying rotation forces on our player.
  • activation: Rapier marks non moving objects as sleeping if they don't move enough. Rapier ignores "sleeping" objects when simulating physics. This prevent setting prevents unwanted behavior triggered by a sleeping object.
  • ccd: for collision detection to be enabled on our player. More on that later.

A collider is responsible for collision detection. The collider also defines the shape of our object as well as its mass (important if we are simulating physics). In this case, we created a basic ColliderBundle with these properties:

  • shape: the shape of our player. Here, it's a square. Note that the x and y components of the collider's shape are half length. It took me a while to figure that out...
  • flags: features to activate on the collider. The provided code activates CONTACT_EVENTSs, more on that later.

Our code adds the rigid body and the collider bundle to our player, along with a RigidBodyPositionSync::Discrete component. This component will synchronize Bevy's transform (position) with the position of our rigid body.

If you run cargo run in your terminal, you should see our player fall into the abyss of our window's bottom.

Floor

Let's add a floor below our player:

// ...
.add_startup_stage("floor_setup", SystemStage::single(spawn_floor.system()))
// ...

fn spawn_floor(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
    let width = 10.;
    let height = 1.;
    let rigid_body = RigidBodyBundle {
        position: Vec2::new(0.0, -2.).into(),
        body_type: RigidBodyType::Static,
        ..Default::default()
    };
    let collider = ColliderBundle {
        shape: ColliderShape::cuboid(width / 2., height / 2.),
        ..Default::default()
    };
    commands
        .spawn_bundle(SpriteBundle {
            material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
            sprite: Sprite::new(Vec2::new(width, height)),
            ..Default::default()
        })
        .insert_bundle(rigid_body)
        .insert_bundle(collider)
        .insert(RigidBodyPositionSync::Discrete);
}
Enter fullscreen mode Exit fullscreen mode

If you run cargo run, you should see our player fall on the ground, and stay there.

Image description

This code is very similar to the other one. One difference is the body_type: RigidBodyType::Static property of the rigid body. This tells Rapier that our floor will never move.

All the code is available here.

Part 1:



Part 2:

Discussion (0)