rustbevyavian

Detect collisions in bevy


Setup: An asteroids clone with bevy and avian

I want to make an asteroids clone in rust with the game engine bevy and the physics engine avian. I already have Components for Bullets and Astroids. When they are spawned, they also get a Collider.

use bevy::prelude::*;
use avian2d::prelude::*;

#[derive(Component)]
struct Bullet;

#[derive(Component)]
struct Asteroid

fn setup(

) {
    asteroid_handle =  asset_server.load("asteroid.png");
    bullet_handle = asset_server.load("bullet.png");
    commands.spawn(
        (
            Asteroid,
            Sprite::from_image(asteroid_handle),
            Collider::circle(50.),
        )
    );
    commands.spawn(
        (
            Bullet,
            Sprite::from_image(bullet_handle),
            Collider::circle(5.),
        )
    );
}

(There is of course some code so they move, turn etc. but that is not relevant for the question)

The Problem: Find collisions between bullets and asteroids

Interactions between entities are where I am struggling: I want to detect when an asteroid is hit by a bullet (so I can despawn the bullet and destroy the asteroid, I don't know how yet) Detecting collisions is actually very easy, I can just listen for collision events.

fn print_collisions(mut collision_event_reader: EventReader<Collision>) {
    for Collision(contacts) in collision_event_reader.read() {
        println!(
            "Entities {} and {} are colliding",
            contacts.entity1,
            contacts.entity2,
        );
    }
}

This does print out collisions as expected. But it includes collisions between the ship and the bullet, the ship and the asteroids... Here all I get are two entities and no further information. How can I test whether they have the components I want?

What I want is to get only those collisions between a bullet and an asteroid.

What the AI suggested

I asked the AI and it suggested something like this:

fn print_collisions(
    mut collision_event_reader: EventReader<Collision>,
    query: Query<(Entity, Option<&Bullet>, Option<&Asteroid>)>,
) {
    for Collision(contacts) in collision_event_reader.read() {
        let (entity1, bullet1, asteroid1) = query.get(contacts.entity1).unwrap_or((contacts.entity1, None, None));
        let (entity2, bullet2, asteroid2) = query.get(contacts.entity2).unwrap_or((contacts.entity2, None, None));

        // Check if one entity is a Bullet and the other is an Asteroid
        if (bullet1.is_some() && asteroid2.is_some()) || (bullet2.is_some() && asteroid1.is_some()) {
            println!(
                "Bullet {} collided with Asteroid {}",
                if bullet1.is_some() { entity1 } else { entity2 },
                if asteroid1.is_some() { entity1 } else { entity2 },
            );
        }
    }
}

I guess that would work, but it looks very inefficient to me. We already have the entities in the event, there should be no need to query all the bullets and asteroids again to see if they are in the query.

What I am looking for: a simple solution

I am a beginner in both rust and bevy. I do have experience in programming (python mainly) and other Game engines (like Godot) and All Bevy code I wrote until now looks elegant and modular. That really impresses me. So I am looking for a simple and elegant solution.


Solution

  • What you have is the right direction. In Bevy, a Query is the intended way to query components on entities.

    In the avian2 docs, it shows a slightly different way to determine which entities are "invulnerable":

    fn filter_collisions(
        mut collisions: ResMut<Collisions>,
        query: Query<(), With<Invulnerable>>,
    ) {
        // uses query.contains(...)
    }
    

    It does it using a With filter instead of actually retrieving the Invulnerable component which is slightly more efficient if you don't need the component data itself, but the effect is the same - you need a Query to determine components.

    You will find this pattern in multiple places because generic actors (e.g. events, triggers, observers) often only give you an Entity and again will need a Query to do more with it.

    We already have the entities in the event, there should be no need to query all the bullets and asteroids again to see if they are in the query.

    This isn't what Query does; if you were to iterate over a query, it would do it on-demand (a.k.a. lazily), there is no upfront cost. And in this case you aren't iterating but rather using .get() calls to query specific entities. In that case it doesn't load all matching entities either, but rather works more like a map lookup (i.e. you're only paying for that specific entity).