Field of View (FOV)

The next step towards a complete roguelike is FOV. This adds a tactical element, and lets the player wonder what’s on the other side of every door and every corner! The FOV works like a light source where the player stands, casting light in every direction but not getting past any walls. Regions in shadow are invisible. You could code it yourself by casting rays outward from the player, but it’s much easier than that; libtcod has a whole module dedicated to it! It includes different methods with varying levels of precision, speed and other interesting properties. There’s an excellent study here if you want to know more about them, including tables and images comparing the different algorithms.

We’ll define the chosen algorithm along with some other constants so they can be changed later. For now we’ll just use the default (Basic) algorithm. There’s also an option to light walls or not; this is a matter of preference. Another important constant is the maximum radius for FOV calculations, how far the player can see in the dungeon. (Whether this is due to the player’s sight range or the light from the player’s torch depends on how you choose to explain this to the player.)

const FOV_ALGO: FovAlgorithm = FovAlgorithm::Basic; // default FOV algorithm
const FOV_LIGHT_WALLS: bool = true; // light walls or not
const TORCH_RADIUS: i32 = 10;

And we need to add colours for the lit tiles:

const COLOR_DARK_WALL: Color = Color { r: 0, g: 0, b: 100 };
const COLOR_LIGHT_WALL: Color = Color {
    r: 130,
    g: 110,
    b: 50,
};
const COLOR_DARK_GROUND: Color = Color {
    r: 50,
    g: 50,
    b: 150,
};
const COLOR_LIGHT_GROUND: Color = Color {
    r: 200,
    g: 180,
    b: 50,
};

The fov map object in tcod is called Map, which conflicts with our own dungeon map type. So we’ll rename tcod’s to FovMap on import:

use tcod::map::{FovAlgorithm, Map as FovMap};  (1)

struct Tcod {
    root: Root,
    con: Offscreen,
    fov: FovMap,  (2)
}
1 Bring the tcod::map::Map type in and alias it to FovMap
2 Add FovMap to the Tcod struct

We need to create the FOV map and ad it to our tcod variable in main. While we’re at it, we’ll initialise the Offscreen console directly too:

let mut tcod = Tcod {
    root,
    con: Offscreen::new(MAP_WIDTH, MAP_HEIGHT),  (1)
    fov: FovMap::new(MAP_WIDTH, MAP_HEIGHT),  (2)
};
1 Initialise con inline
2 Initialise fov

The libtcod FOV module needs to know which tiles block sight. So, we create a map that libtcod can understand, and fill it with the appropriate values from the tiles' own block_sight and blocked properties. Well, actually, only block_sight will be used; the blocked value is completely irrelevant for FOV! It will be useful only for the pathfinding module, but it doesn’t hurt to provide that value anyway. Also, libtcod asks for values that are the opposite of what we defined, so we toggle them with the negation (!) operator. This goes in the main function, before entering the main game loop.

// populate the FOV map, according to the generated map
for y in 0..MAP_HEIGHT {
    for x in 0..MAP_WIDTH {
        tcod.fov.set(
            x,
            y,
            !game.map[x as usize][y as usize].block_sight,
            !game.map[x as usize][y as usize].blocked,
        );
    }
}

FOV needs to be recomputed — but only if the player moves or a tile changes. To that end, we’ll keep track of player’s position from the previous run of the game loop and compare it to the current position.

Add this before the main game loop:

// force FOV "recompute" first time through the game loop
let mut previous_player_position = (-1, -1);

(we’re using (-1, -1) to make sure FOV gets computed on the first time through the loop)

Then this right before handle_keys (which is where the player’s position could change):

previous_player_position = (player.x, player.y);

And lastly, replace the existing call to render_all with these two lines:

// render the screen
let fov_recompute = previous_player_position != (objects[0].x, objects[0].y);
render_all(&mut tcod, &game, &objects, fov_recompute);

Now we need to change the rendering code to actually recompute FOV and display the result.

First, update the function definition to accept fov_map and fov_recompute:

