Bevy Physics: Avian
Avian is the successor to bevy_xpbd
. It's the de-facto ECS native physics solution for Bevy right now.
This guide is a work in progress and is still being updated.
The other big alternative is bevy_rapier
. The key differences between the two libraries come down to how they connect to Bevy:
Avian is using components and doing the calculations on them directly within Bevy's ECS.
Rapier keeps a separate representation of the game's physics that are projected back on to Bevy's ECS.
Rapier is more mature and can be used outside of Bevy while Avian is specifically focused on integrating deeply with Bevy.
Instead of repeating what is already in Jondolf's post about the why. I'll specifically be writing about the pragmatic how.
Making things move
To show up on a window, we have to be rendered. Bevy provides a rendering system which will draw you somewhere on the screen if your entity has the required components.
Your position, in the eyes of Bevy's renderer, is dictated by an entity's Transform
component.
A simple movement system could change that transform's translation
property, adding to its x
field each frame.
fn move_things(mut query: Query<&mut Transform>) {
for mut transform in &mut query {
transform.translation.x += 1.
}
}
There is obviously nothing physical about this. It would break our intuition about objects to see something actually fly through the air like this forever.
That's where libraries like Avian or Rapier come in. They are just like Bevy's renderer. They add systems to our app that will wait for entities with the right combination of components and change them for us.
In Avian we can use a somewhat more convenient Position
component. This is kept in sync with the Transform
automatically by Avian's SyncPlugin
.
fn move_things_with_position(mut query: Query<&mut Position>) {
for mut position in &mut query {
position.x += 1.;
}
}
This can be especially useful in 2D games where there is some awkwardness in translating your Vec2
into Vec3
so the transform's translation
is satisfied.
Making things rotate
Just like a Position
Avian provides a Rotation
component.
fn rotate_things(mut query: Query<&mut Rotation>) {
for mut rotation in &mut query {
*rotation = rotation.add_angle(0.1);
}
}
This is also going to sync with your transform but forms a more convenient API for you to interact with.
Simulating physics
Rigid bodies are components we attach to entities. These components are what Avian's systems will query for and update. Finally one of Avian's systems will translate these changes to the Transform
component which will cause Bevy to change its position on the screen when it renders.
Rigid bodies come in 3 flavors, each specialized for something:
- Dynamic bodies are similar to real life objects and are affected by forces and contacts.
- Kinematic bodies can only be moved programmatically, which is useful for things like player character controllers and moving platforms.
- Static bodies can not move, so they can be good for objects in the environment like the ground and walls.
To create one we simply spawn an entity with the component:
fn spawn_ball(mut commands: Commands) {
commands.spawn(RigidBody::Dynamic)
}
To move the same way as the initial example, but with Avian managing the physics, we would use a LinearVelocity
component:
fn spawn_ball(mut commands: Commands) {
commands.spawn((
RigidBody::Dynamic,
LinearVelocity(Vec2::new(0.0, 0.0)),
));
}
Okay but how big is this thing? How heavy? These kinds of questions are answered via an entities mass properties:
- Mass: Represents resistance to linear acceleration. A higher mass requires more force for the same acceleration.
- Angular Inertia: Represents resistance to angular acceleration. A higher angular inertia requires more torque for the same angular acceleration.
- Center of Mass: The average position of mass in the body. Applying forces at this point produces no torque.
By default, Avian is going to determine these for us via our our collider. But we can also override them by providing our own components for each:
fn spawn_ball(mut commands: Commands) {
commands.spawn((
RigidBody::Dynamic,
LinearVelocity(Vec2::new(0.0, 0.0)),
Collider::circle(0.5),
Mass(5.0),
CenterOfMass::new(0.0, -0.5),
));
}
The type of body you are will also change your mass properties. Static and kinematic rigid bodies have infinite mass and angular inertia. They also do not respond to forces or torques.
Colliding
Now, just because something is a physical body does not mean it has to interact with anything else.
Avian will calculate collisions, but only if your body has a component that explains exactly how it should collide.
fn spawn_ball(mut commands: Commands) {
commands.spawn((
RigidBody::Dynamic,
Collider::circle(0.5),
));
}
Avian is going to use the size of this collider to determine how much mass your body has.
If we want to make compound shapes you have two choices:
Collider::compound
if you want a single collider made of many shapes- Adding a child entity with their own colliders
So we could create our own compounds shapes on a single entity:
fn compound_shaped_ball(mut commands: Commands) {
commands.spawn((
RigidBody::Dynamic,
Collider::compound(vec![
(
Position::from_xy(5., 5.),
Rotation::degrees(0.),
Collider::circle(0.5),
),
(
Position::from_xy(-5., -5.),
Rotation::degrees(0.),
Collider::circle(0.5),
),
]),
));
}
Or we could have had one entity with multiple children:
fn compound_ball_with_children(mut commands: Commands) {
commands.spawn((
RigidBody::Dynamic,
Collider::circle(0.5),
children![
(Collider::circle(0.5), Position::from_xy(5., 5.)),
(Collider::circle(0.5), Position::from_xy(-5., -5.))
],
));
}
Sensors
Sometimes you will want a collider that tells you when it hits things, but does not actually interact with them. A common use case here would be an enemy AI that has a certain radius it can detect the player in.
Adding a Sensor
component will give you this behavior without affecting the entity's mass properties or interacting with other physical bodies.
fn spawn_sensor(mut commands: Commands) {
commands.spawn((
RigidBody::Dynamic,
Collider::circle(0.5),
Sensor,
));
}
Reacting to collisions
When you actually want to react to collisions you can read the events for them.
For processing a large number of collisions at once you would use the EventReader
:
fn react_to_collisions(
mut collision_events: EventReader<CollisionStarted>,
) {
for event in collision_events.read() {
info!("Collision started between {:?} and {:?}", event.0, event.1);
}
}
However if we want entity-specific collisions then we can use observers:
#[derive(Component)]
struct SecurityCamera;
#[derive(Component)]
struct Enemy;
fn setup_security_cameras(mut commands: Commands) {
commands
.spawn((
SecurityCamera,
Collider::circle(3.0), // Detection radius
Sensor,
CollisionEventsEnabled, // So we receive collision events
))
.observe(
|trigger: Trigger<OnCollisionStart>, enemy_query: Query<&Enemy>| {
let camera = trigger.target();
let intruder = trigger.collider;
if enemy_query.contains(intruder) {
println!("Security camera {camera} detected enemy {intruder}!");
}
},
);
}