DEV Community

Sébastien Belzile
Sébastien Belzile

Posted on

Making Games in Rust - Part 13 - Monster AI

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

  1. Let's start by creating a monster_ai module into the game folder.

  2. Declare and publish the module into the src/game/mod.rs file:

    mod monster_ai;
    pub use monster_ai::*;
    
  3. In the monster_ai.rs file, declare a Bevy MonsterAiPlugin:

    pub struct MonsterAiPlugin;
    
    impl Plugin for MonsterAiPlugin {
        fn build(&self, app: &mut AppBuilder) {}
    }
    
  4. 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:

  1. Edit the Monster struct in components.rs to provide a speed and a facing_direction:

    pub struct Monster {
        pub speed: f32,
        pub facing_direction: GameDirection,
    }
    
  2. In, monsters.rs, add values to this component to the monster entity:

    .insert(Monster { speed: 3., facing_direction: GameDirection::Right })
    
  3. 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();
        }
    }
    
  4. 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.

  1. Declare an event for when your monsters hit a wall:

    struct MonsterWalkedIntoWallEvent {
        entity: Entity,
    }
    
  2. 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 })
                    }
                }
            }
        }
    }
    
  3. 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.

  1. 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.

  1. In monsters.rs, add a jumper component to our monster:

    .insert(Jumper {
        jump_impulse: 14.,
        is_jumping: false,
    })
    
  2. 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.

  1. 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.

Discussion (0)