DEV Community

Sébastien Belzile
Sébastien Belzile

Posted on

Making Games in Rust - Part 11 - Killing Monsters with Bullets

Getting killed is not cool. On the other hand, killing monsters is. This article will implement shooting bullets and make them kill monsters on contact.

Components

Let's start by adding the required components:

#[derive(Copy, Clone)]
pub enum GameDirection {
    Left,
    Right
}

pub struct Bullet;
Enter fullscreen mode Exit fullscreen mode

The Bullet component will be used to identify our bullets entities.

The GameDirection will be used to fire our bullets in the right direction.

Drawing Bullets

Let's create a file called bullets.rs that will contain the required code to create a new bullet:

pub struct BulletOptions {
    pub x: f32,
    pub y: f32,
    pub direction: GameDirection,
}

pub fn insert_bullet_at(
    commands: &mut Commands,
    materials: &Res<Materials>,
    options: BulletOptions,
) {
    let speed = match options.direction {
        GameDirection::Left => -14.0,
        _ => 14.0,
    };

    let x = match options.direction {
        GameDirection::Left => options.x - 1.,
        _ => options.x + 1.,
    };
    let rigid_body = RigidBodyBundle {
        position: Vec2::new(x, options.y).into(),
        velocity: RigidBodyVelocity {
            linvel: Vec2::new(speed, 0.0).into(),
            ..Default::default()
        },
        mass_properties: RigidBodyMassPropsFlags::ROTATION_LOCKED.into(),
        activation: RigidBodyActivation::cannot_sleep(),
        forces: RigidBodyForces {
            gravity_scale: 0.,
            ..Default::default()
        },
        ..Default::default()
    };

    let collider = ColliderBundle {
        shape: ColliderShape::cuboid(0.25, 0.05),
        flags: ColliderFlags {
            active_events: ActiveEvents::CONTACT_EVENTS,
            ..Default::default()
        },
        ..Default::default()
    };

    let sprite = SpriteBundle {
        material: materials.bullet_material.clone(),
        sprite: Sprite::new(Vec2::new(0.5, 0.1)),
        ..Default::default()
    };

    commands
        .spawn_bundle(sprite)
        .insert_bundle(rigid_body)
        .insert_bundle(collider)
        .insert(RigidBodyPositionSync::Discrete)
        .insert(Bullet);
}
Enter fullscreen mode Exit fullscreen mode

This code contains the usual boilerplate to create a new entity. It creates a rigid body, a collider and a sprite. The collider must enable the CONTACT_EVENTS flag. The color of our bullets will be yellow. Here is the code to declare the bullet material:

// src/game/mod.rs
bullet_material: materials.add(Color::rgb(0.8, 0.8, 0.).into()),

// src/game/components.rs
pub struct Materials {
    pub player_material: Handle<ColorMaterial>,
    pub floor_material: Handle<ColorMaterial>,
    pub monster_material: Handle<ColorMaterial>,
    pub bullet_material: Handle<ColorMaterial>,
}
Enter fullscreen mode Exit fullscreen mode

Shooting Bullets

In our player.rs file, we can add a system that will create a new bullet whenever the space bar is pressed:

// in build method of PlayerPlugin
SystemSet::on_update(AppState::InGame)
    .with_system(fire_controller.system())

// system implementation
pub fn fire_controller(
    keyboard_input: Res<Input<KeyCode>>,
    mut commands: Commands,
    materials: Res<Materials>,
    players: Query<(&Player, &RigidBodyPosition), With<Player>>,
) {
    if keyboard_input.just_pressed(KeyCode::Space) {
        for (player, position) in players.iter() {
            let options = BulletOptions {
                x: position.position.translation.x,
                y: position.position.translation.y,
                direction: GameDirection::Right,
            };
            insert_bullet_at(&mut commands, &materials, options)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Compiling and running the game should show that pressing space shoots useless bullets. Useless since they do not kill monsters...

Killing Monsters

To kill monsters, we can add a system that detects contacts between monsters and bullets:

// in build method of PlayerPlugin
SystemSet::on_update(AppState::InGame)
    .with_system(kill_on_contact.system())

// system implementation
pub fn kill_on_contact(
    mut commands: Commands,
    bullets: Query<Entity, With<Bullet>>,
    enemies: Query<Entity, With<Enemy>>,
    mut contact_events: EventReader<ContactEvent>,
) {
    for contact_event in contact_events.iter() {
        if let ContactEvent::Started(h1, h2) = contact_event {
            for bullet in bullets.iter() {
                for enemy in enemies.iter() {
                    if (h1.entity() == bullet && h2.entity() == enemy)
                        || (h1.entity() == enemy && h2.entity() == bullet)
                    {
                        commands.entity(enemy).despawn_recursive();
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Try running the game and try shooting at monsters. Monsters should now disappear when they are hit by a bullet.

Destroying Bullets

Our bullets are currently eternal. They do not disappear after hitting a monster, and they get stuck on walls. We should despawn them whenever they hit something:

// in build method of PlayerPlugin
SystemSet::on_update(AppState::InGame)
    .with_system(destroy_bullet_on_contact.system())

// system implementation
pub fn destroy_bullet_on_contact(
    mut commands: Commands,
    bullets: Query<Entity, With<Bullet>>,
    mut contact_events: EventReader<ContactEvent>,
) {
    for contact_event in contact_events.iter() {
        if let ContactEvent::Started(h1, h2) = contact_event {
            for bullet in bullets.iter() {
                if h1.entity() == bullet || h2.entity() == bullet {
                    commands.entity(bullet).despawn_recursive();
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Shooting Left

Our player always shoots in a single direction... It would be better if we could select the direction we shoot to. To do so, we will add a facing direction to our player:

// src/game/components.rs
pub struct Player {
    pub speed: f32,
    pub facing_direction: GameDirection,
}
Enter fullscreen mode Exit fullscreen mode

And initialize it on our player:

.insert(Player {
    speed: 7.,
    facing_direction: GameDirection::Right,
})
Enter fullscreen mode Exit fullscreen mode

Then, we will update our player_controller system to update this facing direction when our player moves:

pub fn player_controller(
    keyboard_input: Res<Input<KeyCode>>,
    mut players: Query<(&mut Player, &mut RigidBodyVelocity)>,
) {
    for (mut player, mut velocity) in players.iter_mut() {
        if keyboard_input.pressed(KeyCode::Left) {
            velocity.linvel = Vec2::new(-player.speed, velocity.linvel.y).into();
            player.facing_direction = GameDirection::Left
        }
        if keyboard_input.pressed(KeyCode::Right) {
            velocity.linvel = Vec2::new(player.speed, velocity.linvel.y).into();
            player.facing_direction = GameDirection::Right
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we will pass this direction to our bullet creation function to fire the bullet in the right direction:

pub fn fire_controller(
    keyboard_input: Res<Input<KeyCode>>,
    mut commands: Commands,
    materials: Res<Materials>,
    players: Query<(&Player, &RigidBodyPosition), With<Player>>,
) {
    if keyboard_input.just_pressed(KeyCode::Space) {
        for (player, position) in players.iter() {
            let options = BulletOptions {
                x: position.position.translation.x,
                y: position.position.translation.y,
                direction: player.facing_direction,
            };
            insert_bullet_at(&mut commands, &materials, options)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Running the game should confirm that you can shoot bullets left and right.

The final code is available here.

Discussion (0)