Roguelike大全,part 3

地下城

地下城的建筑材料

好了,现在到了开始搭建一个像样的地下城的时间了。柱子什么的,忘了它们吧!接下来我们将研究如何写一个函数,这个函数的作用是在地下的石头里挖地下城的房间,通道。这些函数将会是地下城的建筑材料用来在之后制造整个地下城

首先,实现一个辅助性的小类类,用以表征房间的大小

 class Rect:   
 #a rectangle on the map. used to characterize a room. def __init__(self, x, y, w, h):  
      self.x1 = x   
     self.y1 = y      
  self.x2 = x + w      
  self.y2 = y + h

这个类包含了房间的左上角坐标和宽高

为什么不直接保存房间的左上角和右下角两个点呢?因为这样遍历房间的所有tiles更加容易,因为Python的范围函数就是用这种方式取值的。这样定义房间后续用来判断两个房间的关系(例如判断是否重叠)也会比较容易



Python的range函数不包括列表的最后一个元素,举个例子for x in range(x1,x2)只会遍历x1到x2-1。因此我们要特意在x2后面+1

 def create_room(room):  
  global map #go through the tiles in the rectangle and make them passable for x in range(room.x1, room.x2 + 1):     
   for y in range(room.y1, room.y2 + 1):    
        map[x][y].blocked = False map[x][y].block_sight = False


实际上我们要给房间的边缘砌上墙,所以在每个方向我们都要留下一个空格,所以函数又被改变成下面这样

 def create_room(room):  
  global map #go through the tiles in the rectangle and make them passable for x in range(room.x1 + 1, room.x2):   
     for y in range(room.y1 + 1, room.y2):     
       map[x][y].blocked = False map[x][y].block_sight = False


还有一个微妙但是很重要的问题!如果两个房间紧挨着,但是却不重叠,则他们之间只有一面墙而不是两面。现在我们要修改make_map函数并用它在地图上挖出两个房子来

 def make_map(): 
   global map  #fill map with "blocked" 
tiles map = [[ Tile(True) for y in range(MAP_HEIGHT) ] for x in range(MAP_WIDTH) ]  #create two rooms 
   room1 = Rect(20, 15, 10, 15)  
  room2 = Rect(50, 15, 10, 15)  
  create_room(room1)   
 create_room(room2)
 

在测试之前,让玩家人物出现在第一个房间的正中央

    player.x = 25    player.y = 23

我们可以在第一个房间随意溜达,却无法进入第二个房间,我们将定义一个挖水平方向隧道的函数实现这个需求

def create_h_tunnel(x1, x2, y):  
  global map for x in range(min(x1, x2), max(x1, x2) + 1):   
     map[x][y].blocked = False map[x][y].block_sight = False

这里用到了min函数和max函数,它返回输入参数中的最小值或者最大值(比如min(1,5)会返回1)(这里跳过了一大段研究怎么使用min,max函数的废话,实在不懂的直接私信我得了不解释了)大意就是range(min(x1, x2), max(x1, x2) + 1):无视x1,x2孰大孰小的遍历了这两个数范围+1的tiles


纵向的挖房间函数和横向类似

def create_v_tunnel(y1, y2, x):  
  global map #vertical tunnel for y in range(min(y1, y2), max(y1, y2) + 1): 
       map[x][y].blocked = False map[x][y].block_sight = False

现在在make_map函数里,我们可以用横向隧道将房间相连了

create_h_tunnel(25, 55, 23)

有了这几个函数,你就可以通过组合生成迷宫了。但是下面我们将讲解最有趣(也是roguelike游戏最核心)的部分:随机地下城的生成!敬请期待吧


地下城生成器


女士们先生们,最后我们终于来到了最重要的地下城生成器,在这一节你终于将实现一些东西让roguelike游戏像一个真正的roguelike游戏了,这可是个不小的壮举呢!(唉,你吹,你继续吹)这里有很多手段来实现一个地下城生成器,每个人都有自己的方法,也许你看完这一节也会想出自己的优化方案


