McRogueFace/roguelike_tutorial/rogueliketutorials.com/Part 4 - Field of View · Ro...

627 lines
39 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en" style="color-scheme: dark;"><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>
Part 4 - Field of View · Roguelike Tutorials
</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<meta name="description" content="We have a dungeon now, and we can move about it freely. But are we really exploring the dungeon if we can just see it all from the beginning?
Most roguelikes (not all!) only let you see within a certain range of your character, and ours will be no different. We need to implement a way to calculate the “Field of View” for our adventurer, and fortunately, tcod makes that easy!">
<meta name="keywords" content="">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Part 4 - Field of View">
<meta name="twitter:description" content="We have a dungeon now, and we can move about it freely. But are we really exploring the dungeon if we can just see it all from the beginning?
Most roguelikes (not all!) only let you see within a certain range of your character, and ours will be no different. We need to implement a way to calculate the “Field of View” for our adventurer, and fortunately, tcod makes that easy!">
<meta property="og:title" content="Part 4 - Field of View">
<meta property="og:description" content="We have a dungeon now, and we can move about it freely. But are we really exploring the dungeon if we can just see it all from the beginning?
Most roguelikes (not all!) only let you see within a certain range of your character, and ours will be no different. We need to implement a way to calculate the “Field of View” for our adventurer, and fortunately, tcod makes that easy!">
<meta property="og:type" content="article">
<meta property="og:url" content="https://rogueliketutorials.com/tutorials/tcod/v2/part-4/"><meta property="article:section" content="tutorials">
<meta property="article:published_time" content="2020-06-29T00:00:00+00:00">
<meta property="article:modified_time" content="2020-06-29T00:00:00+00:00">
<link rel="canonical" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-4/">
<link rel="preload" href="https://rogueliketutorials.com/fonts/forkawesome-webfont.woff2?v=1.2.0" as="font" type="font/woff2" crossorigin="">
<link rel="stylesheet" href="Part%204%20-%20Field%20of%20View%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.c4d7e93a158eda5a65b3df343745d2092a0a1e2170feeec909.css" integrity="sha256-xNfpOhWO2lpls980N0XSCSoKHiFw/u7JCbiolEOQPGo=" crossorigin="anonymous" media="screen">
<link rel="stylesheet" href="Part%204%20-%20Field%20of%20View%20%C2%B7%20Roguelike%20Tutorials_files/coder-dark.min.78b5fe3864945faf5207fb8fe3ab2320d49c3365def0e.css" integrity="sha256-eLX+OGSUX69SB/uP46sjINScM2Xe8OiKwd8N2txUoDw=" crossorigin="anonymous" media="screen">
<link rel="stylesheet" href="Part%204%20-%20Field%20of%20View%20%C2%B7%20Roguelike%20Tutorials_files/style.min.9d3eb202952dddb888856ff12c83bc88de866c596286bfb4c1.css" integrity="sha256-nT6yApUt3biIhW/xLIO8iN6GbFlihr+0wfjmvq2a42Y=" crossorigin="anonymous" media="screen">
<link rel="icon" type="image/png" href="https://rogueliketutorials.com/images/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="https://rogueliketutorials.com/images/favicon-16x16.png" sizes="16x16">
<link rel="apple-touch-icon" href="https://rogueliketutorials.com/images/apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="180x180" href="https://rogueliketutorials.com/images/apple-touch-icon.png">
<link rel="manifest" href="https://rogueliketutorials.com/site.webmanifest">
<link rel="mask-icon" href="https://rogueliketutorials.com/images/safari-pinned-tab.svg" color="#5bbad5">
<meta name="generator" content="Hugo 0.110.0">
<style>:is([id*='google_ads_iframe'],[id*='taboola-'],.taboolaHeight,.taboola-placeholder,#top-ad,#credential_picker_container,#credentials-picker-container,#credential_picker_iframe,[id*='google-one-tap-iframe'],#google-one-tap-popup-container,.google-one-tap__module,.google-one-tap-modal-div,#amp_floatingAdDiv,#ez-content-blocker-container) {display:none!important;min-height:0!important;height:0!important;}</style></head>
<body class="colorscheme-dark vsc-initialized">
<div class="float-container">
<a id="dark-mode-toggle" class="colorscheme-toggle">
<i class="fa fa-adjust fa-fw" aria-hidden="true"></i>
</a>
</div>
<main class="wrapper">
<nav class="navigation">
<section class="container">
<a class="navigation-title" href="https://rogueliketutorials.com/">
Roguelike Tutorials
</a>
<input type="checkbox" id="menu-toggle">
<label class="menu-button float-right" for="menu-toggle">
<i class="fa fa-bars fa-fw" aria-hidden="true"></i>
</label>
<ul class="navigation-list">
<li class="navigation-item">
<a class="navigation-link" href="https://rogueliketutorials.com/">Home</a>
</li>
<li class="navigation-item">
<a class="navigation-link" href="https://rogueliketutorials.com/tutorials/tcod/v2/">TCOD Tutorial (2020)</a>
</li>
<li class="navigation-item">
<a class="navigation-link" href="https://rogueliketutorials.com/tutorials/tcod/2019/">TCOD Tutorial (2019)</a>
</li>
<li class="navigation-item">
<a class="navigation-link" href="https://rogueliketutorials.com/about/">About</a>
</li>
</ul>
</section>
</nav>
<div class="content">
<section class="container page">
<article>
<header>
<h1 class="title">
<a class="title-link" href="https://rogueliketutorials.com/tutorials/tcod/v2/part-4/">
Part 4 - Field of View
</a>
</h1>
</header>
<p>We have a dungeon now, and we can move about it freely. But are we
really <em>exploring</em> the dungeon if we can just see it all from the
beginning?</p>
<p>Most roguelikes (not all!) only let you see within a certain range of
your character, and ours will be no different. We need to implement a way
to calculate the “Field of View” for our adventurer, and fortunately,
tcod makes that easy!</p>
<p>When walking around the dungeon, there will essentially be three “states” a tile can be in, relating to our field of view.</p>
<ol>
<li>Visible</li>
<li>Not visible</li>
<li>Not visible, but previously seen</li>
</ol>
<p>What this means is that we should draw the “visible” tiles as well as
the “not visible, but previously seen” ones to the screen, but
differentiate them somehow. The “not visible” tiles can simply be drawn
as an empty tile, with the color black, gray, or whatever you want to
use.</p>
<p>In order to differentiate between these tiles, well need two new
Numpy arrays: One to keep track of the tiles that are currently visible,
and another to keep track of all the tiles that our character has seen
before. Add the two arrays to <code>GameMap</code> like this:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class GameMap:
</span></span><span style="display:flex;"><span> def __init__(self, width: int, height: int):
</span></span><span style="display:flex;"><span> self.width, self.height = width, height
</span></span><span style="display:flex;"><span> self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class GameMap:
def __init__(self, width: int, height: int):
self.width, self.height = width, height
self.tiles = np.full((width, height), fill_value=tile_types.wall, order="F")
<span class="new-text">self.visible = np.full((width, height), fill_value=False, order="F") # Tiles the player can currently see
self.explored = np.full((width, height), fill_value=False, order="F") # Tiles the player has seen before</span></pre>
</div>
</div>
<p>We create two arrays, <code>visible</code> and <code>explored</code>, and fill them with the value <code>False</code>. In a moment, well create a function that will update these arrays based on whats in the field of view.</p>
<p>Lets turn our attention back to the tile types. Remember when we
specified the “walkable”, “transparent”, and “dark” attributes? We
called it “dark” because its what the tile will look like when its not
in the field of view, but what about when it <em>is</em>?</p>
<p>For that, well want a new <code>graphic_dt</code> in the <code>tile_dt</code> type, called <code>light</code>. We can add that by modifying <code>tile_types.py</code> like this:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>tile_dt = np.dtype(
</span></span><span style="display:flex;"><span> [
</span></span><span style="display:flex;"><span> ("walkable", np.bool), # True if this tile can be walked over.
</span></span><span style="display:flex;"><span> ("transparent", np.bool), # True if this tile doesn't block FOV.
</span></span><span style="display:flex;"><span> ("dark", graphic_dt), # Graphics for when this tile is not in FOV.
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ ("light", graphic_dt), # Graphics for when the tile is in FOV.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span> ]
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>def new_tile(
</span></span><span style="display:flex;"><span> *, # Enforce the use of keywords, so that parameter order doesn't matter.
</span></span><span style="display:flex;"><span> walkable: int,
</span></span><span style="display:flex;"><span> transparent: int,
</span></span><span style="display:flex;"><span> dark: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]],
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ light: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]],
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>) -&gt; np.ndarray:
</span></span><span style="display:flex;"><span> """Helper function for defining individual tile types """
</span></span><span style="display:flex;"><span><span style="color:#f92672">- return np.array((walkable, transparent, dark), dtype=tile_dt)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ return np.array((walkable, transparent, dark, light), dtype=tile_dt)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+# SHROUD represents unexplored, unseen tiles
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+SHROUD = np.array((ord(" "), (255, 255, 255), (0, 0, 0)), dtype=graphic_dt)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>floor = new_tile(
</span></span><span style="display:flex;"><span><span style="color:#f92672">- walkable=True, transparent=True, dark=(ord(" "), (255, 255, 255), (50, 50, 150)),
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ walkable=True,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ transparent=True,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dark=(ord(" "), (255, 255, 255), (50, 50, 150)),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ light=(ord(" "), (255, 255, 255), (200, 180, 50)),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>)
</span></span><span style="display:flex;"><span>wall = new_tile(
</span></span><span style="display:flex;"><span><span style="color:#f92672">- walkable=False, transparent=False, dark=(ord(" "), (255, 255, 255), (0, 0, 100)),
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ walkable=False,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ transparent=False,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ dark=(ord(" "), (255, 255, 255), (0, 0, 100)),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ light=(ord(" "), (255, 255, 255), (130, 110, 50)),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>)
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>tile_dt = np.dtype(
[
("walkable", np.bool), # True if this tile can be walked over.
("transparent", np.bool), # True if this tile doesn't block FOV.
("dark", graphic_dt), # Graphics for when this tile is not in FOV.
<span class="new-text">("light", graphic_dt), # Graphics for when the tile is in FOV.</span>
]
)
def new_tile(
*, # Enforce the use of keywords, so that parameter order doesn't matter.
walkable: int,
transparent: int,
dark: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]],
<span class="new-text">light: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]],</span>
) -&gt; np.ndarray:
"""Helper function for defining individual tile types """
<span class="crossed-out-text">return np.array((walkable, transparent, dark), dtype=tile_dt)</span>
<span class="new-text">return np.array((walkable, transparent, dark, light), dtype=tile_dt)</span>
<span class="new-text"># SHROUD represents unexplored, unseen tiles
SHROUD = np.array((ord(" "), (255, 255, 255), (0, 0, 0)), dtype=graphic_dt)</span>
floor = new_tile(
<span class="crossed-out-text">walkable=True, transparent=True, dark=(ord(" "), (255, 255, 255), (50, 50, 150)),</span>
<span class="new-text">walkable=True,
transparent=True,
dark=(ord(" "), (255, 255, 255), (50, 50, 150)),
light=(ord(" "), (255, 255, 255), (200, 180, 50)),</span>
)
wall = new_tile(
<span class="crossed-out-text">walkable=False, transparent=False, dark=(ord(" "), (255, 255, 255), (0, 0, 100)),</span>
<span class="new-text">walkable=False,
transparent=False,
dark=(ord(" "), (255, 255, 255), (0, 0, 100)),
light=(ord(" "), (255, 255, 255), (130, 110, 50)),</span>
)</pre>
</div>
</div>
<p>Lets go through the new additions.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span>tile_dt <span style="color:#f92672">=</span> np<span style="color:#f92672">.</span>dtype(
</span></span><span style="display:flex;"><span> [
</span></span><span style="display:flex;"><span> (<span style="color:#e6db74">"walkable"</span>, np<span style="color:#f92672">.</span>bool), <span style="color:#75715e"># True if this tile can be walked over.</span>
</span></span><span style="display:flex;"><span> (<span style="color:#e6db74">"transparent"</span>, np<span style="color:#f92672">.</span>bool), <span style="color:#75715e"># True if this tile doesn't block FOV.</span>
</span></span><span style="display:flex;"><span> (<span style="color:#e6db74">"dark"</span>, graphic_dt), <span style="color:#75715e"># Graphics for when this tile is not in FOV.</span>
</span></span><span style="display:flex;"><span> (<span style="color:#e6db74">"light"</span>, graphic_dt), <span style="color:#75715e"># Graphics for when the tile is in FOV.</span>
</span></span><span style="display:flex;"><span> ]
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>Were adding a new <code>graphic_dt</code> to the <code>tile_dt</code> that we use to define our tiles. <code>light</code> will hold the information about what our tile looks like when its in the field of view.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">new_tile</span>(
</span></span><span style="display:flex;"><span> <span style="color:#f92672">*</span>, <span style="color:#75715e"># Enforce the use of keywords, so that parameter order doesn't matter.</span>
</span></span><span style="display:flex;"><span> walkable: int,
</span></span><span style="display:flex;"><span> transparent: int,
</span></span><span style="display:flex;"><span> dark: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]],
</span></span><span style="display:flex;"><span> light: Tuple[int, Tuple[int, int, int], Tuple[int, int, int]],
</span></span><span style="display:flex;"><span>) <span style="color:#f92672">-&gt;</span> np<span style="color:#f92672">.</span>ndarray:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Helper function for defining individual tile types """</span>
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> np<span style="color:#f92672">.</span>array((walkable, transparent, dark, light), dtype<span style="color:#f92672">=</span>tile_dt)
</span></span></code></pre></div><p>Weve modified the <code>new_tile</code> function to account for the new <code>light</code> attribute. <code>light</code> works the same as <code>dark</code>.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span><span style="color:#75715e"># SHROUD represents unexplored, unseen tiles</span>
</span></span><span style="display:flex;"><span>SHROUD <span style="color:#f92672">=</span> np<span style="color:#f92672">.</span>array((ord(<span style="color:#e6db74">" "</span>), (<span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>), (<span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>)), dtype<span style="color:#f92672">=</span>graphic_dt)
</span></span></code></pre></div><p><code>SHROUD</code> is what well use for when a tile is neither in view nor has been “explored”. Its set to just draw a black tile.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span>floor <span style="color:#f92672">=</span> new_tile(
</span></span><span style="display:flex;"><span> walkable<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span> transparent<span style="color:#f92672">=</span><span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span> dark<span style="color:#f92672">=</span>(ord(<span style="color:#e6db74">" "</span>), (<span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>), (<span style="color:#ae81ff">50</span>, <span style="color:#ae81ff">50</span>, <span style="color:#ae81ff">150</span>)),
</span></span><span style="display:flex;"><span> light<span style="color:#f92672">=</span>(ord(<span style="color:#e6db74">" "</span>), (<span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>), (<span style="color:#ae81ff">200</span>, <span style="color:#ae81ff">180</span>, <span style="color:#ae81ff">50</span>)),
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>wall <span style="color:#f92672">=</span> new_tile(
</span></span><span style="display:flex;"><span> walkable<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span> transparent<span style="color:#f92672">=</span><span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span> dark<span style="color:#f92672">=</span>(ord(<span style="color:#e6db74">" "</span>), (<span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>), (<span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">0</span>, <span style="color:#ae81ff">100</span>)),
</span></span><span style="display:flex;"><span> light<span style="color:#f92672">=</span>(ord(<span style="color:#e6db74">" "</span>), (<span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>, <span style="color:#ae81ff">255</span>), (<span style="color:#ae81ff">130</span>, <span style="color:#ae81ff">110</span>, <span style="color:#ae81ff">50</span>)),
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>Finally, we add <code>light</code> to both the <code>floor</code> and <code>wall</code>
tiles. We also modify the functions to fit a bit better on the screen,
adding new lines after each argument. This is just for the sake of
readability.</p>
<p><code>light</code> in both cases is set to a brighter color, so that
when we draw the field of view to the screen, the player can easily
differentiate between whats in view and whats not. As usual, feel free
to play with the color schemes to match whatever you might have in
mind.</p>
<p>With all that in place, we need to modify the way <code>GameMap</code> draws itself to the screen.</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>class GameMap:
</span></span><span style="display:flex;"><span> ...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> def render(self, console: Console) -&gt; None:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- console.tiles_rgb[0:self.width, 0:self.height] = self.tiles["dark"]
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ """
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ Renders the map.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ If a tile is in the "visible" array, then draw it with the "light" colors.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ Otherwise, the default is "SHROUD".
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.tiles_rgb[0:self.width, 0:self.height] = np.select(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ condlist=[self.visible, self.explored],
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ choicelist=[self.tiles["light"], self.tiles["dark"]],
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ default=tile_types.SHROUD
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
</span></span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>class GameMap:
...
def render(self, console: Console) -&gt; None:
<span class="crossed-out-text">console.tiles_rgb[0:self.width, 0:self.height] = self.tiles["dark"]</span>
<span class="new-text">"""
Renders the map.
If a tile is in the "visible" array, then draw it with the "light" colors.
If it isn't, but it's in the "explored" array, then draw it with the "dark" colors.
Otherwise, the default is "SHROUD".
"""
console.tiles_rgb[0:self.width, 0:self.height] = np.select(
condlist=[self.visible, self.explored],
choicelist=[self.tiles["light"], self.tiles["dark"]],
default=tile_types.SHROUD
)</span></pre>
</div>
</div>
<p>The first part of the statement, <code>console.tiles_rgb[0:self.width, 0:self.height]</code>, hasnt changed. But instead of just setting it to <code>self.tiles["dark"]</code>, were using <code>np.select</code>.</p>
<p><code>np.select</code> allows us to conditionally draw the tiles we want, based on whats specified in <code>condlist</code>. Since were passing <code>[self.visible, self.explored]</code>, it will check if the tile being drawn is either visible, then explored. If its visible, it uses the first value in <code>choicelist</code>, in this case, <code>self.tiles["light"]</code>. If its not visible, but explored, then we draw <code>self.tiles["dark"]</code>. If neither is true, we use the <code>default</code> argument, which is just the <code>SHROUD</code> we defined earlier.</p>
<p>If you run the project now, none of the tiles will be drawn to the screen. This is because we need a way to actually modify the <code>visible</code> and <code>explored</code> tiles. Lets modify <code>Engine</code> to do just that:</p>
<div>
<button class="btn btn-primary data-toggle-tab active" data-toggle-tab="diff">
Diff
</button>
<button class="btn btn-secondary data-toggle-tab" data-toggle-tab="original">
Original
</button>
<div class="data-pane active" data-pane="diff">
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-diff" data-lang="diff"><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>from tcod.context import Context
</span></span><span style="display:flex;"><span>from tcod.console import Console
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+from tcod.map import compute_fov
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span>from entity import Entity
</span></span><span style="display:flex;"><span>...
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>class Engine:
</span></span><span style="display:flex;"><span> def __init__(self, entities: Set[Entity], event_handler: EventHandler, game_map: GameMap, player: Entity):
</span></span><span style="display:flex;"><span> self.entities = entities
</span></span><span style="display:flex;"><span> self.event_handler = event_handler
</span></span><span style="display:flex;"><span> self.game_map = game_map
</span></span><span style="display:flex;"><span> self.player = player
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.update_fov()
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def handle_events(self, events: Iterable[Any]) -&gt; None:
</span></span><span style="display:flex;"><span> for event in events:
</span></span><span style="display:flex;"><span> action = self.event_handler.dispatch(event)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> if action is None:
</span></span><span style="display:flex;"><span> continue
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> action.perform(self, self.player)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.update_fov() # Update the FOV before the players next action.
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ def update_fov(self) -&gt; None:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ """Recompute the visible area based on the players point of view."""
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.game_map.visible[:] = compute_fov(
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.game_map.tiles["transparent"],
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ (self.player.x, self.player.y),
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ radius=8,
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ )
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ # If a tile is "visible" it should be added to "explored".
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ self.game_map.explored |= self.game_map.visible
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> def render(self, console: Console, context: Context) -&gt; None:
</span></span><span style="display:flex;"><span> self.game_map.render(console)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> for entity in self.entities:
</span></span><span style="display:flex;"><span><span style="color:#f92672">- console.print(entity.x, entity.y, entity.char, fg=entity.color)
</span></span></span><span style="display:flex;"><span><span style="color:#f92672"></span><span style="color:#a6e22e">+ # Only print entities that are in the FOV
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ if self.game_map.visible[entity.x, entity.y]:
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e">+ console.print(entity.x, entity.y, entity.char, fg=entity.color)
</span></span></span><span style="display:flex;"><span><span style="color:#a6e22e"></span>
</span></span><span style="display:flex;"><span> context.present(console)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> console.clear()
</span></span></code></pre></div>
</div>
<div class="data-pane" data-pane="original">
<pre>...
from tcod.context import Context
from tcod.console import Console
<span class="new-text">from tcod.map import compute_fov</span>
from entity import Entity
...
class Engine:
def __init__(self, entities: Set[Entity], event_handler: EventHandler, game_map: GameMap, player: Entity):
self.entities = entities
self.event_handler = event_handler
self.game_map = game_map
self.player = player
<span class="new-text">self.update_fov()</span>
def handle_events(self, events: Iterable[Any]) -&gt; None:
for event in events:
action = self.event_handler.dispatch(event)
if action is None:
continue
action.perform(self, self.player)
<span class="new-text">self.update_fov() # Update the FOV before the players next action.</span>
<span class="new-text">def update_fov(self) -&gt; None:
"""Recompute the visible area based on the players point of view."""
self.game_map.visible[:] = compute_fov(
self.game_map.tiles["transparent"],
(self.player.x, self.player.y),
radius=8,
)
# If a tile is "visible" it should be added to "explored".
self.game_map.explored |= self.game_map.visible</span>
def render(self, console: Console, context: Context) -&gt; None:
self.game_map.render(console)
for entity in self.entities:
<span class="crossed-out-text">console.print(entity.x, entity.y, entity.char, fg=entity.color)</span>
<span class="new-text"># Only print entities that are in the FOV
if self.game_map.visible[entity.x, entity.y]:
console.print(entity.x, entity.y, entity.char, fg=entity.color)</span>
context.present(console)
console.clear()</pre>
</div>
</div>
<p>The most important part of our additions is the <code>update_fov</code> function.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-py3" data-lang="py3"><span style="display:flex;"><span> <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">update_fov</span>(self) <span style="color:#f92672">-&gt;</span> <span style="color:#66d9ef">None</span>:
</span></span><span style="display:flex;"><span> <span style="color:#e6db74">"""Recompute the visible area based on the players point of view."""</span>
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>game_map<span style="color:#f92672">.</span>visible[:] <span style="color:#f92672">=</span> compute_fov(
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>game_map<span style="color:#f92672">.</span>tiles[<span style="color:#e6db74">"transparent"</span>],
</span></span><span style="display:flex;"><span> (self<span style="color:#f92672">.</span>player<span style="color:#f92672">.</span>x, self<span style="color:#f92672">.</span>player<span style="color:#f92672">.</span>y),
</span></span><span style="display:flex;"><span> radius<span style="color:#f92672">=</span><span style="color:#ae81ff">8</span>,
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># If a tile is "visible" it should be added to "explored".</span>
</span></span><span style="display:flex;"><span> self<span style="color:#f92672">.</span>game_map<span style="color:#f92672">.</span>explored <span style="color:#f92672">|=</span> self<span style="color:#f92672">.</span>game_map<span style="color:#f92672">.</span>visible
</span></span></code></pre></div><p>Were setting the <code>game_map</code>s <code>visible</code> tiles to equal the result of the <code>compute_fov</code>. Were giving <code>compute_fov</code> three arguments, which it uses to compute our field of view.</p>
<ul>
<li><code>transparency</code>: This is the first argument, which were passing <code>self.game_map.tiles["transparent"]</code>. <code>transparency</code>
takes a 2D numpy array, and considers any non-zero values to be
transparent. This is the array it uses to calculate the field of view.</li>
<li><code>pov</code>: The origin point for the field of view, which is a 2D index. We use the players x and y position here.</li>
<li><code>radius</code>: How far the FOV extends.</li>
</ul>
<p>Theres more that this function can do, including not lighting up
walls, and using different algorithms to calculate the FOV. If youre
interested, you can find the documentation <a href="https://python-tcod.readthedocs.io/en/latest/tcod/map.html#tcod.map.compute_fov">here</a>.</p>
<p>The line <code>self.game_map.explored |= self.game_map.visible</code> sets the <code>explored</code> array to include everything in the <code>visible</code> array, plus whatever it already had. This means that any tile the player can see, the player has also “explored.”</p>
<p>Thats all we need to do to update our field of view. Notice that we call the function when we initialize the <code>Engine</code>
class, so that the field of view is created before the player can move,
and after handling an action, so that whenever the player does move,
the field of view will be updated.</p>
<p>Lastly, we modify the part that draws the entities, so that only entities in the field of view are drawn.</p>
<p>Run the project now, and youll see something like this:</p>
<p><img src="Part%204%20-%20Field%20of%20View%20%C2%B7%20Roguelike%20Tutorials_files/part-4-fov.png" alt="Part 4 - FOV" title="Field of View"></p>
<p>Its hard to believe, but thats all we need to do for a functioning field of view!</p>
<p>This chapter was a shorter one, but weve accomplished quite a lot.
Our dungeon feels a lot more mysterious, and in coming chapters, it will
get a lot more dangerous.</p>
<p>If you want to see the code so far in its entirety, <a href="https://github.com/TStand90/tcod_tutorial_v2/tree/2020/part-4">click
here</a>.</p>
<p><a href="https://rogueliketutorials.com/tutorials/tcod/v2/part-5">Click here to move on to the next part of this
tutorial.</a></p>
</article>
</section>
</div>
<footer class="footer">
<section class="container">
©
2023
·
Powered by <a href="https://gohugo.io/">Hugo</a> &amp; <a href="https://github.com/luizdepra/hugo-coder/">Coder</a>.
</section>
</footer>
</main>
<script src="Part%204%20-%20Field%20of%20View%20%C2%B7%20Roguelike%20Tutorials_files/coder.min.236049395dc3682fb2719640872958e12f1f24067bb09c327b2.js" integrity="sha256-I2BJOV3DaC+ycZZAhylY4S8fJAZ7sJwyeyM+YpDH7aw="></script>
<script src="Part%204%20-%20Field%20of%20View%20%C2%B7%20Roguelike%20Tutorials_files/codetabs.min.cc52451e7f25e50f64c1c893826f606d58410d742c214dce.js" integrity="sha256-zFJFHn8l5Q9kwciTgm9gbVhBDXQsIU3OI/tEfJlh8rA="></script>
</body></html>