fn render_all(tcod: &mut Tcod, game: &Game, objects: &[Object], fov_recompute: bool) {

Next, recompute the FOV map if the caller asked for it:

if fov_recompute {
    // recompute FOV if needed (the player moved or something)
    let player = &objects[0];
    tcod.fov
        .compute_fov(player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO);
}

As you can see, we’re using all the constants we defined earlier. We’ll extend the code that render the tiles so that if they are in the FOV they will get their light colours:

// go through all tiles, and set their background color
for y in 0..MAP_HEIGHT {
    for x in 0..MAP_WIDTH {
        let visible = tcod.fov.is_in_fov(x, y);
        let wall = game.map[x as usize][y as usize].block_sight;
        let color = match (visible, wall) {
            // outside of field of view:
            (false, true) => COLOR_DARK_WALL,
            (false, false) => COLOR_DARK_GROUND,
            // inside fov:
            (true, true) => COLOR_LIGHT_WALL,
            (true, false) => COLOR_LIGHT_GROUND,
        };
        tcod.con
            .set_char_background(x, y, color, BackgroundFlag::Set);
    }
}

We’ve replaced our if with a match. As we’ve added another check (whether a tile is visible or not), we would have to use nested ifs and end up with four separate calls to set_char_background. But since the only thing we’re changing is the colour, we’ll just pattern match to get the right value and use it.

Finally, we’ll make sure we render only objects that are in the player’s FOV. Wrap the object.draw call in render_all in a FOV check:

// draw all objects in the list
for object in objects {
    if tcod.fov.is_in_fov(object.x, object.y) {
        object.draw(&mut tcod.con);
    }
}

We’ve shuffled a lot of code around, but we haven’t changed much, conceptually. Just track whether the player moved, update FOV and render the map if they did, use lit vs. dark colours and only render visible objects.

And look how much better it looks now!

Exploration

The last detail after FOV is exploration, a.k.a Fog of War. You made it this far, so this will be a piece of cake! What, you may say, fog of war can’t possibly be the easiest thing to code in a roguelike! Well, it is. Wait and see.

First, all tiles will store whether they’re explored or not. They start unexplored. Put this in the definition of the Tile struct:

/// A tile of the map and its properties
#[derive(Clone, Copy, Debug)]
struct Tile {
    blocked: bool,
    explored: bool,  (1)
    block_sight: bool,
}
1 Adde the explored field

And you need to update the Tile::empty and Tile::wall bodies:

impl Tile {
    pub fn empty() -> Self {
        Tile {
            blocked: false,
            explored: false,  (1)
            block_sight: false,
        }
    }

    pub fn wall() -> Self {
        Tile {
            blocked: true,
            explored: false,  (2)
            block_sight: true,
        }
    }
}
1 Added the explored field
2 Added the explored field

Now, in the render_all function, after the (visible, wall) match make sure the visible tiles are explored and only render those that are:

let explored = &mut game.map[x as usize][y as usize].explored;
if visible {
    // since it's visible, explore it
    *explored = true;
}
if *explored {
    // show explored tiles only (any visible tile is explored already)
    tcod.con
        .set_char_background(x, y, color, BackgroundFlag::Set);
}

(we take a mutable reference to the explored field so we don’t have to write the full map[x as usize][y as usize].explored bit twice)

And finally, since we’re now actually modifying the map, we’ll need to pass a mutable reference to render_all. Since map is carried in the Game struct, we need to make the game variable mutable:

let mut game = Game {
    // generate map (at this point it's not drawn to the screen)
    map: make_map(&mut objects[0]),
};

Then, we pass a mutable reference to game to render_all:

// render the screen
let fov_recompute = previous_player_position != (objects[0].x, objects[0].y);
render_all(&mut tcod, &mut game, &objects, fov_recompute);  (1)
1 game is now &mut

And last, the render_all function definition:

fn render_all(tcod: &mut Tcod, game: &mut Game, objects: &[Object], fov_recompute: bool) {  (1)
1 game is now &mut Game

And that’s that! If you run the game now, you start in mostly black space except for your immediate surroundings and the map fills in as you explore.

One might argue that that render_all should not actually modify anything and that the FOV/exploration code belongs somewhere else. They wouldn’t necessarily be wrong. But let’s just roll with it for now.

Continue to the next part.