地下城

地下城的建筑材料

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

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

 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.

Roguelike大全,part 3相关推荐

  1. DOS命令大全 黑客必知的DOS命令集合

    一般来说dos命令都是在dos程序中进行的,如果电脑中安装有dos程序可以从开机选项中选择进入,在windows 系统中我们还可以从开始运行中输入cmd命令进入操作系统中的dos命令,如下图: 严格的 ...

  2. DIV+CSS规范命名大全集合

    网页制作中规范使用DIV+CSS命名规则,可以改善优化功效特别是团队合作时候可以提供合作制作效率,具体DIV CSS命名规则CSS命名大全内容篇. 常用DIV+CSS命名大全集合,即CSS命名规则 D ...

  3. web server大全之GoAhead移植(转载)

    转自:http://linux.chinaunix.net/techdoc/develop/2009/06/19/1119124.shtml 注:最近在做goAhead web server和移植其到 ...

  4. DELPHI 中 Window 消息大全使用详解

    Window 消息大全使用详解 导读: Delphi是Borland公司的一种面向对象的可视化软件开发工具. Delphi集中了Visual C++和Visual Basic两者的优点:容易上手.功能 ...

  5. 适合计算机应用的班群名称,班级同学群名字大全

    很多人现在都是一个班级建一个群,以便大家沟通交流,有什么事大家群里一说很方便,没事还可以吹吹牛B策策谈,那么同学班级群用什么样的名字好呢,在此起名网为大家收集整理了班级同学群名字大全.来看看吧. 最新 ...

  6. 狗年拜年php源码,2018狗年拜年词大全!再也不担心拜年没祝词啦~祝您新年快乐!...

    原标题:2018狗年拜年词大全!再也不担心拜年没祝词啦~祝您新年快乐! 2018 狗 年 大 吉 HAPPY NEW YEAR 为了您在春节期间能够在第一时间 为您的亲朋好友送上祝福~ 小编已经贴心的 ...

  7. robo3t 连接服务器数据库_车牌识别软件连接各种数据库方法大全

    软件连接各种数据库方法大全 1:软件连接免安装数据库. 免安装数据库使用方便,不受操作系统版本影响,不用安装,解压打开运行即可,所以免安装数据库不要放在桌面上,也不要解压打开多个. 打开车牌识别软件, ...

  8. 2020卫星参数表大全_王者荣耀比较秀的名字 2020年比较骚气比较浪的王者荣耀名字大全...

    游戏中该起什么样的名字,才能让其他玩家很快的记住,从而达到认识更多玩家,认识到更多的朋友,达到交友目的. 2020年比较骚气比较浪的王者荣耀男性玩家名字大全如下: 骚里骚气 闷里闷气 孤独患者 洁癖患 ...

  9. 2018年英语计算机职称考试,2018年职称计算机考试报考指南大全

    [摘要]环球网校分享的2018年职称计算机考试报考指南大全以下介绍了职称计算机考试简介.考试时间.报名时间等考试安排希望对大家有帮助,更多资料敬请关注环球职称计算机考试频道,网校会及时更新考试资讯 [ ...

最新文章

  1. asp.net中缓存Cache类的使用案例(附源码)
  2. spring.net 注入 配置
  3. python mulit函数_python – 将函数应用于MultiIndex pandas.DataFrame列
  4. 克服35岁焦虑|算法er的尽头会是To B吗?
  5. Displaying Bitmaps Efficiently (一)-----Loading Large Bitmaps Efficiently(二)
  6. chi660e电化学工作站软件_RuddlesdenPopper 型锰酸盐LaSr2Mn2O7的氧还原性能和作为电化学电容器电极材料的性能研究...
  7. (JAVA)FileWriter
  8. Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:2.7:resources
  9. jQuery学习: 实现select multiple左右添加和删除功能
  10. redis_lua_nginx环境配置过程
  11. 【不读唐诗,不足以知盛世】盛唐诗坛的璀璨明星们
  12. 如何短期通过PMP考试?
  13. 趣味证书制作生成微信小程序源码下载-安装简单方便
  14. antv的产品笼统记录
  15. 网站建设应该怎样建?建站流程说明
  16. Teamviwer操作(请激活复选框,证明是本人操作解决办法)
  17. STL ++iter与iter++区别
  18. 图形的逻辑思维题分类以及思路
  19. 什么是fail safe IO
  20. 怎么在alert里加图片_鹅蛋怎么挑选?教你2招,一看一摇听声音

热门文章

  1. 【矩阵求导】对于复向量l1-norm 1范数的求导
  2. 《web结课作业的源码》中华传统文化题材网页设计主题——基于HTML+CSS+JavaScript精美自适应绿色茶叶公司(12页)
  3. 安装包资源下载(暂整理)
  4. Android设置控件背景颜色
  5. android开发笔记之APK大小优化
  6. 利用VTP协议实现交换机 VLAN配置的一致性
  7. 负性情绪信息能否被我们所遗忘?
  8. 宽度优先搜索的复杂度分析
  9. linux 软件源 ppa,使用PPA源安装软件
  10. Testng框架简介