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