A simple physics based 2d game using the Bevy game engine. You can play the game on itch.io.

I made a 2d game starring a giraffe on a bicycle. It’s a mix between Trials and Elasto Mania.

Here’s a video clip from the first level:

A game in Rust using the Bevy framework.

My primary motivation was to complete a game end-to-end. It’s been a while since my last game. To set myself up for success, I picked a very limited scope. The bar on both gameplay and graphics was set low.

Another reason for why I wanted to pick up a game project, was to gain deeper insight on Bevy. Bevy is a popular Rust game engine. It is actively developed, and has received many exciting updates since the last time I used it.

Entity component systems

Bevy is, at its heart, a game engine built around an entity component system1 (ECS). The game objects, or entities, are built through composition.

For example, the player entity consists of the components Name, Transform, and a Scene which refers to the character polygon mesh2. The neck, limbs and wheels of consist of the components RigidBody, Collider, Friction, Velocity and so on.

To update the game, the engine calls systems which in turn query and update components. For example, the physics system queries all entities with the RigidBody component, calculates movement based on their Velocity and resolves collisions between entities with a Collider component.

I did not implement the physics though, the Rapier physics engine takes care of that. Rapier is akin to Box 2d and it integrates well with Bevy3.

To give you a picture of what that looks like, here’s a listing for a very simple game:

// A minimal game - Bevy 0.16
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;

type RapierPlugin = RapierPhysicsPlugin<NoUserData>;

#[derive(Component)]
pub struct Player;

fn main() {
    let mut app = App::new();
    // Register plugins and systems to be called
    app.add_plugins((DefaultPlugins, RapierPlugin::pixels_per_meter(100.)))
        .add_systems(Startup, (setup_system, spawn_player_system))
        .add_systems(Update, update_input_system)
        .run();
}

fn setup_system(mut commands: Commands) {
    commands.spawn(Camera2d::default());

    // Spawn a level
    commands
        .spawn(Collider::cuboid(500., 50.))
        .insert(Transform::from_xyz(0., -100., 0.));
}

fn spawn_player_system(mut commands: Commands) {
    // Spawn a wheel
    commands.spawn((
        Player,
        Transform::IDENTITY,
        RigidBody::Dynamic,
        Collider::ball(1.),
        ColliderMassProperties::Mass(5.),
        // For controlling torque of the wheel
        ExternalForce::default(),
    ));
}

fn update_input_system(
    keyboard: Res<ButtonInput<KeyCode>>,
    mut motors: Query<&mut ExternalForce, With<Player>>,
) {
    let Ok(mut motor) = motors.single_mut() else {
        return;
    };
    // Update wheel torque
    if keyboard.pressed(KeyCode::ArrowLeft) {
        motor.torque = -100.
    } else if keyboard.pressed(KeyCode::ArrowRight) {
        motor.torque = 100.
    } else {
        motor.torque = 0.;
    }
}

It’s bare bones, but it should give you a picture of what using Bevy is like.

Physics-based skeletal animation

I wanted the Giraffe movement to be physics-based instead of animated. It makes the character fun to control, as its neck bends and oscillates whilst the player moves around.

Although the character is 2d, I decided to use 3d meshes to access some great Bevy features. As of this writing, Bevy only supports skinned meshes in 3d.

Hence, I rigged a skeleton to the mesh in Blender. This is sometimes referred to as skinning4. With a skeleton, it’s possible to shape the mesh by transforming its bones.

A character rigged in Blender.
A character rigged in Blender. Rotating the bones transforms the mesh.

The next piece is to link up the bones to the physics simulation.

In the game, the neck of the giraffe is simulated by the physics engine. It consists of a set of square rigid bodies linked with spring joints. The joints bend as the character accelerates.

For the animation to work out, the rigid body colliders should be laid out to match the bones. This is tedious to do in code, so I was delighted to discover a Blender plugin called Skein. Using Skein, it’s possible to define Bevy components within Blender.