最基本的方法就是,为第一个房间随机设置一个位置并生成它,接着对第二个房间做同样的事情,设定其不允许与之前的房间重叠,连接两个房间,如此往复,便可以生成一堆连接在一起的房间


这里有一个很重要的问题就是玩家必须要能够到达所有的房间,这有一个后遗症就是通道和通道之间甚至通道和房间之间有很多的重叠,但是这的也没什么了不起的因为玩家根本就分不清楚通道的地面和房间地面的区别,这种重叠反而让房间显得更加的多样化因为玩家会误以为那是一个长条形的房间(玩家:喵喵喵???)

好了我我们可以看见rect类需要一个方法来检测房间交叉,从而杜绝房间的重叠,我们还提供一个类来返回房间的正中心这样通道就可以在正中心被连接

 def center(self):   
     center_x = (self.x1 + self.x2) / 2     
   center_y = (self.y1 + self.y2) / 2 return (center_x, center_y) 
 def intersect(self, other):        #returns true if this rectangle intersects with another one return
 (self.x1 <= other.x2 and self.x2 >= other.x1 and self.y1 <= other.y2 and self.y2 >= other.y1)


不用为交叉检测逻辑看起来很陌生而担心他就是一个简单的公式,而且不需要搞得特别清楚,你所要做的就是复制粘贴这个公式然后修改公司的两个参数最大房间数和最小房间数

 ROOM_MAX_SIZE = 10 ROOM_MIN_SIZE = 6 MAX_ROOMS = 30

现在我们已经定义了算法的主要部分,并且写出了写出了所有的辅助性的函数和类。现在是把它们运用到make_map里的时候了。将之前生成地图的代码去掉,设定一个循环迭代生成所有的房间,并向之前说的那样随机的给房间安排位置

 

rooms = []  
  num_rooms = 0  for r in range(MAX_ROOMS):        #random width and height  
      w = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE)    
    h = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE) #random position without going out of the boundaries of the map   
     x = libtcod.random_get_int(0, 0, MAP_WIDTH - w - 1)   
     y = libtcod.random_get_int(0, 0, MAP_HEIGHT - h - 1)


random_get_int函数返回一个随机的int值,这个值在给定的两个形参之间,第一个参数“0”定义了获得这个随机数的"stream",这个“stream”对于重新创建随机数很有用。(这里涉及到一个伪随机和真随机的概念,简单的说,第一个参数0定义了这次产生随机数的一个基准值,在c++里,通常会根据当前时间给一个值,这样生成的随机数看上去才更“随机”而不是可以预测的,或者反过来说,如果你给的第一个参数全都是0,你生成的随机数是可以被反推预测出来从而被人加以利用的,一个例子就是《信长之野望12》里被研究出来的跟时间相关的反复sl战法加武力的技巧,不过这就扯远了)在本例子里,伪随机也是可以接受的,所以直接给0就行了


Rect类在这里开始发挥作用了!房间列表将之前的房间保存成一堆Rect,我们可以把一个新的房间与之前的老房间相比较,并让它们不叠加在一起(其实我觉得叠加在一起也无所谓啊。。。可以生成各种奇怪形状的房间不好么,有兴趣的可以试试看不判断房间叠加会生成怎样的地下城)

 #"Rect" class makes rectangles easier to work with    
    new_room = Rect(x, y, w, h)  #run through the other rooms and see if they intersect with this one     
   failed = False for other_room in rooms:     
       if new_room.intersect(other_room):            
    failed = True break

如果一个房间是可行的(满足不与其他房间叠加之类的判断)我们可以用create_room来挖出这个房间!我们还可以设定玩家出现在第一个房间的正中间

 if not failed:     #this means there are no intersections, so this room is valid  #"paint" it to the map's tiles     
  create_room(new_room)  #center coordinates of new room, will be useful later 
(new_x, new_y) = new_room.center() 
 if num_rooms == 0:                #this is the first room, where the player starts at    
            player.x = new_x       
         player.y = new_y


