撰写了文章 发布于 2019-06-19 11:25:47
Roguelike大全,part4
视野和探索
视野(FOV)
下一步是实现视野。这是一个战术元素,它让玩家打开每扇门和经过每个转角时都要小心翼翼。FOV实现起来有点像一个点光源,它以玩家为中心向四面八方投射光线,但是光线会被墙挡住。阴影里的地区不可见,你可以自己编码试试看,思路是向每个方向投射一条射线,但是这里有更简单的方法。
libtcod有一整个库来实现这个功能。它包含了多钟方法,并且可以调节精度,速度和其他有趣的参数。想深入研究的可以细看英文链接里的内容,包含了表格和图片以对比不同算法的区别。我们这里定义了几个参数用以表征算法类型以及算法的一些参数,以便在后面进行更改
我们选择参数为0的默认算法,这里还可以选择要不要照亮墙壁,随你们的喜欢进行选择吧。另一个重要的参量是FOV计算的最大半径,用以定义你在地下城最远可以看多远。(这取决于角色的视力或者玩家手里的火把可以照多远)
FOV_ALGO = 0 #default FOV algorithmFOV_LIGHT_WALLS = TrueTORCH_RADIUS = 10
我们还需要更多的颜色以定义地块!它们用以表征地块被照亮后的颜色。颜色定义如下:
color_dark_wall = libtcod.Color(0, 0, 100) color_light_wall = libtcod.Color(130, 110, 50) color_dark_ground = libtcod.Color(50, 50, 150) color_light_ground = libtcod.Color(200, 180, 50)
这里的代码是直接从libtcod的范例里借鉴过来的,如果你希望你的游戏更加特别,参考之前提供的与颜色相关的文档。libtcod Fov模块需要知道哪些地块被光线照射到了,我们创建一个让libtcod可以理解的地图(fov_map),并用tiles的属性block_sight和blockedproperties来填充它。事实上,只有block_sight会被用到。blockedvalue与FOV没什么关联,他只在寻路中被使用,但是现在将它实现也没什么坏处。但是libtcod需要的变量跟我们定义的正好相反,(两个坐标体系,在gdi编程中经常遇到这种情况,例如a坐标系从坐上到右下,而b坐标系从右下到左上,这时候就需要将ab坐标系进行统一)所以我们要进行变换。这个转换在进入主循环之前进行。
fov_map = libtcod.map_new(MAP_WIDTH, MAP_HEIGHT)for y in range(MAP_HEIGHT): for x in range(MAP_WIDTH): libtcod.map_set_properties(fov_map, x, y, not map[x][y].block_sight, not map[x][y].blocked)
FOV就是在角色移动或者tile改变之后,才需要被重新计算。我们在主循环之前定义了一个全局变量fov_recompute = true。因此在handlekey函数,当玩家移动之后,就将这个值再次置为true,就像下面的源码这样。
#movement keys if libtcod.console_is_key_pressed(libtcod.KEY_UP): player.move(0, -1) fov_recompute = True elif libtcod.console_is_key_pressed(libtcod.KEY_DOWN): player.move(0, 1) fov_recompute = True elif libtcod.console_is_key_pressed(libtcod.KEY_LEFT): player.move(-1, 0) fov_recompute = True elif libtcod.console_is_key_pressed(libtcod.KEY_RIGHT): player.move(1, 0) fov_recompute = True
就像你看到的,我们使用了之前定义的参数。在那之后我们遍历所有的tiles并且在console上显示它们。我们之后还要再为每个tile增加一个参数
visible = libtcod.map_is_in_fov(fov_map, x, y)
靠着参数visible,tile将会被绘制成不同的颜色(点亮或黑暗)。我们将要将这部分函数模块化使得这一部分更加清晰。
#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 = libtcod.map_is_in_fov(fov_map, x, y) wall = map[x][y].block_sight if not visible: #it's out of the player's FOV if wall: libtcod.console_set_char_background(con, x, y, color_dark_wall, libtcod.BKGND_SET) else: libtcod.console_set_char_background(con, x, y, color_dark_ground, libtcod.BKGND_SET) else: #it's visible if wall: libtcod.console_set_char_background(con, x, y, color_light_wall, libtcod.BKGND_SET ) else: libtcod.console_set_char_background(con, x, y, color_light_ground, libtcod.BKGND_SET )
完成了!
最后一个细节是物体必须在玩家的FOV里才能被显示。在物体绘制的方法中,在绘制之前增加FOV检测函数:
if libtcod.map_is_in_fov(fov_map, self.x, self.y):
全部代码如链接所示。
The whole code for this section is here.
现在,在render_all函数里,在是否可见的代码后面加入如下代码
#if it's not visible right now, the player can only see it if it's explored if map[x][y].explored:
探索
FOV之后的另一个细节是探索,也就是所谓的战争谜雾。如果你细心的看到了这里,这个问题就是一个小case了。什么?你说对于对于一个roguelike游戏来说可不是一件容易的事情,是的,它确实不容易。但是请注意,每个tile包含了是否被探索到的参数。他们的初始状态是未被探索到,这在初始化函数中被定义。
self.explored = False
然后增加如下代码,使得它们将被运行如果值为TRUE,现在只有被探索到的tiles会被显示了。
接着,在渲染tile之后(就在函数的结尾),探索可视tile
map[x][y].explored = True
这就是本章的全部内容了,整个关卡一开始是全黑的,但是你将慢慢探索它。已探索区域将会可视,但是当它们不在视野范围内时,会以不一样的颜色显示,并且不显示其上的object(例如前夫的怪物)。现在这是一个探险游戏了。
完整代码如下
The whole code is available here.
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;
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 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 = 0 #default FOV algorithmFOV_LIGHT_WALLS = TrueTORCH_RADIUS = 10
Also, we'll need more colors for lit tiles! The color definitions will now be:
color_dark_wall = libtcod.Color(0, 0, 100) color_light_wall = libtcod.Color(130, 110, 50) color_dark_ground = libtcod.Color(50, 50, 150) color_light_ground = libtcod.Color(200, 180, 50)
These are taken straight away from the libtcod sample that comes with the library, and you may want to change them to give your game a more unique feel (see the earlier notes about colors).
The libtcod FOV module needs to know which tiles block sight. So, we create a map that libtcod can understand (fov_map), and fill it with the appropriate values from the tiles' own block_sight and blockedproperties. Well, actually, only block_sight will be used; the blockedvalue 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 not operator. This goes in the body of the script, before entering the main game loop.
fov_map = libtcod.map_new(MAP_WIDTH, MAP_HEIGHT)for y in range(MAP_HEIGHT): for x in range(MAP_WIDTH): libtcod.map_set_properties(fov_map, x, y, not map[x][y].block_sight, not map[x][y].blocked)
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 libtcod.console_is_key_pressed(libtcod.KEY_UP): player.move(0, -1) fov_recompute = True elif libtcod.console_is_key_pressed(libtcod.KEY_DOWN): player.move(0, 1) fov_recompute = True elif libtcod.console_is_key_pressed(libtcod.KEY_LEFT): player.move(-1, 0) fov_recompute = True elif libtcod.console_is_key_pressed(libtcod.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_allfunction. 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 libtcod.map_compute_fov(fov_map, player.x, player.y, TORCH_RADIUS, FOV_LIGHT_WALLS, FOV_ALGO)
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 = libtcod.map_is_in_fov(fov_map, x, y)
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 = libtcod.map_is_in_fov(fov_map, x, y) wall = map[x][y].block_sight if not visible: #it's out of the player's FOV if wall: libtcod.console_set_char_background(con, x, y, color_dark_wall, libtcod.BKGND_SET) else: libtcod.console_set_char_background(con, x, y, color_dark_ground, libtcod.BKGND_SET) else: #it's visible if wall: libtcod.console_set_char_background(con, x, y, color_light_wall, libtcod.BKGND_SET ) else: libtcod.console_set_char_background(con, x, y, color_light_ground, libtcod.BKGND_SET )
There, it's done!
The last detail is to make sure objects only show if they're in the player's FOV. In the Object 's draw method, add a FOV check before drawing:
if libtcod.map_is_in_fov(fov_map, self.x, self.y):
Apart from defining the newly used global values in render_all and handle_keys (they're fov_map 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 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:
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.