Sébastien Belzile
Posted on December 13, 2021
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:
- Detect that our player is in contact with a surface (our floor)
- 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"
Step 2: Register Rapier plugin with our application:
// main.rs
use bevy_rapier2d::prelude::*;
// ...
App::build()
// ...
.add_plugin(RapierPhysicsPlugin::<NoUserData>::default())
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);
}
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
: theROTATION_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 activatesCONTACT_EVENTS
s, 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);
}
If you run cargo run
, you should see our player fall on the ground, and stay there.
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:
Making Games in Rust - Part 1 - Bevy and ECS
Sébastien Belzile ・ Dec 7 '21
Part 2:
Posted on December 13, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.