|
| 1 | +--- |
| 2 | +toc: true |
| 3 | +--- |
| 4 | + |
| 5 | +# Part 4 - Field of view |
| 6 | + |
| 7 | +Currently the dungeon is fully visible at all times. |
| 8 | +To add a sense of exploration we will begin tracking which tiles are explored or in view at all times. |
| 9 | + |
| 10 | +First is to add two new arrays to `GameMap`. |
| 11 | +`visible` is a boolean array of all currently visible tiles. |
| 12 | +`explored` is a boolean array of all tiles that have ever been seen. |
| 13 | +Add these changes to `/game/game_map.py`: |
| 14 | +```diff |
| 15 | + self.entities: Set[game.entity.Entity] = set() |
| 16 | + self.enter_xy = (width // 2, height // 2) # Entrance coordinates. |
| 17 | +{{' '}} |
| 18 | ++ self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see |
| 19 | ++ self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before |
| 20 | ++ |
| 21 | + def in_bounds(self, x: int, y: int) -> bool: |
| 22 | +``` |
| 23 | +An alternative way to create the above arrays is with `np.zeros((width, height), dtype=bool, order="F")`. |
| 24 | + |
| 25 | +Python-tcod ports Libtcod's FOV algorithms with the [tcod.map.compute_fov](https://python-tcod.readthedocs.io/en/latest/tcod/map.html#tcod.map.compute_fov) function. |
| 26 | +This function takes a 2D array where the zero values are opaque walls, and all non-zero tiles are transparent. |
| 27 | +Because we've already used 0 for walls and 1 for floors the tiles of `GameMap.tiles` can be passed directory to this function. |
| 28 | +The function returns which tiles are visible as a boolean array. |
| 29 | + |
| 30 | +We now add a method to `Engine` to handle updating the FOV. |
| 31 | +Make the following changes to `/game/engine.py`: |
| 32 | +```diff |
| 33 | + import random |
| 34 | +{{' '}} |
| 35 | ++import tcod |
| 36 | ++ |
| 37 | + import game.entity |
| 38 | + ... |
| 39 | + player: game.entity.Entity |
| 40 | + rng: random.Random |
| 41 | ++ |
| 42 | ++ def update_fov(self) -> None: |
| 43 | ++ """Recompute the visible area based on the players point of view.""" |
| 44 | ++ self.game_map.visible[:] = tcod.map.compute_fov( |
| 45 | ++ self.game_map.tiles, |
| 46 | ++ (self.player.x, self.player.y), |
| 47 | ++ radius=8, |
| 48 | ++ algorithm=tcod.FOV_SYMMETRIC_SHADOWCAST, |
| 49 | ++ ) |
| 50 | ++ # If a tile is currently "visible" it will also be marked as "explored". |
| 51 | ++ self.game_map.explored |= self.game_map.visible |
| 52 | +``` |
| 53 | + |
| 54 | + |
| 55 | +We must call this function after every action is performed in `/game/input_handlers.py`: |
| 56 | +```diff |
| 57 | + def handle_action(self, action: game.actions.Action) -> EventHandler: |
| 58 | + """Handle actions returned from event methods.""" |
| 59 | + action.perform() |
| 60 | ++ self.engine.update_fov() |
| 61 | + return self |
| 62 | +``` |
| 63 | +The above will only update the visible area after the player moves. |
| 64 | +Because of that we also need to update it before the first turn, so another call gets added to `/main.py` after the map and player is setup: |
| 65 | +```diff |
| 66 | + engine.player = game.entity.Entity(engine.game_map, *engine.game_map.enter_xy, "@", (255, 255, 255)) |
| 67 | ++ engine.update_fov() |
| 68 | + |
| 69 | + event_handler = game.input_handlers.EventHandler(engine) |
| 70 | +``` |
| 71 | + |
| 72 | +Now it is time to modify the rendering function. |
| 73 | +We need a default graphic for the unexplored area so a NumpPy scalar called `SHROUD` (a thing that obscures) is made, which is a blank black tile. |
| 74 | +Tiles in the current view will have the graphics stay the same as before. |
| 75 | +We use `tile_graphics[gamemap.tiles]` as before to generate the entire array but will render only the visible area by the end, this will be called `light`. |
| 76 | +A copy of `light` is made called `dark` and this array modified to be darker with the foreground at half brightness and the background at 1/8th brightness. |
| 77 | + |
| 78 | +Now [np.select](https://numpy.org/doc/stable/reference/generated/numpy.select.html) is used to determine which |
| 79 | +If a value on `gamemap.visible` is True then the value at `light` will be shown, then `gamemap.explored` is checked and if that is True then `dark` is shown, otherwise `SHROUD` is used. |
| 80 | +Because `SHROUD` is a scalar it is [broadcast](https://numpy.org/doc/stable/user/basics.broadcasting.html) along the entire array. |
| 81 | +The `default` parameter could have been another 2D array, or the arrays in `condlist` could have been scalars like `SHROUD`, the important part is that these fit the shape of `console.rgb[0 : gamemap.width, 0 : gamemap.height]` acorind to the [broadcasting rules](https://numpy.org/doc/stable/user/basics.broadcasting.html). |
| 82 | + |
| 83 | +The final step is the entities, any entity that is not on a visible tile is not printed to the screen. |
| 84 | + |
| 85 | +With this in mind we can edit `/game/rendering.py`: |
| 86 | +```diff |
| 87 | ++# SHROUD represents unexplored, unseen tiles |
| 88 | ++SHROUD = np.array((ord(" "), (255, 255, 255), (0, 0, 0)), dtype=tcod.console.rgb_graphic) |
| 89 | ++ |
| 90 | + |
| 91 | + def render_map(console: tcod.Console, gamemap: game.game_map.GameMap) -> None: |
| 92 | +- console.rgb[0 : gamemap.width, 0 : gamemap.height] = tile_graphics[gamemap.tiles] |
| 93 | ++ # The default graphics are of tiles that are visible. |
| 94 | ++ light = tile_graphics[gamemap.tiles] |
| 95 | ++ |
| 96 | ++ # Apply effects to create a darkened map of tile graphics. |
| 97 | ++ dark = light.copy() |
| 98 | ++ dark["fg"] //= 2 |
| 99 | ++ dark["bg"] //= 8 |
| 100 | ++ |
| 101 | ++ # If a tile is in the "visible" array, then draw it with the "light" colors. |
| 102 | ++ # If it isn't, but it's in the "explored" array, then draw it with the "dark" colors. |
| 103 | ++ # Otherwise, the default graphic is "SHROUD". |
| 104 | ++ console.rgb[0 : gamemap.width, 0 : gamemap.height] = np.select( |
| 105 | ++ condlist=[gamemap.visible, gamemap.explored], |
| 106 | ++ choicelist=[light, dark], |
| 107 | ++ default=SHROUD, |
| 108 | ++ ) |
| 109 | + |
| 110 | + for entity in gamemap.entities: |
| 111 | ++ if not gamemap.visible[entity.x, entity.y]: |
| 112 | ++ continue # Skip entities that are not in the FOV. |
| 113 | + console.print(entity.x, entity.y, entity.char, fg=entity.color) |
| 114 | +``` |
| 115 | + |
| 116 | +You can see the current progress of this code in its entirety [here](https://github.com/TStand90/tcod_tutorial_v2/tree/2021/part-4). |
| 117 | + |
| 118 | +[Continue to part 5](part-5). |
| 119 | + |
| 120 | +[Return to the hub](.). |
0 commit comments