Making Games in Rust - Part 13 - Monster AI
Sébastien Belzile
Posted on February 7, 2022
Our monsters currently are boring inanimate red square. It would be great if they could somewhat be more aggressive. This article will implement a Super Mario type of monster AI: they will move from left to right, and change direction as they meet obstacles. To that, we will learn how to make them jump and shoot.
Monster AI Plugin
Let's start by creating a
monster_ai
module into the game folder.-
Declare and publish the module into the
src/game/mod.rs
file:
mod monster_ai; pub use monster_ai::*;
-
In the
monster_ai.rs
file, declare a BevyMonsterAiPlugin
:
pub struct MonsterAiPlugin; impl Plugin for MonsterAiPlugin { fn build(&self, app: &mut AppBuilder) {} }
-
In the game module, register the plugin:
.add_plugin(MonsterAiPlugin)
Monsters are Walkers
The first thing our monsters should do is walk. To do so:
-
Edit the
Monster
struct incomponents.rs
to provide aspeed
and afacing_direction
:
pub struct Monster { pub speed: f32, pub facing_direction: GameDirection, }
-
In,
monsters.rs
, add values to this component to themonster
entity:
.insert(Monster { speed: 3., facing_direction: GameDirection::Right })
-
In
monster_ai
, add a monster walking system. The code sets the velocity of the monster to their speed in the direction they are facing:
fn monster_walking_system(mut monsters: Query<(&Monster, &mut RigidBodyVelocity)>) { for (monster, mut velocity) in monsters.iter_mut() { let speed = match monster.facing_direction { GameDirection::Left => -monster.speed, GameDirection::Right => monster.speed, }; velocity.linvel = Vec2::new(speed, velocity.linvel.y).into(); } }
-
Register the system:
impl Plugin for MonsterAiPlugin { fn build(&self, app: &mut AppBuilder) { app.add_event::<MonsterWalkedIntoWallEvent>() .add_system_set(SystemSet::on_update(AppState::InGame).with_system(monster_walking_system.system()) ) } }
If you run the game now, the monsters should all walk right until they get stuck.
Monsters Change Direction on Contacts:
To make the monsters change their direction, we will trigger an event when monsters get in contact with with something, and react to this event to change the direction of our monsters.
-
Declare an event for when your monsters hit a wall:
struct MonsterWalkedIntoWallEvent { entity: Entity, }
-
Add a system to trigger an event when the contact actually happens:
fn monster_wall_contact_detection( monsters: Query<Entity, With<Monster>>, mut contact_events: EventReader<ContactEvent>, mut send_monster_walked_into_wall: EventWriter<MonsterWalkedIntoWallEvent> ) { for contact_event in contact_events.iter() { if let ContactEvent::Started(h1, h2) = contact_event { for monster in monsters.iter() { if h1.entity() == monster || h2.entity() == monster { send_monster_walked_into_wall.send(MonsterWalkedIntoWallEvent { entity: monster }) } } } } }
-
Add another system to handle the direction change:
fn monster_change_direction_on_contact(mut events: EventReader<MonsterWalkedIntoWallEvent>, mut monster_query: Query<&mut Monster>) { for event in events.iter() { // bullet contacts may destroy monster before running this system. if let Ok(mut monster) = monster_query.get_mut(event.entity) { monster.facing_direction = match monster.facing_direction { GameDirection::Left => GameDirection::Right, GameDirection::Right => GameDirection::Left } } } }
New thing here: query.get_mut(entity)
allows us to query a specific entity by ID.
Note that since event reaction may be caused by a bullet, monster destruction may happen before the change of direction. This is why we ignore monsters that are not found.
-
Register the systems in the
MonsterAiPlugin
plugin:
.with_system(monster_wall_contact_detection.system()) .with_system(monster_change_direction_on_contact.system())
Running the game now should show that the monsters change their direction when they get in contact with a wall or another monster.
Ramdom Jumps
This section shows how to make the monsters jump from time to time. The same logic can be used to make the monsters shoot bullets to our player.
-
In
monsters.rs
, add a jumper component to our monster:
.insert(Jumper { jump_impulse: 14., is_jumping: false, })
-
Add a jump system for our monsters in
monster_ai.rs
:
fn monster_jumps(mut monsters: Query<(&mut Jumper, &mut RigidBodyVelocity), With<Monster>>,) { for (monster, mut velocity) in monsters.iter_mut() { if should_jump() { velocity.linvel = Vec2::new(0., monster.jump_impulse).into(); } } } fn should_jump() -> bool { let mut rng = thread_rng(); rng.gen_bool(0.1) }
This system has a 10% chance of making our monster jump.
-
Register this system to run at regular intervals:
.add_system_set( SystemSet::new() .with_run_criteria(FixedTimestep::step(2.0)) .with_system(monster_jumps.system()) );
Running the game now should show that our monsters jump randomly from time to time.
Final Thoughts
In this tutorial, we learned the basis for implementing a Mario Bros like type of monster AI. There are still a few issues with our current implementations: floor collisions are detected as wall contacts, and our monsters may get stuck on walls after they jumped or fall.
We also learned how to make our player do some actions at random. As mentioned, we could make our monster shoot random bullets this way.
The final code is available here.
A fully playable version of the game is available here.
Posted on February 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.