Roguelike Tutorial, using python3+tdl, part 4

From RogueBasin
Jump to navigation Jump to search

This is part of a series of tutorials; the main page can be found here.

The tutorial uses tdl version 3.1.0 and Python 3.5


Field-of-view and exploration


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; tdl has a function that can do it for you! 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 it's 0, the default 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.)


FOV_ALGO = 'BASIC'  #default FOV algorithm
FOV_LIGHT_WALLS = True
TORCH_RADIUS = 10


Also, we'll need more colors for lit tiles! The color definitions will now be:


color_dark_wall = (0, 0, 100)
color_light_wall = (130, 110, 50)
color_dark_ground = (50, 50, 150)
color_light_ground = (200, 180, 50)


FOV will only need to be recomputed if the player moves, or a tile changes. To model that we'll define a global variable fov_recompute = True before the main loop. Then, in the handle_keys function, whenever the player moves we set it to True again, like in the following code.


    #movement keys
    if user_input.key == 'UP':
        player.move(0, -1)
        fov_recompute = True

    elif user_input.key == 'DOWN':
        player.move(0, 1)
        fov_recompute = True

    elif user_input.key == 'LEFT':
        player.move(-1, 0)
        fov_recompute = True

    elif user_input.key == 'RIGHT':
        player.move(1, 0)
        fov_recompute = True


Now we need to change the rendering code to actually recompute FOV, and display the result! It's a major overhaul of the render_all function. We only need to recompute FOV and render the map if recompute_fov is True (and then we reset it to False), done by the following code.


    if fov_recompute:
        #recompute FOV if needed (the player moved or something)
        fov_recompute = False
        visible_tiles = tdl.map.quickFOV(player.x, player.y,
                                         is_visible_tile,
                                         fov=FOV_ALGO,
                                         radius=TORCH_RADIUS,
                                         lightWalls=FOV_LIGHT_WALLS)


As you can see we're using all the constants we defined earlier. After that comes the code that iterates over all tiles and displays them in the console. We'll add an extra condition for each tile:


visible = (x, y) in visible_tiles


Depending on the value of visible, the tile may be drawn in different colors (lit or dark). We'll show all of the modified map display code to make this a bit more clear.


        #go through all tiles, and set their background color according to the FOV
        for y in range(MAP_HEIGHT):
            for x in range(MAP_WIDTH):
                visible = (x, y) in visible_tiles
                wall = my_map[x][y].block_sight
                if not visible:
                    #it's out of the player's FOV
                    if wall:
                        con.draw_char(x, y, None, fg=None, bg=color_dark_wall)
                    else:
                        con.draw_char(x, y, None, fg=None, bg=color_dark_ground)
                else:
                    #it's visible
                    if wall:
                        con.draw_char(x, y, None, fg=None, bg=color_light_wall)
                    else:
                        con.draw_char(x, y, None, fg=None, bg=color_light_ground)


There, it's done!

The last detail is to make sure objects only show if they're in the player's FOV. In the GameObject 's draw method, add a FOV check before drawing:


        if (self.x, self.y) in visible_tiles:


Apart from defining the newly used global values in render_all and handle_keys (they're visible_tiles and fov_recompute), that's all there is to it. This is actually one aspect that can take a long time to get right in a roguelike, fortunately we were able to do it with a modest amount of work!

The whole code for this section is here.

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. This is in the Tile 's __init__ method.


self.explored = False


Now, in the render_all function, after the if not visible: line, add this:


                    #if it's not visible right now, the player can only see it if it's explored
                    if my_map[x][y].explored:


And indent the next four lines so they only execute if that's true. So only explored tiles will be drawn.

Then, after rendering a visible tile (right at the end of the function), explore the visible tile:


                    my_map[x][y].explored = True


And that is all. The level will start black, but you'll slowly uncover it. Explored regions are still visible but are in a different color and won't reveal any objects (such as lurking monsters)! It's an exploration game now.

The whole code is available here.

Go on to the next part.