Collisions
This example shows how to detect collisions in Bevy. Game engines like Unity have built in features such as bounding boxes to manage collisions. Bevy however does not have those so we are going to make our own.
Lets start off with this simple example: CollisionsExampleBefore. Unlike the previous example, this one doesn’t not contain any calculations. In fact, the player doesn’t even turn. Its movement is caused by pressing the arrow keys. If UpArrow is pressed then the player moves up, if RightArrow is pressed then it moves right and so on.
The goal is to introduce a stationary Obstacle entity into the game and detect when the player comes into contact with it, as well as make sure that the player doesn’t just go through it.
This is our current code:
main.rs
use bevy::prelude::*;
use bevy::color::palettes::basic::*;
#[derive(Component)]
struct Player {
position: Vec2,
color: Srgba,
size_radius: f32,
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup) // Startup runs once at the beginning
.add_systems(Update, draw_player) // Update runs every frame
.run();// Runs the application
}
fn setup(mut commands: Commands) {
commands.spawn(Camera2dBundle::default()); //Spawn a 2D camera entity
commands.spawn(Player { //Spawn a Player entity
position: Vec2::new(0.0, 0.0),
color: RED,
size_radius: 20.0,
});
}
fn draw_player(
mut gizmos: Gizmos,
mut player_query: Query<&mut Player>,
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
let step = 5.0;
for mut player in &mut player_query {
gizmos.circle_2d(player.position, player.size_radius, player.color); // Draw player
if keyboard_input.pressed(KeyCode::ArrowLeft) {
player.position.x -= step; // Move left
}
if keyboard_input.pressed(KeyCode::ArrowRight) {
player.position.x += step; // Move right
}
if keyboard_input.pressed(KeyCode::ArrowUp) {
player.position.y += step; // Move up
}
if keyboard_input.pressed(KeyCode::ArrowDown) {
player.position.y -= step; // Move down
}
}
}
Create Obstacle
First, we need to introduce the Obstacle struct. The obstacle will have properties similar to the player, such as position, color, and radius. To keep things simple, the obstacle will also be a circle, just a bit larger. Let’s add this code under the Player struct declaration.
#[derive(Component)]
struct Obstacle {
position: Vec2,
color: Srgba,
size_radius: f32,
}
Now we need to actually spawn the obstacle in the main()
function. We are going to spawn the obstacle a bit further than the player and make it a different color to tell them apart.
commands.spawn(Obstacle { //Spawn an Obstacle entity
position: Vec2::new(100.0, 100.0),
color: BLUE,
size_radius: 50.0,
});
Now that we’ve spawned our Obstacle entity, we can draw it using Gizmos, just like we did with the player. For that, we first need to query the obstacle entity. Because the obstacle will be stationary at all times, obstacle_query doesn’t need to be mutable. Then, we draw a circle using the Gizmo function based on the parameters of the Obstacle struct.
The draw_player()
function now looks like this:
fn draw_player(
mut gizmos: Gizmos,
mut player_query: Query<&mut Player>,
obstacle_query: Query<&Obstacle>, // Add query
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
let step = 5.0;
for mut player in &mut player_query {
for obstacle in &obstacle_query {
gizmos.circle_2d(player.position, player.size_radius, player.color); // Draw player
gizmos.circle_2d(obstacle.position, obstacle.size_radius, obstacle.color); // Add draw obstacle
if keyboard_input.pressed(KeyCode::ArrowLeft) {
player.position.x -= step; // Move left
}
if keyboard_input.pressed(KeyCode::ArrowRight) {
player.position.x += step; // Move right
}
if keyboard_input.pressed(KeyCode::ArrowUp) {
player.position.y += step; // Move up
}
if keyboard_input.pressed(KeyCode::ArrowDown) {
player.position.y -= step; // Move down
}
}
}
}
Let’s run the game to make sure everything is working. You should see a blue circle next to the player on the screen.
Detect Collision
We want to detect the when the player comes into contact with the obstacle. When it does, we’ll make it change color so that we know the collision happened. For this, we will create a new function called check_collisions()
. This function will calculate if the collision happened based on the distance between the two circles and the sum of their radii. It will then change player.color if a collision happened.
The function takes in the Player and Obstacle entities as its parameters. Player needs to be mutable because changes color if a collision happens. First, we calculate the distance between the centers of the two objects, and then the sum of their radii. If the distance between the two circles is smaller than the sum of their radii then they are touching, and player.color changes. If not, then the collision didn’t happen and nothing changes.
This is our new function:
check_collisions()
fn check_collisions(
player: &mut Player,
obstacle: &Obstacle,
) {
let distance = player.position.distance(obstacle.position);
let sum_radius = player.size_radius + obstacle.size_radius;
if distance < sum_radius { // if distance smaller than sum of radii
player.color = GREEN;
}
}
We then call the function in the draw_player()
function, just beneath all the if statements.
Let’s run the code. The player should turn green when it touches the obstacle.
Stop Player going through Obstacle
Notice that the player goes right through the obstacle. We want to fix that, as usually in games, the obstacle is supposed to stop the player from passing through it.
To achieve this, we need to create a variable called new_position in the draw_player()
function and pass it as an argument to the check_collisions()
function. The distance between the two circles will be now calculated using new_position. If a collision occurs, player.position will remain the same to prevent it from moving past the obstacle. However, if there is no collision, the player_position will be updated to new_position. This approach ensures that the player stops when it touches the obstacle and can only move in the opposite direction.
We need to modify the draw_player()
function to introduce new_position. When the arrow keys are pressed, new_position will be updated instead of us directly changing player.position.
The check_collisions()
function will also need to return a boolean because there needs to be an if statement in draw_player()
, which checks if the collision happened or not. If the distance between new_position of the player and obstacle.position is smaller than sum_radius then the function returns true. Else false.
Here are the two updated functions:
draw_player()
fn draw_player(
mut gizmos: Gizmos,
mut player_query: Query<&mut Player>,
obstacle_query: Query<&Obstacle>,
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
let step = 5.0;
for mut player in &mut player_query {
for obstacle in &obstacle_query {
gizmos.circle_2d(player.position, player.size_radius, player.color); // Draw player
gizmos.circle_2d(obstacle.position, obstacle.size_radius, obstacle.color); // Draw obstacle
let mut new_position = player.position;
if keyboard_input.pressed(KeyCode::ArrowLeft) {
new_position.x -= step; // Move left
}
if keyboard_input.pressed(KeyCode::ArrowRight) {
new_position.x += step; // Move right
}
if keyboard_input.pressed(KeyCode::ArrowUp) {
new_position.y += step; // Move up
}
if keyboard_input.pressed(KeyCode::ArrowDown) {
new_position.y -= step; // Move down
}
if !check_collisions(new_position, &mut player, &obstacle) {
player.position = new_position; // Update if no collision
}
}
}
}
check_collisions
fn check_collisions(
new_position: Vec2,
player: &mut Player,
obstacle: &Obstacle,
) -> bool {
let distance = new_position.distance(obstacle.position);
let sum_radius = player.size_radius + obstacle.size_radius;
if distance < sum_radius { // if distance smaller than sum of radii
player.color = GREEN;
return true;
}
else {
return false;
}
}
Now, let’s run the code and see the results. Notice how the player can’t move past the obstacle. We’ve created somewhat of a bounding sphere (or circle, call it whatever you want). In Bevy, when dealing with irregularly shaped objects, you can use these bounding spheres and make them invisible in order to detect collisions between the objects.
Hopefully, this example made sense and this is a file with the finished code: CollisionsExampleAfter
Thank you for reading!