通道是需要特别注意的(这个例子可够长的,不过请保持耐心吧!)有时候一个简单的竖通道或横通道并不能将两个房间连接,这时候就需要至少两根通道才能将其连接,这里提供的方案是如果一个房间的中心是x1,y1,另一个是x2,y2,则将一根通道伸到x1,y2的位置,再连接x1,y2和x2,y2,但是其实有很多其他的生成通道的方法(想想仙剑齐瞎转的地图)比如判断两根房间的最近点,再进行连接什么的,自己尝试一下吧


我们下面就来着手解决这些问题,你会发现,代码不仅包含了这些特殊的房间情况,还完美解决了紧挨在一起的房间的问题!在这种情况下,联通的管道将特别短,仔细阅读代码是如何实现的吧(这一段英文有点莫名其妙,还是直接看代码算了)

else:                #all rooms after the first: #connect it to the previous room with a tunnel  #center coordinates of previous room (prev_x, prev_y) = rooms[num_rooms-1].center()  #draw a coin (random number that is either 0 or 1) if libtcod.random_get_int(0, 0, 1) == 1:                    #first move horizontally, then vertically                    create_h_tunnel(prev_x, new_x, prev_y)                    create_v_tunnel(prev_y, new_y, new_x) else:                    #first move vertically, then horizontally                    create_v_tunnel(prev_y, new_y, prev_x)                    create_h_tunnel(prev_x, new_x, new_y)  #finally, append the new room to the list            rooms.append(new_room)            num_rooms += 1



好了,测试一下代码吧,你会惊讶的发现这简陋的代码生成的地下城其实还蛮不错呢!


最终的代码英文原文里找

The final code for the dungeon generator is here.


可选部分:构思地下城

如果地下城满足不了你的要求,你可以在生成地下城的任意房间时显示它的序号,你可以在房间中间显示一个数字来显示它是几号房间,由于房间数量一般超过10个,所以最好用字母+符号的方式(因为显示10以上的数要占用两个格子会比较麻烦了),并将这种显示放在 (new_x, new_y) = new_room.center()后面,这样你就可以生成62个房间了

这是个可选代码,所以没有出现在正式代码里









 

The Dungeon

Dungeon building blocks

Alright then, it's about time our dungeon takes a recognizable shape! I never cared much for pillars anyway. What we're gonna do now is create functions to carve rooms and tunnels in the underground rock. These functions will be the building blocks used to generate the whole dungeon which you'll get to later.

 

First of all, a little helper class that will be very handy when dealing with rectangular rooms:

class Rect: #a rectangle on the map. used to characterize a room. def __init__(self, x, y, w, h): self.x1 = x self.y1 = y self.x2 = x + w self.y2 = y + h


This will take top-left coordinates for a rectangle (in tiles, of course), and its size, to define it in terms of two points: top-left (x1, y1) and bottom-right (x2, y2).

Why not store directly its top-left coordinates and size? Well, it's much easier to loop through the room's tiles this way, since Python's range function takes arguments in this form. It will also be apparent later that the code for intersecting rectangles (for example, to be sure rooms don't overlap) is easier to define this way.

Python's range function, however, excludes the last element in the loop. For example, for x in range(x1, x2) will only loop until x2 - 1. So the code to fill a room with unblocked tiles could be:

def create_room(room): global map #go through the tiles in the rectangle and make them passable for x in range(room.x1, room.x2 + 1): for y in range(room.y1, room.y2 + 1): map[x][y].blocked = False map[x][y].block_sight = False


The + 1 at the end takes care of the issue we mentioned. But we actually want to leave some walls at the border of the room, so we'll leave out one tile in all directions.

def create_room(room): global map #go through the tiles in the rectangle and make them passable for x in range(room.x1 + 1, room.x2): for y in range(room.y1 + 1, room.y2): map[x][y].blocked = False map[x][y].block_sight = False


Subtle, but important! This way, even if two rooms are right next to each other (but not overlapping), there's always one wall separating them. Now we're going to modify the make_map function to start out with all tiles blocked, and carve two rooms in the map with our new function.

