Maciej Główka
Blog Games Contact

← Return to Blog Index

npc_spawn.png
April 2, 2023

Bevy roguelike tutorial / devlog part 4 - NPC units

rust bevy gamedev

In the previous part we have left off after developing a functioning action queue - for our turn-based logic. This time we are going to make a use of it and add some moving NPCs to the game.

Final code for this one is on the branch here: https://github.com/maciekglowka/hike_deck/tree/part_04

NPC Spawning

Last time we have already created an Actor component that enables certain entities to perform actions within the game loop. Our only unit so far has been the player character, that is directly controlled by us (in terms of assigning the actions). Obviously, for the AI characters we are going to need a different approach when planning their movements.

Let's start by first defining another component (in the pieces module):


#[derive(Component)]
// movement behaviour for non-player pieces
pub struct Walk;

As written in the comment, we are going to spawn this component on the AI entities only. It will tell our systems that those units need to have their walk moves planned.

Before we start with our NPC movement systems though, we need some temporary units to work with. So let's turn the pieces module into a Bevy plugin (remember to register!) and add a dirty system that will create two of those:


// pieces/mod.rs
use bevy::prelude::*;

use crate::board::components::Position;
use crate::states::MainState;
use crate::vectors::Vector2Int;

pub mod components;

pub struct PiecesPlugin;

impl Plugin for PiecesPlugin {
    fn build(&self, app: &mut App) {
        app.add_system(spawn_npcs.in_schedule(OnEnter(MainState::Game)));
    }
}

pub fn spawn_npcs(
    mut commands: Commands
) {
    commands.spawn((
        components::Actor::default(),
        components::Piece { kind: "NPC".to_string() },
        Position { v: Vector2Int::new(3, 5) },
        components::Walk
    ));
    commands.spawn((
        components::Actor::default(),
        components::Piece { kind: "NPC".to_string() },
        Position { v: Vector2Int::new(5, 5) },
        components::Walk
    ));
}

We are going to revisit the units spawning later - after we make our dungeon generation. Now for the testing purposes it should be enough.

If we run the game we should see two question marks spawned on our board - the new units :). We have only modified the logic layer of our game, but they still get rendered. It's because our graphics system (that we created for the player) is generic enough to pick also those. Our code separations are starting to pay off :)

If you want you can assign different sprite indexes for the "NPC" piece kind if you want (in the graphics module)

npc_spawn.png

Action planning

We can still move our player, but the NPCs stand still. No wonder, they are not even added to the ActorQueue yet!

As you may remember from the previous part, we are initializing our queue in the player input system:


queue.0 = VecDeque::from([entity]);

(the entity here is the player object)

So, now we have to create some system that would add all the NPC actors to the queue:


pub fn populate_actor_queue(
    query: Query<Entity, (With<Actor>, Without<Player>)>,
    mut queue: ResMut<ActorQueue>
) {
    queue.0.extend(
        query.iter()
    );
}

We are going to schedule this system to run after the player input, so we assume that the ActorQueue resource already contains a single element Vec with the player entity inside. I am going to place this system in the actions/systems.rs file and register it like so:


.add_system(systems::populate_actor_queue
    .in_schedule(OnExit(GameState::PlayerInput)))

As you can see the queue will be updated every time we leave the PlayerInput state - so once per turn only.

The scheduling will be different though for the actual NPC movement planning. We will not create all the movement actions for all the units at once, at the beginning of the turn. As the subsequent actions might change the board layout, we will plan each NPC's move only right before it's sub-turn - using the current state. It might not matter now - as the units do not interact with each other. However, as we develop more systems later, it will be important that we for example do not plan some interactions with a unit that has been just killed in the previous move.

That's why we fire the NextActorEvent at the end of each successful queue processing - to trigger running of the movement planning system - for the next entity in the queue.


pub fn plan_walk(
    mut query: Query<(&Position, &mut Actor), With<Walk>>,
    queue: Res<ActorQueue>
) {
    let Some(entity) = queue.0.get(0) else { return };
    let Ok((position, mut actor)) = query.get_mut(*entity) else { return };
    let mut rng = thread_rng();
    let dir = ORTHO_DIRECTIONS.choose(&mut rng).unwrap();
    actor.0 = Some(Box::new(WalkAction(*entity, position.v + *dir)));
}

This system is also very simple. We just try to take the first entity existing in our queue and than query for it's Position and Actor components. Next, with a help of the rand crate, we pick a random Vector2Int direction. ( ORTHO_DIRECTIONS is just a 4 element array that we defined in the vectors module, containing all the cardinal directions). Finally we create a new target position for the unit and construct the WalkAction. It is then injected to the Actor component.

We register this system in actions/mod.rs like that:


.add_system(systems::plan_walk
    .run_if(on_event::<NextActorEvent>()))

And because of all the heavy work we did last time - that should be it! If we run the game now the NPCs should perform random moves, one at a time and after the player.

In the next parts we are going to create some more action possibilities for our units - like melee hits. We will see how we can easily extend our planning phase by simply adding parallel systems that plan different kinds of actions.

← Bevy roguelike tutorial / devlog part 3 - Action queue Bevy roguelike tutorial / devlog part 5 - more actions→