Profile Picture

Assorted tech

Hi, my name is Spencer. I'm a CS graduate focused on systems programming, Linux, and low-complexity design. Other interests: metal, hiking, guitar, rationality.

feed | contact

Gravity and Physics in the Bevy Game Engine

Calendar icon January 20, 2021

Clock icon 6 min read

Folder icon #bevy #gamedev #rust #physics #rapier in tech

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:

[dependencies]
bevy = "0.4"
bevy_rapier3d = "*"
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();
}
// ...
    .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.

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:

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