Pretty much every game needs collisions at some point. In this tutorial I will show you how you can implement 2D collision detections in Bevy using the Rapier2D crate. I will focus on detection and intersections, rather then physics based collisions with rigidbodies.
Given that Bevy doesn't ship with a physics engine we will have to choose one for ourselves. The Rapier crate mentioned above is a great fit. The description on their page is as follows
Rapier is a set of 2D and 3D physics engines written using the Rust programming language. It targets applications requiring real-time physics like video games, animation, and robotics. It is designed to be fast, stable, and optionally cross-platform deterministic.
Pretty good! It has all the features you would need in a physics game engine. I am going to focus on 2D Collision detection here (intersions of colliders), as opposed to doing physics based collisions with rigidbodies.
Let's start by understanding how we can define a very simple collider in rapier/bevy. Firstly, we use bevy_rapier2d crate which defines bindings to the bevy engine for us. Add the following to your app.
use bevy_rapier2d::prelude::*;
app.add_plugins((
RapierPhysicsPlugin::::default(),
RapierDebugRenderPlugin::default(),
))
Using this, we can create our first Collider Component. You can add it on any entity you like as it is a Component.
commands.spawn(
Collider::cuboid(2.0, 4.0);
)
This will add the Collider component to
the entity and is all we need in order to specify the collision
shape. It defines the geometric shape of our entity and allows
it to interact with the world if attached to a Rigidbody,
which we will not be doing in this tutorial.
Check out the the docs
for a full list of all shapes.
Rapier provides the function
intersections_with_shape,
which we can use to get information about all the shapes a provided collider intersects with.
Note that this will include all colliders,
meaning also the collider itself.
fn test_intersections(rapier_context: Res) { let shape = Collider::cuboid(1.0, 2.0); let shape_pos = Vec2::new(0.0, 1.0); let shape_rot = 0.8; let filter = QueryFilter::default(); rapier_context.intersections_with_shape( &shape_pos, &shape_rot, &shape, filter, |entity| { println!("The entity {:?} intersects our shape.", entity); // Continue searching intersections with other shapes, // false to stop after the first hit true }); }
This will print all the entities our shape is colliding with,
even itself! So if you only have a single collider in your scene
it would still print something (as it intersects itself).
To prevent this, we can make use of the QueryFilter.
let filter = QueryFilter {
exclude_collider: Some(entity),
..default()
};
Where entity is the Entity that our Collider
is attached to. This is really all we need to get collision
detection working in our game. There is however one
feature you should be aware of before implementing this,
and that is the CollisionGroups.
These allow us to specify which colliders should intersect
with which other colliders. This is very similar to
collision layers/masks of other engines like Unity or Godot.
The CollisionGroups takes two u32 values,
the first is the membership and the second the filter.
You can think of them like this.
The membership tells us "I exist on the following layer(s)." The filter tells us "I will collide with items that exist on the following layer(s)."
Each bit that is a 1 indicates a
true value, meaning
it exists on this layer or will collide with this layer.
By default, all bits are 1. To define our own
CollisionGroups we can use the following code.
collision_groups: CollisionGroups::new(
Group::from_bits(0b1000).unwrap(),
Group::from_bits(0b0100).unwrap(),
)
Here the collider with the attached collision_groups
will exist only on the first layer and will only mask (collide)
with other colliders that live on the second layer.
You could for example have the first bit represent the player
and the second bit represent your enemies.
Then to detech collision of player and enemey you simply have
to set the collision layer and mask appropriately and you will
see that they intersect eachother.
And that's all there is to know for basic 2D collision detection in bevy! If you are still a little confused or would like to see this approach implemented in a demo game, check out this commit. or following code, which is a minimal project that you can run as a standalone.
use bevy::prelude::*;
use bevy_rapier2d::prelude::*;
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
RapierPhysicsPlugin::::pixels_per_meter(100.0),
RapierDebugRenderPlugin::default(),
))
.add_systems(Startup, setup)
.add_systems(Update, test_intersections)
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
commands.spawn((Collider::ball(50.0), TransformBundle::default()));
}
fn test_intersections(
rapier_context: Res,
q_colliders: Query<(&Transform, &Collider)>,
) {
let filter = QueryFilter::default();
for (transform, collider) in &q_colliders {
rapier_context.intersections_with_shape(
transform.translation.truncate(),
transform.rotation.to_euler(EulerRot::ZYX).0,
collider,
filter,
|entity| {
println!("The entity {:?} intersects our shape.", entity);
true // Return `false` instead if we want to stop searching for other colliders that contain this point.
},
);
}
}