// Adding the SkeinPlugin - Bevy 0.16
use bevy_skein::SkeinPlugin;

// ...

fn main() {

    // ...

    // For Skein reflection
    app.register_type::<ColliderMassProperties>();

    app.add_plugins(SkeinPlugin::default());
}

The component data follows along, when exporting the model from Blender. By adding the SkeinPlugin to Bevy, Skein will add the components to entities spawned from the mesh.

I modeled the neck in Blender and added RigidBody and ColliderMassProperties components to the mesh data blocks5, as shown in figure 2.

Bevy collier components defined directly in Blender using Skein.
The collider components for the neck can be defined directly in Blender using the Skein plugin. The Object data properties panel on the bottom right lists the Bevy components attached to the mesh data.

Although Skein is great, I was unable to define Collider components directly in Blender. I resorted to doing the Collider and joint setup in code.

To demonstrate that, we can update the code to spawn a mesh in spawn_player_system() and then do the setup in on_scene_spawn().

// Setting up a skinned mesh with Rapier - Bevy 0.16
use bevy::prelude::*;
use bevy::render::primitives::Aabb;
use bevy::scene::SceneInstanceReady;
use bevy_rapier2d::prelude::*;

// ...


fn spawn_player_system(mut commands: Commands, asset_server: Res<AssetServer>) {
    // Spawn a player mesh
    let mesh = asset_server.load(
        GltfAssetLabel::Scene(0).from_asset("player.gltf")
    );
    commands.spawn((Player, SceneRoot(mesh)));
}

// Runs when the player GLTF scene (or any scene) is spawned
fn on_scene_spawn(
    trigger: Trigger<SceneInstanceReady>,
    scene_spawner: ResMut<SceneSpawner>,
    names: Query<&Name>,
    parents: Query<(&Name, &Transform, &Children)>,
    rigidbodies: Query<&Aabb, (With<Transform>, With<RigidBody>)>,
    mut commands: Commands,
) {
    let instance_id = trigger.event().instance_id;
    let scene_entities = scene_spawner.iter_instance_entities(instance_id);

    let mut colliders = Vec::new();
    let mut bones = Vec::new();

    // Iterate over the entities in the `SceneInstance`
    for parent_entity in scene_entities {

        // Store bones for later
        if let Ok(name) = names.get(parent_entity) {
            if name.contains("Bone") {
                bones.push(parent_entity);
                continue;
            }
        }

        // Add `Collider` components (could not add these in Skein)
        if let Ok((parent_name, transform, children)) = parents.get(parent_entity) {
            if parent_name.as_str().starts_with("COL_") {
                let collider = setup_colliders(
                    parent_name,
                    children.iter(),
                    commands.reborrow(),
                    rigidbodies,
                );
                colliders.push((collider, transform.translation.xy()))
            }
        }
    }

    // Link `Collider`s with `ImpulseJoint`s according to the bones
    setup_joints(commands.reborrow(), &colliders, &bones);
}

In the above listing, we add a handler that runs when a scene is spawned. It walks the entities of the mesh, and sets up colliders and joints using two helper functions; setup_colliders() and setup_joints(). These are explained next.

// Adding colliders based on mesh `Aabb` properties - Bevy 0.16

/// Add `Collider` for children with a `RigidBody` component
fn setup_colliders(
    parent_name: &Name,
    children: impl Iterator<Item = Entity>,
    mut commands: Commands,
    rigidbodies: Query<&Aabb, (With<Transform>, With<RigidBody>)>,
) -> Entity {
    for child_entity in children {
        if let Ok(aabb) = rigidbodies.get(child_entity) {
            let Aabb { half_extents, .. } = aabb;
            commands.entity(child_entity).insert(
                Collider::cuboid(half_extents.x * SCALE, half_extents.y * SCALE),
            );
            // Assuming there's only one `RigidBody`
            return child_entity;
        }
    }
    panic!("Missing RigidBody for {parent_name}");
}