def make_map(): global map
#fill map with "blocked" tiles map = [[ Tile(True) for y in range(MAP_HEIGHT) ] for x in range(MAP_WIDTH) ]
#create two rooms room1 = Rect(20, 15, 10, 15) room2 = Rect(50, 15, 10, 15) create_room(room1) create_room(room2)


Before testing out, make the player appear in the center of the first room:

player.x = 25 player.y = 23


You can walk around the first room, but not reach the second. We'll define a function to carve a horizontal tunnel:

def create_h_tunnel(x1, x2, y): global map for x in range(min(x1, x2), max(x1, x2) + 1): map[x][y].blocked = False map[x][y].block_sight = False


There's some creative use of the min and max functions there, so we'll explain that a bit. Feel free to skip this if you got it. They'll return the minimum or maximum of both arguments, but why are they needed? Well, if x1 < x2, x1 will be the minimum of both, and x2 the maximum. So that line will be the same as:

for x in range(x1, x2 + 1):


If they're reversed, the same line wouldn't work -- as it is, for only loops from a small number to a bigger number. But returning to the original line, then x2 will be the minimum, and x1 maximum. You guessed it -- it will be the same as:

 

for x in range(x2, x1 + 1):


It could be solved with other logic: swapping their values, changing the for 's step to negative, having an if to choose between the two lines... The functions min and max tend to give the shortest code, though it may be harder to understand if you're not used to them much.

The function to carve vertical functions is pretty similar.

def create_v_tunnel(y1, y2, x): global map #vertical tunnel for y in range(min(y1, y2), max(y1, y2) + 1): map[x][y].blocked = False map[x][y].block_sight = False


Now, in make_map, we can connect both rooms with a horizontal tunnel.

create_h_tunnel(25, 55, 23)


That concludes the test for the carving functions! It's really starting to look better by the minute. You can create different dungeon configurations with them, but it's tricky defining the coordinates by hand and we're going to rush right away to the most interesting bit: random dungeon generation!

You can find the whole code for this part here.

Dungeon generator

Finally, the dungeon generator. After this point you'll have accomplished something that is truly a turning point in the development of a roguelike, and that is no small feat! There are many different approaches to coding a dungeon generator, and there is no consensus; everyone has their own interpretation of the right way to do this. Perhaps after being content with this one for a while you'll think of different ways to improve it.

The basic idea is this. Pick a random location for the first room, and carve it. Then pick another location for the second; it cannot overlap with the first. Connect the two with a tunnel, and repeat. This will yield a sequence of connected rooms.

One important property is that the player is guaranteed to be able to reach every room. The downside is that there will be lots of overlaps between the tunnels, and even tunnels overlapping rooms, but that's not too bad since the player can't distinguish between tunnel floor and room floor. The overlaps actually give it a more complicated look than if it were just a simple string of rooms!

Alright, we can see that the Rect class needs a method to check for intersections, so that no two rooms can overlap. We'll also create a method to return the center coordinates of the room since that's where all tunnels will be connected.

def center(self): center_x = (self.x1 + self.x2) / 2 center_y = (self.y1 + self.y2) / 2 return (center_x, center_y)
def intersect(self, other): #returns true if this rectangle intersects with another one return (self.x1 <= other.x2 and self.x2 >= other.x1 and self.y1 <= other.y2 and self.y2 >= other.y1)


Don't worry if the intersection logic seems alien, it's a direct formula that doesn't require interpretation! It's one of those copy-and-paste things. The constants for the dungeon generation are simply the room's minimum and maximum size, and the maximum number of rooms.

ROOM_MAX_SIZE = 10
ROOM_MIN_SIZE = 6
MAX_ROOMS = 30


Ok, we've described the gist of the algorithm, took care of all helper functions and classes... Now to implement it in make_map. Remove the previous code that creates the example rooms and tunnel, and make a loop to iterate until the maximum number of rooms, assigning random coordinates and size to each one as we go.

