Making Games in Rust - Part 10 - Death & Enemies
Sébastien Belzile
Posted on January 17, 2022
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();
}
}
}
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();
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;
}
}
}
Now, running the game and falling into a pit should kill our player:
Monsters
To add monsters:
- Add an
Enemy
and aMonster
component:
pub struct Enemy;
pub struct Monster;
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.
- 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()),
});
}
- 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);
}
Here, our monster gets a rigid body, a collider, a red sprite and the Enemy
and Monster
components.
- 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,
}
}
If you run the game now, monsters should be visible:
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();
}
}
}
}
}
}
And let's register it:
.with_system(death_by_enemy.system())
If you run the game, your player should die when it touches a monster:
The final code is available here.
Posted on January 17, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.