Introduction
Bevy is a really neat game engine for Rust. As of version 0.4, Bevy does not come with physics. Luckily, the Rapier physics engine folks maintain an official Rapier plugin for Bevy with helpful documentation.
Throughout this post, the demos will be from one of my codebases (will be public soon), and will not match the minimal relevant code presented in this post. I will be focusing on physics in a 3d context specifically, though most of this should also apply to 2d games. If you want something similar, you can add my flycam plugin for controls. Some familiarity with Bevy is assumed, try the Bevy book if you need help.
Basic Setup
You will need:
bevy
andbevy_rapier3d
inCargo.toml
:
[dependencies]
bevy = "0.4"
bevy_rapier3d = "*"
- Some sort of ground object so that our objects will have something to fall onto in a startup system:
use bevy::prelude::*;
// Runs on startup, adds a ground plane
pub fn setup_world(
commands: &mut Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Spawns a white plane at one unit below the origin
commands
.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Plane { size: 128.0 })),
material: materials.add(Color::WHITE.into()),
transform: Transform::from_translation(Vec3::unit_y() / 2.0),
..Default::default()
});
}
#[bevy_main]
fn main() {
App::build()
.add_plugins(DefaultPlugins)
.add_startup_system(setup_world.system())
.run();
}
- A mechanism for creating objects that will be affected by physics and gravity. For example you could spawn a cube 10 units above the origin in the above startup system or any other system:
// ...
.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
transform: Transform::from_translation(Vec3::unit_y * 10.0)
..Default::default()
});
In my demo, cubes are spawned on right-click.
Physics
Before implementing physics there’s some basic physics terminology worth knowing.
- A rigid body is an object modeled to undergo zero deformation in collisions (tungsten brick, diamond, etc).
- A soft body is much more complicated to simulate and does undergo deformation (pillows, aluminum ball, etc).
We’ll focus on rigid bodies because they are far more common in games, and Rapier does not support soft bodies. Now there are three main types of rigid bodies:
- A static rigid body is not subject to forces such as gravity or collisions making it ideal for ground objects that should not fall eternally through the abyss
- A kinematic rigid body generates and applies a force to other objects, typically either the player object or vehicles that generate force upon keyboard input
- A dynamic rigid body is subject to external gravity and collisions such as those from a kinematic rigid body
Adding Gravity
To be considered in Rapier’s physics calculations, an object needs two things: a collider and a rigid body. Let’s add both to the cube above the origin in our startup system:
use bevy_rapier3d::rapier::dynamics::RigidBodyBuilder;
use bevy_rapier3d::rapier::geometry::ColliderBuilder;
// ...
commands
// spawn ground plane ...
.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })),
transform: Transform::from_translation(Vec3::unit_y() * 10.0)
..Default::default()
})
// Needed for physics
.with(RigidBodyBuilder::new_dynamic().translation(Vec3::unit_y() * 10.0)
.with(ColliderBuilder::cuboid(0.5, 0.5, 0.5));
We use RigidBodyBuilder::new_dynamic()
to create a dynamic rigid body.
There is also new_static()
and new_kinematic()
.
If translation is not specified, the object will exist at the origin.
A collider is created with ColliderBuilder::cuboid()
where collider size is more like a radius, (0.5, 0.5, 0.5)
fits the size: 1.0
cube perfectly.
Run the game and you should see behavior like this:
Collisions
The cubes go straight through the floor! Depending on your circumstances, you will probably find this behavior undesirable. The cube doesn’t “see” that the plane exists because it has no rigid body or collider. As mentioned earlier, adding a dynamic rigid body would be a bit strange:
To prevent this you should use a static rigid body, but it also works with dynamic rigid bodies if one of the collider’s dimensions is ≤ 0 because this means a density of 0 (ρ=/mv).
commands
// Spawn the ground plane
.spawn(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Plane { size: 128.0 })),
material: materials.add(Color::rgb(0.4, 0.4, 0.4).into()),
transform: Transform::from_translation(Vec3::unit_y() / 2.0),
..Default::default()
})
// Static rigid body, collider
.with(RigidBodyBuilder::new_static().translation(0., 0., 0.))
.with(ColliderBuilder::cuboid(64., 0., 64.))
Remember that collider dimensions are half of size
!
Now this looks a lot better:
Customizing Physics
You might have noticed that my gravity looks a bit slower than yours. This is because I configured mine like this:
use bevy::prelude::*;
use bevy_rapier3d::physics::{RapierPhysicsPlugin, RapierConfiguration};
use bevy_rapier3d::rapier::na::Vector;
#[bevy_main]
fn main() {
App::build()
.add_plugins(DefaultPlugins)
.add_plugin(RapierPhysicsPlugin)
.add_startup_system(setup_world.system())
.add_resource(RapierConfiguration {
gravity: -Vector::y(),
..Default::default()
})
.run();
}
I’m setting gravity to a rate of -1 units here, whereas it is -9.81 by default as in the real world.
Note that RapierConfiguration
does not accept Bevy’s Vec3
type (for now?).
You can view all of the configuration options here.
Let’s have some fun with positive gravity (gravity: Vector::y()
):
Customizing Rigid Bodies
Check the docs here for a list of things you can set with RigidBodyBuilder
.
A few examples of things you can set are whether the rigid body should have translations or rotations locked, the mass of the body, and the initial velocity.
Conclusion
It’s easy to add gravity to Bevy games, and can be fun to play around with. Perhaps in the future I’ll update this guide to include adding a kinematic rigid body to the camera controller so that the player can knock things over. As always, let me know if you have any questions or feedback.
Comments