We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
7. Take Turns with the Monsters
Now we will build a way to make the monsters move. We will learn to selectively schedule systems execution based upon turn state.
Making Monsters Wander Randomly
Let’s first build a component to randomly move around the map. Let’s open up the component.rs then add this new line.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct MovingRandomly;
Random Movement System
Now that we have a new component we can make the system for it. Create a new file src/system/random_move.rs this will be used for the random movement but keep in mind you will be able to use this for many different random movement.
use crate::prelude::*;
#[system]
#[write_component(Point)]
#[read_component(MovingRandomly)]
pub fn random_move(ecs: &mut SubWorld, #[resource] map: &Map) {
let mut movers = <(&mut Point, &MovingRandomly)>::query();
movers.iter_mut(ecs).for_each(|(pos, _)| {
let mut rng = RandomNumberGenerator::new();
let destination = match rng.range(0, 4) {
0 => Point::new(-1, 0),
1 => Point::new(1, 0),
2 => Point::new(0, -1),
_ => Point::new(0, 1),
} + *pos;
if map.can_enter_tile(destination) {
*pos = destination;
}
});
}
Again we set up what we want the system to have access to, we query for all entities that have Point and MovingRandomly, we set a random move, then check to see if it is in bounds. Now we can add that system to the mod.rs.
mod collisions;
mod entity_render;
mod map_render;
mod player_input;
mod random_move;
use crate::prelude::*;
pub fn build_scheduler() -> Schedule {
Schedule::builder()
.add_system(player_input::player_input_system())
.add_system(collisions::collisions_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(random_move::random_move)
.build()
}
There is a new command .flush(), this make sure that all the events get executed before we move on. Lastly we need to add the new tag to the new monsters. Head to spawners.rs
pub fn spawn_monster(ecs: &mut World, rng: &mut RandomNumberGenerator, pos: Point) {
ecs.push((
Enemy,
pos,
Render {
color: ColorPair::new(WHITE, BLACK),
glyph: match rng.range(0, 4) {
0 => to_cp437('E'),
1 => to_cp437('O'),
2 => to_cp437('o'),
_ => to_cp437('g'),
},
},
MovingRandomly {},
));
}
Moving Entities in a Turn-Based Game
Now we can talk about how to set up the turn-based style. The basic way to make this work is to have 3 separate states: Player moving, Monsters moving, Waiting for input. Let’s start to work on that.
Storing Turn State
Let’s start by making a new file and then defining and Enum, src/turn_state.rs. We need to add the Enum then introduce them into the main.rs we also need to introduce the new State into the overall state of the game.
// turn_state.rs
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum TurnState {
AwaitingInput,
PlayerTurn,
MonsterTurn,
}
// main.rs
mod turn_state;
...
mod prelude {
...
pub use crate::turn_state::*;
}
// State.new()
resources.insert(TurnState::AwaitingInput);
Self {
mode: GameMode::OnMap,
ecs,
resources,
systems: build_scheduler(),
}
Taking Turns
For this we need to create an other system to end the turn of the Player. This will make sure that we can start to move the monster and then wait for the player input. src/systems/end_turn.rs then add the new system to mod.rs
// end_turn.rs
use crate::prelude::*;
#[system]
pub fn end_turn(#[resource] turn_state: &mut TurnState) {
let new_state = match turn_state {
TurnState::AwaitingInput => return,
TurnState::PlayerTurn => TurnState::MonsterTurn,
TurnState::MonsterTurn => TurnState::AwaitingInput,
};
*turn_state = new_state;
}
// mod.rs
mod collisions;
mod entity_render;
mod map_render;
mod player_input;
mod random_move;
use crate::prelude::*;
mod end_turn;
We make sure that we are sending in the mutable TurnState, then we pattern match based on the current TurnState then we change the turn state.
Dividing the Scheduler
Now we need to understand what needs to happen for any given state. I already have something like that with the GameState where we have a different set of ticks based off of that. We will now take the time to change the scheduler based off the new Enum we just made.
use crate::prelude::*;
mod collisions;
mod end_turn;
mod entity_render;
mod map_render;
mod player_input;
mod random_move;
pub fn build_input_scheduler() -> Schedule {
Schedule::builder()
.add_system(player_input::player_input_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.build()
}
pub fn build_player_scheduler() -> Schedule {
Schedule::builder()
.add_system(collisions::collisions_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(end_turn::end_turn_system())
.build()
}
pub fn build_monster_scheduler() -> Schedule {
Schedule::builder()
.add_system(random_move::random_move_system())
.flush()
.add_system(collisions::collisions_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(end_turn::end_turn_system())
.build()
}
Take a look at the functions that we are building, they will be called when we are in different states. We only need the input when we are waiting for the player input, then when we are in the playerscheduler we will only really care about the collisions. Then in the monster_scheduler we will worry about the monster collision, etc. Now we need to add those to the _main.rs State.
struct State {
mode: GameMode,
ecs: World,
resources: Resources,
input_systems: Schedule,
player_systems: Schedule,
monster_systems: Schedule,
}
impl State {
fn new() -> Self {
let mut ecs = World::default();
let mut resources = Resources::default();
let mut rng = RandomNumberGenerator::new();
let map_builder = MapBuilder::new(&mut rng);
spawn_player(&mut ecs, map_builder.player_start);
map_builder
.rooms
.iter()
.skip(1)
.map(|r| r.center())
.for_each(|pos| spawn_monster(&mut ecs, &mut rng, pos));
resources.insert(map_builder.map);
resources.insert(Camera::new(map_builder.player_start));
resources.insert(TurnState::AwaitingInput);
Self {
mode: GameMode::OnMap,
ecs,
resources,
input_systems: build_input_scheduler(),
player_systems: build_player_scheduler(),
monster_systems: build_monster_scheduler(),
}
}
...
}
Now we need to take the replace the single call to the input_scheduler to a patter match based on the TurnState.
fn on_map(&mut self, ctx: &mut BTerm) {
ctx.set_active_console(0);
ctx.cls();
ctx.set_active_console(1);
ctx.cls();
self.resources.insert(ctx.key);
let current_state = self.resources.get::<TurnState>().unwrap().clone();
match current_state {
TurnState::AwaitingInput => self
.input_systems
.execute(&mut self.ecs, &mut self.resources),
TurnState::PlayerTurn => {
self.player_systems
.execute(&mut self.ecs, &mut self.resources);
}
TurnState::MonsterTurn => self
.monster_systems
.execute(&mut self.ecs, &mut self.resources),
}
render_draw_buffer(ctx).expect("Render error");
}
We now have a way to execute different systems based on the current TurnState
Ending the Player’s Turn
Let’s add a change of TurnState within the player_input.rs
use crate::prelude::*;
#[system]
#[write_component(Point)]
#[read_component(Player)]
pub fn player_input(
ecs: &mut SubWorld,
#[resource] map: &Map,
#[resource] key: &Option<VirtualKeyCode>,
#[resource] camera: &mut Camera,
#[resource] turn_state: &mut TurnState,
) {
if let Some(key) = key {
let delta = match key {
VirtualKeyCode::Left => Point::new(-1, 0),
VirtualKeyCode::Right => Point::new(1, 0),
VirtualKeyCode::Up => Point::new(0, -1),
VirtualKeyCode::Down => Point::new(0, 1),
_ => Point::new(0, 0),
};
if delta.x != 0 || delta.y != 0 {
let mut players = <&mut Point>::query().filter(component::<Player>());
players.iter_mut(ecs).for_each(|pos| {
let destination = *pos + delta;
if map.can_enter_tile(destination) {
*pos = destination;
camera.on_player_move(destination);
*turn_state = TurnState::PlayerTurn;
}
});
}
}
}
We added in the new way to change the state from input to player turn.
Sending Messages of Intent
We now need to work on a way to send messages to other states so that we can make changes or not based on those things. Like if we Stun an enemy on a player_turn we don’t want them to move and the enemy_turn.
Messages Can Be Entities Too
Let’s open up the components.rs and add in an other struct WantsToMove.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WantsToMove {
pub entity: Entity,
pub destination: Point,
}
Receiving Messages and Moving
Now let’s add an other file src/systems/movements.rs this will allow us to check for WantsToMove.
use crate::prelude::*;
#[system(for_each)]
#[read_component(Player)]
pub fn movement(
entity: &Entity,
want_move: &WantsToMove,
#[resource] map: &Map,
#[resource] camera: &mut Camera,
ecs: &mut SubWorld,
commands: &mut CommandBuffer,
) {
if map.can_enter_tile(want_move.destination) {
commands.add_component(want_move.entity, want_move.destination);
if ecs
.entry_ref(want_move.entity)
.unwrap()
.get_component::<Player>()
.is_ok()
{
camera.on_player_move(want_move.destination);
}
}
commands.remove(*entity);
}
Using the #[system(foreach)] allows us to make it so when we start the function we are saying that we want this to run the query for the parameters that we set, commands (a command buffer) is a better way of a write because we don’t want things to get over written and to be done in a sequential order, we can access components outside the scope with _ecs.entry_ref(), you then need to unwrap() the component, check to see if the component exists with .is_ok(), then we execute the .on_player_move(), then we need to remove all the old messages (commands.remove(*entity);)
Now we can add this new system to the mod.rs
// mod.rs
pub fn build_player_scheduler() -> Schedule {
Schedule::builder()
.add_system(movement::movement_system())
.flush()
.add_system(collisions::collisions_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(end_turn::end_turn_system())
.build()
}
pub fn build_monster_scheduler() -> Schedule {
Schedule::builder()
.add_system(random_move::random_move_system())
.flush()
.add_system(movement::movement_system())
.flush()
.add_system(collisions::collisions_system())
.flush()
.add_system(map_render::map_render_system())
.add_system(entity_render::entity_render_system())
.add_system(end_turn::end_turn_system())
.build()
}
One thing that I want to add or reiterate is that there will be components that can just be passed or can be queried, there will also be resources that will need to be asked for, because ECS will take care of the read/write, but will need a way to know what will need to be accessed by different modules or systems.
Simplified Player Input
Now let’s take that new system and make the player_input.rs work with it. Remember we are asking the system to create a message and then adding that command to the buffer.
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(Player)]
pub fn player_input(
ecs: &mut SubWorld,
commands: &mut CommandBuffer,
#[resource] key: &Option<VirtualKeyCode>,
#[resource] turn_state: &mut TurnState,
) {
let mut players = <(Entity, &Point)>::query().filter(component::<Player>());
if let Some(key) = key {
let delta = match key {
VirtualKeyCode::Left => Point::new(-1, 0),
VirtualKeyCode::Right => Point::new(1, 0),
VirtualKeyCode::Up => Point::new(0, -1),
VirtualKeyCode::Down => Point::new(0, 1),
_ => Point::new(0, 0),
};
if delta.x != 0 || delta.y != 0 {
players.iter(ecs).for_each(|(entity, pos)| {
let destination = *pos + delta;
commands.push((
(),
WantsToMove {
entity: *entity,
destination,
},
));
});
*turn_state = TurnState::PlayerTurn;
}
}
}
So now we need only ask for the resources and then find the delta like before and then add in the WantsToMove command, then change the player state. Remember that if there is 1 entity for a resource you will need to ask for it so ECS can keep track of who is using the resource.
Monster Movement Messages
Now we can change the random_movement to use the WantsToMove. Lets make it create the message now.
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(MovingRandomly)]
pub fn random_move(ecs: &SubWorld, commands: &mut CommandBuffer) {
let mut movers = <(Entity, &Point, &MovingRandomly)>::query();
movers.iter(ecs).for_each(|(entity, pos, _)| {
let mut rng = RandomNumberGenerator::new();
let destination = match rng.range(0, 4) {
0 => Point::new(-1, 0),
1 => Point::new(1, 0),
2 => Point::new(0, -1),
_ => Point::new(0, 1),
} + *pos;
commands.push((
(),
WantsToMove {
entity: *entity,
destination,
},
));
});
}
What really changed? We added in the commands.push() so that we can say that the entity wants to move. We still have the random movement but now we have a way of knowing that a monster wants to move so we can later add a way to stop of augment that movement.
Wrap-Up
Great job you have finished the current version of the game we have a message system for movement that will take care of the able-to-move needs and then we can even add an ability to add status effects and more for movement. Its nice to see that a system can be used for many different components, and entities. We also saw in this chapter that ECS has a resource management system that is robust buts need to know what/who is asking for a resource so it can stop bad things from happening.