We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
10. Fields of View
Now we get to add “line-of-sight” to the monster. Before this we have had omniscient monsters that know where the player is at all times. We also had players that can see everything that is within the field of view of the camera. We want to now add the following:
• The player is no longer certain of their exact location on the map and has
less warning about what may lie around the next corner.
• Restricted vision adds dramatic tension to the game—every corner might
hide a terrible demise.
• The game is more tactical, with the player having the option to hide and
avoid combat—and to engage at their own pace.
Defining an Entity’s Field of View
First we want to define which tiles can be seen through, then we need to prepare a component to store that data, then we need a system that will run to create and store that data.
Tile Opacity
First let’s add in a check for opacity to the map and then make sure that the floor is not opaque.
// map.rs
impl BaseMap for Map {
...
fn is_opaque(&self, idx: usize) -> bool {
self.tiles[idx as usize] != TileType::Floor
}
}
Collating Data with HashSets
We have used Vectors so far but there are other ways to store the data, we can now try and use HashSets, there are some key reasons to do so:
• Only contains unique entries. Adding a duplicate replaces the existing
entry.
• Searching for an entry is very fast. A vector would require you to search
each entry in turn—a HashSet consults its index.
• Adding data is slower than for a vector, because a hash has to be computed
and the index updated in addition to just storing the entry.
So for this we are getting a very fast look-up with the cost of a more involved insert.
Storing Visible Tile Sets in a Component
It’s a component so where do we go?
// components.rs
use std::collections::HashSet;
#[derive(Clone, Debug, PartialEq)]
pub struct FieldOfView{
pub visible_tiles : HashSet<Point>,
pub radius: i32,
pub is_dirty: bool,
}
Couple of things: we can’t define the Copy as HashSet can’t do that as a default (this is fine as most time it will be passed by reference), you can define most any data type for a HashSet, radius will be the circle around where a player will be able to see, we don’t want to have to update the hash more than we need so its normal for you to mark a dirty so that we know when to see if the data needs to be updated. Let’s now add in the constructor and a way to make a dirty copy.
impl FieldOfView {
pub fn new(radius: i32) -> Self {
Self {
visible_tiles: HashSet::new(),
radius,
is_dirty: true,
}
}
pub fn clone_dirty(&self) -> Self {
Self {
visible_tiles: HashSet::new(),
radius: self.radius,
is_dirty: true,
}
}
}
We now have a way to build a new FieldOfView struct and will be able to set the radius to a set value. Let’s head to the spawner and add in the new component to a player and the monsters.
pub fn spawn_player(ecs: &mut World, pos: Point) {
ecs.push((
Player,
pos,
Render {
color: ColorPair::new(WHITE, BLACK),
glyph: to_cp437('@'),
},
Health {
current: 10,
max: 10,
},
FieldOfView::new(8),
));
}
pub fn spawn_monster(ecs: &mut World, rng: &mut RandomNumberGenerator, pos: Point) {
let (hp, name, glyph) = match rng.roll_dice(1, 10) {
1..=8 => goblin(),
_ => orc(),
};
ecs.push((
Enemy,
pos,
Render {
color: ColorPair::new(WHITE, BLACK),
glyph,
},
ChasingPlayer {},
MovingRandomly {},
Health {
current: hp,
max: hp,
},
Name(name),
FieldOfView::new(6),
));
}
Plotting Fields of View
Tons of ways to build a field of view exist but bracket-lib uses a simple one that draws a circle around a player and then adds tiles to the view if they can draw a line from the player to the point with only 0 opacity tiles. Let’s create a new file and then do our work to add that system to the tree and then add that to the set of schedules.
// fov.rs
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[write_component(FieldOfView)]
pub fn fov(ecs: &mut SubWorld, #[resource] map: &Map) {
let mut views = <(&Point, &mut FieldOfView)>::query();
views
.iter_mut(ecs)
.filter(|(_, fov)| fov.is_dirty)
.for_each(|(pos, mut fov)| {
fov.visible_tiles = field_of_view_set(*pos, fov.radius, map);
fov.is_dirty = false;
});
}
// mod.rs
pub fn build_input_scheduler() -> Schedule {
Schedule::builder()
.add_system(player_input::player_input_system())
.add_system(fov::fov_system())
.flush()
...
}
pub fn build_player_scheduler() -> Schedule {
Schedule::builder()
.add_system(combat::combat_system())
.flush()
.add_system(movement::movement_system())
.flush()
.add_system(fov::fov_system())
.flush()
...
}
pub fn build_monster_scheduler() -> Schedule {
Schedule::builder()
.add_system(random_move::random_move_system())
.add_system(chasing::chasing_system())
.flush()
.add_system(combat::combat_system())
.flush()
.add_system(movement::movement_system())
.flush()
.add_system(fov::fov_system())
.flush()
...
}
Let’s really just go over the fov. Build a query with a mut FieldOfView, pull those out of the ecs, filter for entities with a dirty fov, field_of_view_set is the built-in bracket-lib for FOV, then set is_dirty to false so we don’t update it again.
Rendering Fields of View
Now we need to update the map_render.rs to use the new fov. This is because we only want to have the player see that is in their FOV.
#[system]
#[read_component(FieldOfView)]
#[read_component(Player)]
pub fn map_render(ecs: &SubWorld, #[resource] map: &Map, #[resource] camera: &Camera) {
let mut fov = <&FieldOfView>::query().filter(component::<Player>());
let player_fov = fov.iter(ecs).nth(0).unwrap();
...
for y: ...
...
...
if map.in_bounds(pt) && player_fov.visible_tiles.contains(&pt) {
}
Okay so we now can run the game and we will see a smaller field of view for the player. It doesn’t move with the player (dirty wasn’t set to true), and the monsters are all visible as well. Let’s start by hiding the entities first.
Hiding Entities
Let’s head to entity_render.rs and change up the way this works. We will need access to the the players fov and then we need to make sure that we only render the enemies that are in the field of view. This will only work for the FOV that is set at the beginning of the game but it should work.
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(Render)]
#[read_component(FieldOfView)]
#[read_component(Player)]
pub fn entity_render(ecs: &SubWorld, #[resource] camera: &Camera) {
let mut draw_batch = DrawBatch::new();
let mut fov = <&FieldOfView>::query().filter(component::<Player>());
draw_batch.target(1);
let offset = Point::new(camera.left_x, camera.top_y);
let player_fov = fov.iter(ecs).nth(0).unwrap();
<(&Point, &Render)>::query()
.iter(ecs)
.filter(|(pos, _)| player_fov.visible_tiles.contains(&pos))
.for_each(|(pos, render)| {
draw_batch.set(*pos - offset, render.color, render.glyph);
});
draw_batch.submit(5000).expect("Batch error");
}
Notice the new read at the top and the new mut we set at the beginning of the function. Then notice that we are now filtering for a FieldOfView from the player component.
Now we want to do the same things for the tooltip as well as you might be able to find monsters that are on their way with mouse-over.
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(Name)]
#[read_component(Health)]
#[read_component(FieldOfView)]
#[read_component(Player)]
pub fn tooltips(ecs: &SubWorld, #[resource] mouse_pos: &Point, #[resource] camera: &Camera) {
let mut positions = <(Entity, &Point, &Name)>::query();
let mut fov = <&FieldOfView>::query().filter(component::<Player>());
let offset = Point::new(camera.left_x, camera.top_y);
let map_pos = *mouse_pos + offset;
let mut draw_batch = DrawBatch::new();
let player_fov = fov.iter(ecs).nth(0).unwrap();
draw_batch.target(2);
positions
.iter(ecs)
.filter(|(_, pos, _)| **pos == map_pos && player_fov.visible_tiles.contains(&pos))
.for_each(|(entity, _, name)| {
let screen_pos = *mouse_pos * 2;
let display =
if let Ok(health) = ecs.entry_ref(*entity).unwrap().get_component::<Health>() {
format!("{} : {} hp", &name.0, health.current)
} else {
name.0.clone()
};
draw_batch.print(screen_pos, &display);
});
draw_batch.submit(10100).expect("Batch error");
}
We added in the new reads and then set the filter to pull of the player_fov.visible_tiles.contains(&pos).
Updating Fields of View
Okay so now we need to update the FOV for players and that of the monsters when they move. We already have the flag for have moved so we can utilize that for this to start to work.
// movement.rs
use crate::prelude::*;
#[system(for_each)]
#[read_component(Player)]
#[read_component(FieldOfView)]
pub fn movement(
entity: &Entity,
want_move: &WantsToMove,
#[resource] map: &mut 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 let Ok(entry) = ecs.entry_ref(want_move.entity) {
if let Ok(fov) = entry.get_component::<FieldOfView>() {
commands.add_component(want_move.entity, fov.clone_dirty());
if entry.get_component::<Player>().is_ok()
// (1)
{
camera.on_player_move(want_move.destination);
fov.visible_tiles.iter().for_each(|pos| {
// (2)
map.revealed_tiles[map_idx(pos.x, pos.y)] = true;
});
}
}
}
}
commands.remove(*entity);
}
New read and then a way to set the a dirty to true if we can enter the tile. Now we need to work on the Monsters.
Limiting Monsters’ Fields of View
Let’s add the functionality that we only can chase after a player if they are in the monsters field of view. What do you think we need to do? I would say that we need to have the monsters field of view and only run the chase algorithm if the player is with their fov. Let’s head to chasing.rs.
use crate::prelude::*;
#[system]
#[read_component(Point)]
#[read_component(ChasingPlayer)]
#[read_component(FieldOfView)]
#[read_component(Health)]
#[read_component(Player)]
pub fn chasing(#[resource] map: &Map, ecs: &SubWorld, commands: &mut CommandBuffer) {
let mut movers = <(Entity, &Point, &ChasingPlayer, &FieldOfView)>::query();
let mut positions = <(Entity, &Point, &Health)>::query();
let mut player = <(&Point, &Player)>::query();
// Finding the Player
let player_pos = player.iter(ecs).nth(0).unwrap().0;
let player_idx = map_idx(player_pos.x, player_pos.y);
//Dijkstra Maps
let search_targets = vec![player_idx];
let dijkstra_map =
DijkstraMap::new(CONSOLE_WIDTH, CONSOLE_HEIGHT, &search_targets, map, 1024.0);
// Chasing the Player
movers.iter(ecs).for_each(|(entity, pos, _, fov)| {
if !fov.visible_tiles.contains(&player_pos) {
return;
}
let idx = map_idx(pos.x, pos.y);
if let Some(destination) = DijkstraMap::find_lowest_exit(&dijkstra_map, idx, map) {
let distance = DistanceAlg::Pythagoras.distance2d(*pos, *player_pos);
let destination = if distance > 1.2 {
map.index_to_point2d(destination)
} else {
*player_pos
};
let mut attacked = false;
positions
.iter(ecs)
.filter(|(_, target_pos, _)| **target_pos == destination)
.for_each(|(victim, _, _)| {
if ecs
.entry_ref(*victim)
.unwrap()
.get_component::<Player>()
.is_ok()
{
commands.push((
(),
WantsToAttack {
attacker: *entity,
victim: *victim,
},
));
}
attacked = true;
});
if !attacked {
commands.push((
(),
WantsToMove {
entity: *entity,
destination,
},
));
}
}
});
}
Now the monsters will have a query that will check to see if the player is within their POV, once we have that we will check to see if any of the monsters are within sight range of the player then they will chase.
Adding Spatial Memory
Now comes the part of the game that will make it look like we are discovering the map as we go. We can assume that the player will have some memory of the map so lets make that work.
Revealing the Map
Let’s open up the map.rs and create a new vector for the revealed map tiles.
pub struct Map {
pub tiles: Vec<TileType>,
pub revealed_tiles: Vec<bool>,
}
...
impl Map {
pub fn new() -> Self {
Self {
tiles: vec![TileType::Floor; NUM_TILES],
revealed_tiles: vec![false; NUM_TILES],
}
}
...
}
Now we have a way to use the revealed tiles for the map, let’s update the revealed tiles when we move.
Updating the Map
if let Ok(fov) = entry.get_component::<FieldOfView>() {
commands.add_component(want_move.entity, fov.clone_dirty());
if entry.get_component::<Player>().is_ok() {
camera.on_player_move(want_move.destination);
fov.visible_tiles.iter().for_each(|pos| {
map.revealed_tiles[map_idx(pos.x, pos.y)] = true;
});
} // (3)
}
Okay so every time that we are in the game we are adding to the revealed when we move. We now need a way to render the memory a little different than the FOV.
Rendering Your Memory
Let’s head back to the map_render.rs and swap the view of the tiles if they are from memory.
use crate::prelude::*;
#[system]
#[read_component(FieldOfView)]
#[read_component(Player)]
pub fn map_render(ecs: &SubWorld, #[resource] map: &Map, #[resource] camera: &Camera) {
let mut fov = <&FieldOfView>::query().filter(component::<Player>());
let mut draw_batch = DrawBatch::new();
let player_fov = fov.iter(ecs).nth(0).unwrap();
draw_batch.target(0);
for y in camera.top_y..=camera.bottom_y {
for x in camera.left_x..camera.right_x {
let pt = Point::new(x, y);
let offset = Point::new(camera.left_x, camera.top_y);
let idx = map_idx(x, y);
if map.in_bounds(pt)
&& (player_fov.visible_tiles.contains(&pt) | map.revealed_tiles[idx])
{
let tint = if player_fov.visible_tiles.contains(&pt) {
WHITE
} else {
DARK_GRAY
};
match map.tiles[idx] {
TileType::Floor => {
draw_batch.set(pt - offset, ColorPair::new(tint, BLACK), to_cp437('.'));
}
TileType::Wall => {
draw_batch.set(pt - offset, ColorPair::new(tint, BLACK), to_cp437('#'));
}
}
}
}
}
draw_batch.submit(0).expect("Batch error");
}
Now depending on whether its part of the the current POV, the revealed_tiles or neither the render will show something different.
Other Uses of FOV
Now that we have that out of the way here are some uses of FOV:
• You now know how to check line of sight between points. If you’d like to
implement ranged combat, this solves one of the hardest problems—determining
if the shooter can see their target.
• If you’d like a fog effect, you could calculate two fields of view with different
ranges and vary the rendering effect on each.
• You could add items that vary the player’s visibility range—maybe magical
spectacles or night vision goggles.
• Using the render tinting system, you don’t have to use grey for areas. A
green tint provides a night-vision effect. Red tints could represent heat
signatures in a suspenseful “bug hunt” game. The sky’s the limit for your
imagination.
• You could include items such as a Scroll of Magic Mapping that reveal the map
as a reward.
• Explosions typically affect everything in line of sight of the boom. Calculate
a visibility set for your explosion and you have a list of all of the targets.
Wrap-Up
Okay so start to think about many different uses for these new items that we have worked on we now have a very dynamic and more difficult navigating dungeon.