rooms = [] num_rooms = 0
for r in range(MAX_ROOMS): #random width and height w = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE) h = libtcod.random_get_int(0, ROOM_MIN_SIZE, ROOM_MAX_SIZE) #random position without going out of the boundaries of the map x = libtcod.random_get_int(0, 0, MAP_WIDTH - w - 1) y = libtcod.random_get_int(0, 0, MAP_HEIGHT - h - 1)


The random_get_int function returns a random integer between two numbers; the first parameter, zero, identifies the "stream" to get that number from. Random number streams are useful if you want to recreate sequences of random numbers; for our purposes it's not needed, so we use the default stream.

The Rect class will really start to come in handy at this point! The rooms list stores all previous rooms as Rect 's. That way we can compare the new room with previous ones for intersection, and reject it if it overlaps.

#"Rect" class makes rectangles easier to work with new_room = Rect(x, y, w, h)
#run through the other rooms and see if they intersect with this one failed = False for other_room in rooms: if new_room.intersect(other_room): failed = True break


If the room is valid, we can carve it with create_room! We'll also handle a special case: the player will start in the center of the first room.

 

if not failed: #this means there are no intersections, so this room is valid
#"paint" it to the map's tiles create_room(new_room)
#center coordinates of new room, will be useful later (new_x, new_y) = new_room.center()
if num_rooms == 0: #this is the first room, where the player starts at player.x = new_x player.y = new_y


The tunnels deserve some attention. (This is a long example, but please be patient!) Sometimes a strictly horizontal or vertical tunnel can't connect two rooms. Imagine one is on one corner of the map (e.g., top-left), and the other in the opposing corner (e.g., bottom-right.) You'd need two tunnels going from the first room: one horizontal tunnel to reach the right side of the map, and one vertical tunnel to go from there to the bottom of the map (reaching the second room). Or the other way around: one vertical tunnel to reach the bottom of the map, and another horizontal one to reach the right side (reaching the second room.) Both options are valid, so we choose between them randomly.

So we're going to code that, and you'll see that this not only covers extreme cases like the one in the example, but also works perfectly when two rooms are side-by-side! Since in that case one of the tunnel segments (horizontal or vertical) will be very small, and only the other segment will be visible. It's a bit hard to visualize though, you'll have to see it in action.

else: #all rooms after the first: #connect it to the previous room with a tunnel
#center coordinates of previous room (prev_x, prev_y) = rooms[num_rooms-1].center()
#draw a coin (random number that is either 0 or 1) if libtcod.random_get_int(0, 0, 1) == 1: #first move horizontally, then vertically create_h_tunnel(prev_x, new_x, prev_y) create_v_tunnel(prev_y, new_y, new_x) else: #first move vertically, then horizontally create_v_tunnel(prev_y, new_y, prev_x) create_h_tunnel(prev_x, new_x, new_y)
#finally, append the new room to the list rooms.append(new_room) num_rooms += 1


You made it through all that in one piece, eh? Let's test it out! With some luck, some of the dungeons you get can be pretty amazing. Not too shabby for our own little algorithm!

The final code for the dungeon generator is here.

 

Optional part: Visualizing the dungeon

If you're looking at the maps this program is creating and trying to figure out just how the code works, or you've tried playing around with the code to create a dungeon differently but it isn't doing quite what you expected, you can add a label to each room to show the order in which they were created. The game works with single characters, and we will often have more than 10 rooms, so a number doesn't work. We'll put a letter in the middle of each room instead. Add these lines after (new_x, new_y) = new_room.center().

 

#optional: print "room number" to see how the map drawing worked # we may have more than ten rooms, so print 'A' for the first room, 'B' for the next... room_no = Object(new_x, new_y, chr(65+num_rooms), libtcod.white) objects.insert(0, room_no) #draw early, so monsters are drawn on top


This allows for 26 rooms. MAX_ROOMS is set to 30, but it would be extremely unlikely that all 30 would be created without any overlaps. However, if you tweak some of the code and do end up with more than 30 rooms, this will print various symbols such as '[' and then lower case letters. It won't fail unless you have more than 62 rooms.

Because this is optional code, it isn't included in the code samples linked from each part of the tutorial.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值