Making Games in Rust - Part 10 - Death & Enemies

sbelzile

Sébastien Belzile

Posted on January 17, 2022

Making Games in Rust - Part 10 - Death & Enemies

We can start and stop our game, we have a map to explore, but our player cannot win or lose.

This article will allow our player to die, and will add enemies to the game.

Death By Falling Down

The easiest way to die to implement is death by falling down. If our player falls below some level, he dies.

fn death_by_height(
    mut commands: Commands,
    players: Query<(Entity, &RigidBodyPosition), With<Player>>,
) {
    for (entity, position) in players.iter() {
        if position.position.translation.y < -1. {
            commands.entity(entity).despawn_recursive();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If you add this system, register it, and run the game, you should be able to kill our player by jumping into a pit.

You should notice one problem though: this death also destroy our camera since it is defined as a child of our player...

To fix this, let's render the camera on the same level as our player:

// player.rs
struct PlayerData {
    player_entity: Entity,
    camera_entity: Entity,
}

// in spawn_player, to remove
- .with_children(|parent| {
-     parent.spawn_bundle(new_camera_2d());
- })

// in spawn_player, to add
let camera_entity = commands.spawn_bundle(new_camera_2d()).id();
    commands.insert_resource(PlayerData {
        player_entity,
        camera_entity,
    });

// in cleanup_player
commands
    .entity(player_data.camera_entity)
    .despawn_recursive();
Enter fullscreen mode Exit fullscreen mode

And let's re-implement the camera should follow player requirement with a system:

fn camera_follow_player(
    mut cameras: Query<&mut Transform, With<Camera>>,
    players: Query<&RigidBodyPosition, With<Player>>,
) {
    for player in players.iter() {
        for mut camera in cameras.iter_mut() {
            camera.translation.x = player.position.translation.x;
            camera.translation.y = player.position.translation.y;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, running the game and falling into a pit should kill our player:

Image description

Monsters

To add monsters:

  1. Add an Enemy and a Monster component:
pub struct Enemy;
pub struct Monster;
Enter fullscreen mode Exit fullscreen mode

The Enemy component will be useful for everything "enemy" related, and the Monster component is the specific for monsters. This way we could easily implement a Trap entity and have it automatically catch up every Enemy related functionalities such as death by contact, but not things like a monster artificial intelligence.

  1. Add a monster material to the materials:
pub struct Materials {
    pub player_material: Handle<ColorMaterial>,
    pub floor_material: Handle<ColorMaterial>,
    pub monster_material: Handle<ColorMaterial>,
}

fn setup(mut commands: Commands, mut materials: ResMut<Assets<ColorMaterial>>) {
    commands.insert_resource(Materials {
        player_material: materials.add(Color::rgb(0.969, 0.769, 0.784).into()),
        floor_material: materials.add(Color::rgb(0.7, 0.7, 0.7).into()),
        monster_material: materials.add(Color::rgb(0.8, 0., 0.).into()),
    });
}
Enter fullscreen mode Exit fullscreen mode
  1. In monster.rs, we will add a function to add our monsters to the scene:
pub fn insert_monster_at(commands: &mut Commands, x: usize, y: usize, materials: &Res<Materials>) {
    let rigid_body = RigidBodyBundle {
        position: Vec2::new(x as f32, y as f32).into(),
        mass_properties: RigidBodyMassPropsFlags::ROTATION_LOCKED.into(),
        activation: RigidBodyActivation::cannot_sleep(),
        forces: RigidBodyForces {
            gravity_scale: 3.,
            ..Default::default()
        },
        ..Default::default()
    };

    let collider = ColliderBundle {
        shape: ColliderShape::round_cuboid(0.35, 0.35, 0.1),
        flags: ColliderFlags {
            active_events: ActiveEvents::CONTACT_EVENTS,
            ..Default::default()
        },
        ..Default::default()
    };

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

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

Here, our monster gets a rigid body, a collider, a red sprite and the Enemy and Monster components.

  1. Add a step to the map generation script to randomly add monsters to our world:
// In map.rs
// in the spawn_floor method
add_enemies(&mut commands, &world, &materials);

// Function to insert monsters
fn add_enemies(commands: &mut Commands, world: &Vec<usize>, materials: &Res<Materials>) {
    world.iter().enumerate().for_each(|(x, height)| {
        if should_add_enemy(x) {
            insert_monster_at(commands, x, *height + 1, materials)
        }
    })
}

// Determines whether we should add a monster or not
fn should_add_enemy(x: usize) -> bool {
    if x <= 5 {
        return false;
    }
    let mut rng = thread_rng();
    let random_number: u32 = rng.gen_range(0..100);
    match random_number {
        0..=90 => false,
        _ => true,
    }
}
Enter fullscreen mode Exit fullscreen mode

If you run the game now, monsters should be visible:

Image description

Dying to the Hand of your Enemies

Enemies should kill the player on contact. We learned how to implement something similar in a previous chapter with contact events.

Let's create a new system:

pub fn death_by_enemy(
    mut commands: Commands,
    mut players: Query<Entity, With<Player>>,
    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 player in players.iter_mut() {
                for enemy in enemies.iter() {
                    if (h1.entity() == player && h2.entity() == enemy) || (h1.entity() == enemy && h2.entity() == player) {
                        commands.entity(player).despawn_recursive();
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And let's register it:

.with_system(death_by_enemy.system())
Enter fullscreen mode Exit fullscreen mode

If you run the game, your player should die when it touches a monster:

Image description

The final code is available here.

💖 💪 🙅 🚩
sbelzile
Sébastien Belzile

Posted on January 17, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related