Actual Physics

Bevy has plenty of third-party plugins.

Let's pick a physics engine that's easy to use with Bevy. There are two options:









We'll use Avian in this workshop, but you could use Rapier and get similar results.

First we'll add a dependency on avian2d to our Cargo.toml file:

[dependencies]
avian2d = "0.3.0"

At the time of writing, Avian has not yet been released for Bevy 0.16, but it has been updated in git.

avian2d = { git = "https://github.com/Jondolf/avian" }

To finish the setup, we need to add the PhysicsPlugins::default() to our app. And as we're in space, let's remove gravity! This can be done by adding the resource Gravity::ZERO.

Asteroid Movements

Asteroids are the easiest to do! First remove the inertia system, as that will now be handled by the physics engine.

When spawning an asteroid, we'll need to add the following components:

  • RigidBody
  • Collider
  • LinearVelocity
  • AngularVelocity

And that's it! As a bonus, now asteroids will bounce off each other.

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate avian2d;
extern crate rand;
use std::f32::consts::TAU;
use bevy::prelude::*;
use avian2d::prelude::*;
use crate::rand::Rng;
#[derive(Component)]
struct Asteroid;
#[derive(Resource)]
struct GameAssets {
    asteroid: Handle<Image>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, States, Default)]
enum GameState {
    #[default]
    Game,
}
fn display_level(mut commands: Commands, game_assets: Res<GameAssets>) {
    // Same player spawning

    let mut rng = rand::thread_rng();
    for (x, y) in [(1., 1.), (-1., 1.), (-1., -1.), (1., -1.)] {
        commands.spawn((
            Sprite::from_image(game_assets.asteroid.clone()),
            Transform::from_xyz(300.0 * x, 200.0 * y, 0.0),
            RigidBody::Dynamic,
            Collider::circle(50.0),
            LinearVelocity(Vec2::from_angle(rng.gen_range(0.0..TAU)) * rng.gen_range(10.0..100.0)),
            AngularVelocity(rng.gen_range(-1.5..1.5)),
            Asteroid,
            StateScoped(GameState::Game),
        ));
    }
}
}

Ship Movements

Ship movements are a bit more complicated. As it doesn't have fixed linear and angular velocities, we'll need to change them when reacting to user input.

First, we'll add some components when spawning the ship entity:

  • RigidBody
  • Collider

Another component we'll add is AngularDamping. As the ship is in space, once it's rotating it shouldn't slow down by itself, but that isn't very pleasant to control. Adding damping means that it will stop rotating by itself.

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate avian2d;
use bevy::prelude::*;
use avian2d::prelude::*;
#[derive(Component)]
struct Player;
#[derive(Resource)]
struct GameAssets {
    player_ship: Handle<Image>,
    jets: Handle<Image>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, States, Default)]
enum GameState {
    #[default]
    Game,
}
fn display_level(mut commands: Commands, game_assets: Res<GameAssets>) {
    commands.spawn((
        Sprite::from_image(game_assets.player_ship.clone()),
        RigidBody::Dynamic,
        Collider::circle(40.0),
        AngularDamping(5.0),
        Player,
        StateScoped(GameState::Game),
        children![(
            Sprite::from_image(game_assets.jets.clone()),
            Transform::from_xyz(0.0, -40.0, -1.0),
            Visibility::Hidden,
        )],
    ));

    // Same asteroids spawning
}
}

And when reacting to user input, we'll modify the AngularVelocity and LinearVelocity components. One thing to keep in mind is to set a maximum LinearVelocity or the ship could accelerate forever and reach an uncontrollable speed.

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate avian2d;
use bevy::prelude::*;
use avian2d::prelude::*;
#[derive(Component)]
struct Player;
fn control_player(
    keyboard_input: Res<ButtonInput<KeyCode>>,
    mut player: Query<
        (
            &Transform,
            &mut AngularVelocity,
            &mut LinearVelocity,
            &Children,
        ),
        With<Player>,
    >,
    mut visibility: Query<&mut Visibility>,
) -> Result {
    let Ok((transform, mut angular_velocity, mut linear_velocity, children)) = player.single_mut()
    else {
        // No player at the moment, skip control logic
        return Ok(());
    };
    if keyboard_input.pressed(KeyCode::KeyA) {
        angular_velocity.0 += 0.2;
    }
    if keyboard_input.pressed(KeyCode::KeyD) {
        angular_velocity.0 -= 0.2;
    }
    if keyboard_input.pressed(KeyCode::KeyW) {
        linear_velocity.0 += transform.local_y().xy() * 2.0;
        linear_velocity.0 = linear_velocity.0.clamp_length_max(200.0);
        *visibility.get_mut(children[0])? = Visibility::Visible;
    } else {
        visibility
            .get_mut(children[0])?
            .set_if_neq(Visibility::Hidden);
    }
    Ok(())
}
}

With that done, we can now remove the move_player system!

Collisions

Last task to move to the physic engine is collision detection

Avian exposes Collisions system parameter that we can use to easily query if something is colliding with an entity.

This is not the idiomatic way to do it. Avian send trigger events that can be caught with observers, which we'll explore later.

#![allow(unused)]
fn main() {
extern crate bevy;
extern crate avian2d;
use bevy::prelude::*;
use avian2d::prelude::*;
#[derive(Component)]
struct Player;
#[derive(Component)]
struct Explosion(Timer);
#[derive(Resource)]
struct GameAssets {
    explosion: Handle<Image>,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, States, Default)]
enum GameState {
    #[default]
    Game,
}
fn collision(
    collisions: Collisions,
    player: Query<(&Transform, Entity), With<Player>>,
    mut commands: Commands,
    game_assets: Res<GameAssets>,
) -> Result {
    let Ok((transform, entity)) = player.single() else {
        return Ok(());
    };

    if collisions.collisions_with(entity).next().is_some() {
        commands.spawn((
            Sprite::from_image(game_assets.explosion.clone()),
            (*transform).with_scale(Vec3::splat(0.2)),
            Explosion(Timer::from_seconds(1.0, TimerMode::Once)),
            StateScoped(GameState::Game),
        ));
        commands.entity(entity).despawn();
    }

    Ok(())
}
}