The setup_colliders(), shown in the listing above, adds the Collider component to any scene object, that has both a name starting with COL_ (as in “collision”), and a RigidBody component.

We added the RigidBody in Blender above, so we should hopefully end up with colliders.

Then setup_joints() adds ImpulseJoint components between the colliders.

// Adding joints - Bevy 0.16
use std::iter::zip;

// ...

fn main() {
    // ...

    app.add_systems(Update, setup_joints);
}

/// Add `ImpulseJoint` between colliders
fn setup_joints(mut commands: Commands, colliders: &[(Entity, Vec2)], bones: &Vec<Entity>) {

    let all = zip(colliders.windows(2), bones);

    // Join colliders in pairs with a joint
    for (pairs, &bone) in all {
        if let [(c1, t1), (c2, t2)] = *pairs {
            // Calculate displacement from parent (c1) to child (c2)
            let parent_to_child = t1 - t2;

            // Calculate offsets (omitted from the example)
            let child_to_joint =  // ...

            let joint = RevoluteJointBuilder::new()
                .local_anchor2(parent_to_child)
                .local_anchor1(child_to_joint);

            commands.entity(c2).insert((
                // Add joint as component on the child
                ImpulseJoint::new(c1, joint),
                // Also add a marker that specifies what bone
                // this joint should affect
                ControlsBone(bone),
            ));
        }
    }
}

The code listing above has been simplified, but the general idea still holds. In the joint setup, we pair two colliders with an ImpulseJoint. We also add a ControlsBone marker, that eases the lookup from a joint to a corresponding bone.

At this point we have a physics based Collider and ImpulseJoint skeleton that’s simulated by Rapier, as well as a Mesh3d with the player texture and bones with Transforms.

We aren’t done however. The physics engine will simulate the skeleton, but it isn’t linked to the player mesh.

Rigid body colliders attached with spring joints make up the neck.
Rigid body colliders attached with spring joints make up the neck. The physics engine simulates the neck, but its movement hasn't been linked to the mesh yet.

Wiring the physics to the skeleton

As seen in figure 3, the physics simulation is independent from the player mesh. To link those up, we’ll add a system that reads the ImpulseJoint angles, looks up the corresponding ControlsBone markers, and updates the Transforms of the bones.

// Syncing bones with the phsyics simulation - Bevy 0.16

// ...

#[derive(Component, Clone, Debug)]
pub struct ControlsBone(pub Entity);


fn update_bones(
    context: ReadRapierContext,
    sources: Query<(Entity, &ImpulseJoint, &ControlsBone)>,
    mut bones: Query<&mut Transform>,
) {
    // Iterate over each joint
    for (joint_entity, joint, target) in sources.iter() {
        if let TypedJoint::RevoluteJoint(_) = &joint.data {
            let ControlsBone(bone_entity) = target;

            // Read `ImpulseJoint` angle
            let joint_angle = context
                .single()
                .expect("joint")
                .impulse_revolute_joint_angle(joint_entity)
                .unwrap_or(0.0);

            // Update `Transform` for the bone
            if let Ok(mut transform) = bones.get_mut(*bone_entity) {
                transform.rotation = Quat::from_rotation_z(angle);
            }
        }
    }
}

And there we go. With the sync in place, the mesh is animated by the physics engine. Here’s a video of running the game using Rapiers RapierDebugRenderPlugin.

Physics based skeletal mesh with Bevy and Rapier.

If you think skeletal animation is fun, why not try my game next? It runs in the browser.

Play it: You can play the game on itch.io.

  1. Entity component system on Wikipedia. 

  2. Polygon mesh on Wikipedia. 

  3. bevy_rapier plugin on GitHub. 

  4. Skeletal animation on Wikipedia. 

  5. About blender data blocks in the Blender Manual.