http://indienova.com/indie-game-development/roguelike-dungeon-building-algorithm/
Dungeon Algorithm
文章作者:Mike Anderson
随机生成的地图是 Roguelike 类游戏最独特的一点,它让游戏变得很有乐趣,因为玩家永远要面对新的挑战。
但是随机地图却不是那么容易生成的。在传统的游戏中,一般你都会有一个地图编辑器,可以自由的创建地图。在任何一款称得上是“Roguelike”的游戏中,开发者都要自己创造一个“虚拟地图编辑器”,这样才能随机创建无限的动态地图,从而让玩家在其中流连忘返。
在这篇文章里,我会将自己在开发一款名为 Tyrant 的 Roguelike 游戏中使用的方法记录下来。我怀疑这可能只能算是一个原型,但是我之前也没有见过什么一本正经讲述生成 Roguelike 地图算法的文章。而且,它工作得还是比较令人满意的,所以,我愿意将它分享给大家。
这款算法的目标
在写任何代码之前,了解自己的目标总是很重要的,这对编程很有帮助,哪怕你随后会做无数的修改。
一个地牢(Dungeon)应该包含以下要点:
一组相互连通的房间、门和通道
一个入口(向上走的楼梯)
一个出口(向下走的楼梯)
所有的空间必须能够到达
最后一点尤其重要。要知道,你的玩家在契而不舍的努力之后,应该能够顺利通过这一层,不要让他们失望。另外,如果放了某个物品到地图上的某个空间,它应该不会被藏在无法到达的地方。
计划
在我写 Tyrant 的时候,我尝试了很多种不同的算法来生成地图,这里所讲的是我能做到的最好的一个,也是目前游戏中使用的那个。
我的灵感来自于此:“如果我是地下城的一个居民,那么我该怎么去建设我的地牢呢?”
显然,我并不会将我的地下城建造成一个一个看起来不错的小房间,然后在中间用长长的通道连接起来。所以,当我需要为我的小怪物们提供更多空间的时候,我应该是拿起我的斧头,挖一个更大一些的洞。这样当他们有所需要的时候就会增加一些新房间——尽管它们看起来可能杂乱无章。
有些地下城主可能想要用吊桥呀、陷阱呀什么的来守护比较“有趣”的房间,但是这些需求都异曲同工。由一个小的地牢开始,慢慢向四周扩散,直到整个地牢形成。这就是我们的计划。
算法
在这个算法里面,“元素”代表着某种地图元素,比如:大房间、小房间、通道、圆形竞技场、保险柜等等。
将整个地图填满土
在地图中间挖一个房间出来
选中某一房间(如果有多个的话)的墙壁
确定要修建某种新元素
查看从选中的墙延伸出去是否有足够的空间承载新的元素
如果有的话继续,不然就返回第 3 步
从选中的墙处增加新的元素
返回第 3 步,直到地牢建设完成
在地图的随机点上安排上楼和下楼的楼梯
最后,放进去怪兽和物品
第 1、2 步很简单。只要你创建好地图就可以去做到。我发现,写一个 fillRect 指令用来填充一个区域是比较有效的做法。
第 3 步麻烦一些。你不能随意的寻找一个方块区域去添加你的元素,因为规则是要将元素添加到当前的地牢当中。这样会使得连接看起来比较不错,也确保了所有的区域都可以到达。Tyrant 的做法是:在地图上随机选择一个方块,直到找到横向或者纵向毗邻一个干净的方块那个。这样做的好处是:它给了你一个近乎公平的方式去选择某一面墙。
第 4 步不太困难。我自己写了一个随机方法来决定建造哪一种元素。你可以自己定义它们,调整某些元素出现的权重,这会让你的地牢有自己的特点和侧重点。一个规划比较好的地牢会有很多规矩的房间,中间有长而且直的走廊连接。而洞穴则可能有一堆打洞以及曲折的小道等等。
第 5 步更复杂一些,而且也是整个算法的核心。针对每一种元素,你需要知道它会占用的空间大小。然后你要去判断它是否和已经有的元素相交。Tyrant 使用了相对简单的一种方法:它会先得到要创建的元素所占用的空间大小,得到这个空间的数据,然后检查是否这个空间由土填满。
第 6 步决定是否创建这个元素。如果这个待确定的空间包含有除了土之外的内容,那么就回到第 3 步继续。注意,大部分元素在这步都会被打回。不过这不是个问题,因为处理时间可以忽略。Tyrant 尝试着将某个元素加入 300 次左右到地牢中去,一般只有 40 次左右会通过这步。
第 7 步会将新元素添加到地图上去。在这步,你还可以增加一些有趣的元素,比如动物、居民、秘道门和财宝什么的。
第 8 步返回去创建更多的房间。确切的次数跟你地牢的尺寸以及其它参数有关。
第 9 步要看个人喜好了。最简单的方法就是随机的去查找方块,直到找到一个空的位置去放置楼梯。
第 10 步就是随机的创建怪兽。Tyrant 在这一步才加入游戏中大多数的怪兽,由少量的特殊怪兽或者生物会在生成房间的时候添加进去。
就这样啦,这里所说的只是算法的规则,具体还要您自己去实现啦。
例子
好了,在看了半天算法之后,我们来一个例子吧:
Key:
# = 地板
D = 门
W = 正在考查中的墙
C = 宝箱
1. 第一个房间
#####
#####
#####
2. 随机选择一面墙
#####
#####W
#####
3. 为新的通道元素进行区域搜索(包括两边的空间)
#####**********
#####W*********
#####**********
4. 是空的,可以添加元素
#####
#####D########
#####
5. 选择另外一面墙
##### W
#####D########
#####
6. 扫描寻找新的房间所占用空间:
******
******
******
******
******
##### ***W**
#####D########
#####
7. 这个地区也可以,那就添加一个新房间,再往里面扔一个宝箱 C(Chest):
####
###C
####
####
##### D
#####D########
#####
8. 跟前面做法一样,我们增加一个新的通道元素
#
#
#### #
###C #
#### #
#### #
##### D #
#####D########
#####
9. 这一次,我们试着为第二个房间增加一个通道元素
#
#
#### #
###C*******
####W******
####*******
##### D #
#####D########
#####
10. 扫描失败了,已经被占用
#
#
#### #
###C #
#### #
#### #
##### D #
#####D########
#####
11. 比较特别的元素,一个菱形的房间
#
# ###
#### # #####
###C # #######
#### #D#######
#### # #######
##### D # #####
#####D######## ###
#####
12. 添加一个隐藏的暗门,以及充满陷阱的通道:
#
# ###
#### # #####
###C # #######S###T##TT#T##
#### #D#######
#### # #######
##### D # #####
#####D######## ###
#####
13. 继续……
总结
好了,这就是我的算法,我希望它对你有用,或者从一个有趣的角度去看如何解决一个问题。
代码实现
Java 代码实现
Java 代码实现
你可以通过 Open Processing 在浏览器里面运行它(需要做一些小修改)。它会创建一个图形化的地牢。
Python Curses 代码实现
Python Curses 代码实现
C++ 代码实现
C++ 代码实现
C# 代码实现
C# 代码实现
原文地址:链接
Dungeon Algorithm
文章作者:Mike Anderson
随机生成的地图是 Roguelike 类游戏最独特的一点,它让游戏变得很有乐趣,因为玩家永远要面对新的挑战。
但是随机地图却不是那么容易生成的。在传统的游戏中,一般你都会有一个地图编辑器,可以自由的创建地图。在任何一款称得上是“Roguelike”的游戏中,开发者都要自己创造一个“虚拟地图编辑器”,这样才能随机创建无限的动态地图,从而让玩家在其中流连忘返。
在这篇文章里,我会将自己在开发一款名为 Tyrant 的 Roguelike 游戏中使用的方法记录下来。我怀疑这可能只能算是一个原型,但是我之前也没有见过什么一本正经讲述生成 Roguelike 地图算法的文章。而且,它工作得还是比较令人满意的,所以,我愿意将它分享给大家。
这款算法的目标
在写任何代码之前,了解自己的目标总是很重要的,这对编程很有帮助,哪怕你随后会做无数的修改。
一个地牢(Dungeon)应该包含以下要点:
一组相互连通的房间、门和通道
一个入口(向上走的楼梯)
一个出口(向下走的楼梯)
所有的空间必须能够到达
最后一点尤其重要。要知道,你的玩家在契而不舍的努力之后,应该能够顺利通过这一层,不要让他们失望。另外,如果放了某个物品到地图上的某个空间,它应该不会被藏在无法到达的地方。
计划
在我写 Tyrant 的时候,我尝试了很多种不同的算法来生成地图,这里所讲的是我能做到的最好的一个,也是目前游戏中使用的那个。
我的灵感来自于此:“如果我是地下城的一个居民,那么我该怎么去建设我的地牢呢?”
显然,我并不会将我的地下城建造成一个一个看起来不错的小房间,然后在中间用长长的通道连接起来。所以,当我需要为我的小怪物们提供更多空间的时候,我应该是拿起我的斧头,挖一个更大一些的洞。这样当他们有所需要的时候就会增加一些新房间——尽管它们看起来可能杂乱无章。
有些地下城主可能想要用吊桥呀、陷阱呀什么的来守护比较“有趣”的房间,但是这些需求都异曲同工。由一个小的地牢开始,慢慢向四周扩散,直到整个地牢形成。这就是我们的计划。
算法
在这个算法里面,“元素”代表着某种地图元素,比如:大房间、小房间、通道、圆形竞技场、保险柜等等。
将整个地图填满土
在地图中间挖一个房间出来
选中某一房间(如果有多个的话)的墙壁
确定要修建某种新元素
查看从选中的墙延伸出去是否有足够的空间承载新的元素
如果有的话继续,不然就返回第 3 步
从选中的墙处增加新的元素
返回第 3 步,直到地牢建设完成
在地图的随机点上安排上楼和下楼的楼梯
最后,放进去怪兽和物品
第 1、2 步很简单。只要你创建好地图就可以去做到。我发现,写一个 fillRect 指令用来填充一个区域是比较有效的做法。
第 3 步麻烦一些。你不能随意的寻找一个方块区域去添加你的元素,因为规则是要将元素添加到当前的地牢当中。这样会使得连接看起来比较不错,也确保了所有的区域都可以到达。Tyrant 的做法是:在地图上随机选择一个方块,直到找到横向或者纵向毗邻一个干净的方块那个。这样做的好处是:它给了你一个近乎公平的方式去选择某一面墙。
第 4 步不太困难。我自己写了一个随机方法来决定建造哪一种元素。你可以自己定义它们,调整某些元素出现的权重,这会让你的地牢有自己的特点和侧重点。一个规划比较好的地牢会有很多规矩的房间,中间有长而且直的走廊连接。而洞穴则可能有一堆打洞以及曲折的小道等等。
第 5 步更复杂一些,而且也是整个算法的核心。针对每一种元素,你需要知道它会占用的空间大小。然后你要去判断它是否和已经有的元素相交。Tyrant 使用了相对简单的一种方法:它会先得到要创建的元素所占用的空间大小,得到这个空间的数据,然后检查是否这个空间由土填满。
第 6 步决定是否创建这个元素。如果这个待确定的空间包含有除了土之外的内容,那么就回到第 3 步继续。注意,大部分元素在这步都会被打回。不过这不是个问题,因为处理时间可以忽略。Tyrant 尝试着将某个元素加入 300 次左右到地牢中去,一般只有 40 次左右会通过这步。
第 7 步会将新元素添加到地图上去。在这步,你还可以增加一些有趣的元素,比如动物、居民、秘道门和财宝什么的。
第 8 步返回去创建更多的房间。确切的次数跟你地牢的尺寸以及其它参数有关。
第 9 步要看个人喜好了。最简单的方法就是随机的去查找方块,直到找到一个空的位置去放置楼梯。
第 10 步就是随机的创建怪兽。Tyrant 在这一步才加入游戏中大多数的怪兽,由少量的特殊怪兽或者生物会在生成房间的时候添加进去。
就这样啦,这里所说的只是算法的规则,具体还要您自己去实现啦。
例子
好了,在看了半天算法之后,我们来一个例子吧:
Key:
# = 地板
D = 门
W = 正在考查中的墙
C = 宝箱
1. 第一个房间
#####
#####
#####
2. 随机选择一面墙
#####
#####W
#####
3. 为新的通道元素进行区域搜索(包括两边的空间)
#####**********
#####W*********
#####**********
4. 是空的,可以添加元素
#####
#####D########
#####
5. 选择另外一面墙
##### W
#####D########
#####
6. 扫描寻找新的房间所占用空间:
******
******
******
******
******
##### ***W**
#####D########
#####
7. 这个地区也可以,那就添加一个新房间,再往里面扔一个宝箱 C(Chest):
####
###C
####
####
##### D
#####D########
#####
8. 跟前面做法一样,我们增加一个新的通道元素
#
#
#### #
###C #
#### #
#### #
##### D #
#####D########
#####
9. 这一次,我们试着为第二个房间增加一个通道元素
#
#
#### #
###C*******
####W******
####*******
##### D #
#####D########
#####
10. 扫描失败了,已经被占用
#
#
#### #
###C #
#### #
#### #
##### D #
#####D########
#####
11. 比较特别的元素,一个菱形的房间
#
# ###
#### # #####
###C # #######
#### #D#######
#### # #######
##### D # #####
#####D######## ###
#####
12. 添加一个隐藏的暗门,以及充满陷阱的通道:
#
# ###
#### # #####
###C # #######S###T##TT#T##
#### #D#######
#### # #######
##### D # #####
#####D######## ###
#####
13. 继续……
总结
好了,这就是我的算法,我希望它对你有用,或者从一个有趣的角度去看如何解决一个问题。
代码实现
Java 代码实现
Java 代码实现
你可以通过 Open Processing 在浏览器里面运行它(需要做一些小修改)。它会创建一个图形化的地牢。
Python Curses 代码实现
Python Curses 代码实现
C++ 代码实现
C++ 代码实现
C# 代码实现
C# 代码实现
原文地址:链接
Share the post “一种 Roguelike 地牢生成算法”
===============
C++ Example of Dungeon-Building Algorithm
Contents[hide] |
Version 1
(Translated to C++ by MindControlDx)
#include <iostream> #include <string> #include <cstdlib> #include <cstdio> #include <ctime> class Dungeon { int xmax; int ymax; int xsize; int ysize; int objects; int chanceRoom; int chanceCorridor; int* dungeon_map; long oldseed; enum { tileUnused = 0, tileDirtWall, tileDirtFloor, tileStoneWall, tileCorridor, tileDoor, tileUpStairs, tileDownStairs, tileChest }; std::string msgXSize; std::string msgYSize; std::string msgMaxObjects; std::string msgNumObjects; std::string msgHelp; std::string msgDetailedHelp; void setCell(int x, int y, int celltype) { dungeon_map[x + xsize * y] = celltype; } int getCell(int x, int y) { return dungeon_map[x + xsize * y]; } int getRand(int min, int max) { time_t seed; seed = time(NULL) + oldseed; oldseed = seed; srand(seed); int n = max - min + 1; int i = rand() % n; if(i < 0) i = -i; return min + i; } bool makeCorridor(int x, int y, int lenght, int direction) { int len = getRand(2, lenght); int floor = tileCorridor; int dir = 0; if(direction > 0 && direction < 4) dir = direction; int xtemp = 0; int ytemp = 0; switch(dir) { case 0: { if(x < 0 || x > xsize) return false; else xtemp = x; for(ytemp = y; ytemp > (y-len); ytemp--) { if(ytemp < 0 || ytemp > ysize) return false; if(getCell(xtemp, ytemp) != tileUnused) return false; } for(ytemp = y; ytemp > (y - len); ytemp--) { setCell(xtemp, ytemp, floor); } break; } case 1: { if(y < 0 || y > ysize) return false; else ytemp = y; for(xtemp = x; xtemp < (x + len); xtemp++) { if(xtemp < 0 || xtemp > xsize) return false; if(getCell(xtemp, ytemp) != tileUnused) return false; } for(xtemp = x; xtemp < (x + len); xtemp++) { setCell(xtemp, ytemp, floor); } break; } case 2: { if(x < 0 || x > xsize) return false; else xtemp = x; for(ytemp = y; ytemp < (y + len); ytemp++) { if(ytemp < 0 || ytemp > ysize) return false; if(getCell(xtemp, ytemp) != tileUnused) return false; } for (ytemp = y; ytemp < (y+len); ytemp++){ setCell(xtemp, ytemp, floor); } break; } case 3: { if (ytemp < 0 || ytemp > ysize) return false; else ytemp = y; for (xtemp = x; xtemp > (x-len); xtemp--){ if (xtemp < 0 || xtemp > xsize) return false; if (getCell(xtemp, ytemp) != tileUnused) return false; } for (xtemp = x; xtemp > (x-len); xtemp--){ setCell(xtemp, ytemp, floor); } break; } } //woot, we're still here! let's tell the other guys we're done!! return true; } bool makeRoom(int x, int y, int xlength, int ylength, int direction){ //define the dimensions of the room, it should be at least 4x4 tiles (2x2 for walking on, the rest is walls) int xlen = getRand(4, xlength); int ylen = getRand(4, ylength); //the tile type it's going to be filled with int floor = tileDirtFloor; //jordgolv.. int wall = tileDirtWall; //jordv????gg //choose the way it's pointing at int dir = 0; if (direction > 0 && direction < 4) dir = direction; switch(dir){ case 0: //north //Check if there's enough space left for it for (int ytemp = y; ytemp > (y-ylen); ytemp--){ if (ytemp < 0 || ytemp > ysize) return false; for (int xtemp = (x-xlen/2); xtemp < (x+(xlen+1)/2); xtemp++){ if (xtemp < 0 || xtemp > xsize) return false; if (getCell(xtemp, ytemp) != tileUnused) return false; //no space left... } } //we're still here, build for (int ytemp = y; ytemp > (y-ylen); ytemp--){ for (int xtemp = (x-xlen/2); xtemp < (x+(xlen+1)/2); xtemp++){ //start with the walls if (xtemp == (x-xlen/2)) setCell(xtemp, ytemp, wall); else if (xtemp == (x+(xlen-1)/2)) setCell(xtemp, ytemp, wall); else if (ytemp == y) setCell(xtemp, ytemp, wall); else if (ytemp == (y-ylen+1)) setCell(xtemp, ytemp, wall); //and then fill with the floor else setCell(xtemp, ytemp, floor); } } break; case 1: //east for (int ytemp = (y-ylen/2); ytemp < (y+(ylen+1)/2); ytemp++){ if (ytemp < 0 || ytemp > ysize) return false; for (int xtemp = x; xtemp < (x+xlen); xtemp++){ if (xtemp < 0 || xtemp > xsize) return false; if (getCell(xtemp, ytemp) != tileUnused) return false; } } for (int ytemp = (y-ylen/2); ytemp < (y+(ylen+1)/2); ytemp++){ for (int xtemp = x; xtemp < (x+xlen); xtemp++){ if (xtemp == x) setCell(xtemp, ytemp, wall); else if (xtemp == (x+xlen-1)) setCell(xtemp, ytemp, wall); else if (ytemp == (y-ylen/2)) setCell(xtemp, ytemp, wall); else if (ytemp == (y+(ylen-1)/2)) setCell(xtemp, ytemp, wall); else setCell(xtemp, ytemp, floor); } } break; case 2: //south for (int ytemp = y; ytemp < (y+ylen); ytemp++){ if (ytemp < 0 || ytemp > ysize) return false; for (int xtemp = (x-xlen/2); xtemp < (x+(xlen+1)/2); xtemp++){ if (xtemp < 0 || xtemp > xsize) return false; if (getCell(xtemp, ytemp) != tileUnused) return false; } } for (int ytemp = y; ytemp < (y+ylen); ytemp++){ for (int xtemp = (x-xlen/2); xtemp < (x+(xlen+1)/2); xtemp++){ if (xtemp == (x-xlen/2)) setCell(xtemp, ytemp, wall); else if (xtemp == (x+(xlen-1)/2)) setCell(xtemp, ytemp, wall); else if (ytemp == y) setCell(xtemp, ytemp, wall); else if (ytemp == (y+ylen-1)) setCell(xtemp, ytemp, wall); else setCell(xtemp, ytemp, floor); } } break; case 3: //west for (int ytemp = (y-ylen/2); ytemp < (y+(ylen+1)/2); ytemp++){ if (ytemp < 0 || ytemp > ysize) return false; for (int xtemp = x; xtemp > (x-xlen); xtemp--){ if (xtemp < 0 || xtemp > xsize) return false; if (getCell(xtemp, ytemp) != tileUnused) return false; } } for (int ytemp = (y-ylen/2); ytemp < (y+(ylen+1)/2); ytemp++){ for (int xtemp = x; xtemp > (x-xlen); xtemp--){ if (xtemp == x) setCell(xtemp, ytemp, wall); else if (xtemp == (x-xlen+1)) setCell(xtemp, ytemp, wall); else if (ytemp == (y-ylen/2)) setCell(xtemp, ytemp, wall); else if (ytemp == (y+(ylen-1)/2)) setCell(xtemp, ytemp, wall); else setCell(xtemp, ytemp, floor); } } break; } //yay, all done return true; } void showDungeon(){ for (int y = 0; y < ysize; y++){ for (int x = 0; x < xsize; x++){ //System.out.print(getCell(x, y)); switch(getCell(x, y)){ case tileUnused: printf(" "); break; case tileDirtWall: printf("#"); break; case tileDirtFloor: printf("."); break; case tileStoneWall: printf("X"); break; case tileCorridor: printf("."); break; case tileDoor: printf("+"); break; case tileUpStairs: printf("<"); break; case tileDownStairs: printf(">"); break; case tileChest: printf("*"); break; }; } //if (xsize <= xmax) printf("\n"); } } bool createDungeon(int inx, int iny, int inobj){ if (inobj < 1) objects = 10; else objects = inobj; //justera kartans storlek, om den ????r st????rre eller mindre ????n "gr????nserna" //adjust the size of the map, if it's smaller or bigger than the limits if (inx < 3) xsize = 3; else if (inx > xmax) xsize = xmax; else xsize = inx; if (iny < 3) ysize = 3; else if (iny > ymax) ysize = ymax; else ysize = iny; //printf("%s %d\n", msgXSize.c_str(), xsize); //printf("%s %d\n", msgYSize.c_str(), + ysize); //printf("%s %d\n", msgMaxObjects.c_str(), objects); //redefine the map var, so it's adjusted to our new map size dungeon_map = new int[xsize * ysize]; //start with making the "standard stuff" on the map for (int y = 0; y < ysize; y++){ for (int x = 0; x < xsize; x++){ //ie, making the borders of unwalkable walls if (y == 0) setCell(x, y, tileStoneWall); else if (y == ysize-1) setCell(x, y, tileStoneWall); else if (x == 0) setCell(x, y, tileStoneWall); else if (x == xsize-1) setCell(x, y, tileStoneWall); //and fill the rest with dirt else setCell(x, y, tileUnused); } } /******************************************************************************* And now the code of the random-map-generation-algorithm begins! *******************************************************************************/ //start with making a room in the middle, which we can start building upon makeRoom(xsize/2, ysize/2, 8, 6, getRand(0,3)); //getrand saken f????r att slumpa fram riktning p?? rummet //keep count of the number of "objects" we've made int currentFeatures = 1; //+1 for the first room we just made //then we sart the main loop for (int countingTries = 0; countingTries < 1000; countingTries++){ //check if we've reached our quota if (currentFeatures == objects){ break; } //start with a random wall int newx = 0; int xmod = 0; int newy = 0; int ymod = 0; int validTile = -1; //1000 chances to find a suitable object (room or corridor).. //(yea, i know it's kinda ugly with a for-loop... -_-') for (int testing = 0; testing < 1000; testing++){ newx = getRand(1, xsize-1); newy = getRand(1, ysize-1); validTile = -1; //System.out.println("tempx: " + newx + "\ttempy: " + newy); if (getCell(newx, newy) == tileDirtWall || getCell(newx, newy) == tileCorridor){ //check if we can reach the place if (getCell(newx, newy+1) == tileDirtFloor || getCell(newx, newy+1) == tileCorridor){ validTile = 0; // xmod = 0; ymod = -1; } else if (getCell(newx-1, newy) == tileDirtFloor || getCell(newx-1, newy) == tileCorridor){ validTile = 1; // xmod = +1; ymod = 0; } else if (getCell(newx, newy-1) == tileDirtFloor || getCell(newx, newy-1) == tileCorridor){ validTile = 2; // xmod = 0; ymod = +1; } else if (getCell(newx+1, newy) == tileDirtFloor || getCell(newx+1, newy) == tileCorridor){ validTile = 3; // xmod = -1; ymod = 0; } //check that we haven't got another door nearby, so we won't get alot of openings besides //each other if (validTile > -1){ if (getCell(newx, newy+1) == tileDoor) //north validTile = -1; else if (getCell(newx-1, newy) == tileDoor)//east validTile = -1; else if (getCell(newx, newy-1) == tileDoor)//south validTile = -1; else if (getCell(newx+1, newy) == tileDoor)//west validTile = -1; } //if we can, jump out of the loop and continue with the rest if (validTile > -1) break; } } if (validTile > -1){ //choose what to build now at our newly found place, and at what direction int feature = getRand(0, 100); if (feature <= chanceRoom){ //a new room if (makeRoom((newx+xmod), (newy+ymod), 8, 6, validTile)){ currentFeatures++; //add to our quota //then we mark the wall opening with a door setCell(newx, newy, tileDoor); //clean up infront of the door so we can reach it setCell((newx+xmod), (newy+ymod), tileDirtFloor); } } else if (feature >= chanceRoom){ //new corridor if (makeCorridor((newx+xmod), (newy+ymod), 6, validTile)){ //same thing here, add to the quota and a door currentFeatures++; setCell(newx, newy, tileDoor); } } } } /******************************************************************************* All done with the building, let's finish this one off *******************************************************************************/ //sprinkle out the bonusstuff (stairs, chests etc.) over the map int newx = 0; int newy = 0; int ways = 0; //from how many directions we can reach the random spot from int state = 0; //the state the loop is in, start with the stairs while (state != 10){ for (int testing = 0; testing < 1000; testing++){ newx = getRand(1, xsize-1); newy = getRand(1, ysize-2); //cheap bugfix, pulls down newy to 0<y<24, from 0<y<25 //System.out.println("x: " + newx + "\ty: " + newy); ways = 4; //the lower the better //check if we can reach the spot if (getCell(newx, newy+1) == tileDirtFloor || getCell(newx, newy+1) == tileCorridor){ //north if (getCell(newx, newy+1) != tileDoor) ways--; } if (getCell(newx-1, newy) == tileDirtFloor || getCell(newx-1, newy) == tileCorridor){ //east if (getCell(newx-1, newy) != tileDoor) ways--; } if (getCell(newx, newy-1) == tileDirtFloor || getCell(newx, newy-1) == tileCorridor){ //south if (getCell(newx, newy-1) != tileDoor) ways--; } if (getCell(newx+1, newy) == tileDirtFloor || getCell(newx+1, newy) == tileCorridor){ //west if (getCell(newx+1, newy) != tileDoor) ways--; } if (state == 0){ if (ways == 0){ //we're in state 0, let's place a "upstairs" thing setCell(newx, newy, tileUpStairs); state = 1; break; } } else if (state == 1){ if (ways == 0){ //state 1, place a "downstairs" setCell(newx, newy, tileDownStairs); state = 10; break; } } } } //all done with the map generation, tell the user about it and finish //printf("%s %d\n",msgNumObjects.c_str(), currentFeatures); return true; } void cmain() { int x = 80; int y = 25; int dungeon_objects = 100; dungeon_map = new int[x * y]; for(;;) { if(createDungeon(x, y, dungeon_objects)) showDungeon(); std::cin.get(); } } public: Dungeon() { xmax = 80; ymax = 25; xsize = 0; ysize = 0; objects = 0; chanceRoom = 75; chanceCorridor = 25; msgXSize = "X size of dungeon: \t"; msgYSize = "Y size of dungeon: \t"; msgMaxObjects = "max # of objects: \t"; msgNumObjects = "# of objects made: \t"; msgHelp = ""; msgDetailedHelp = ""; cmain(); } }; int main() { Dungeon d; return EXIT_SUCCESS; }
Version 2
Cleaned up and modernised a little by netherh.
(Use C++11 random number generator. Common functionality abstracted. Simple Map class added.)
#include <iostream> #include <string> #include <random> #include <cassert> enum class Tile { Unused, DirtWall, DirtFloor, Corridor, Door, UpStairs, DownStairs }; enum class Direction { North, South, East, West, }; class Map { public: Map(): xSize(0), ySize(0), data() { } Map(int x, int y, Tile value = Tile::Unused): xSize(x), ySize(y), data(x * y, value) { } void SetCell(int x, int y, Tile celltype) { assert(IsXInBounds(x)); assert(IsYInBounds(y)); data[x + xSize * y] = celltype; } Tile GetCell(int x, int y) const { assert(IsXInBounds(x)); assert(IsYInBounds(y)); return data[x + xSize * y]; } void SetCells(int xStart, int yStart, int xEnd, int yEnd, Tile cellType) { assert(IsXInBounds(xStart) && IsXInBounds(xEnd)); assert(IsYInBounds(yStart) && IsYInBounds(yEnd)); assert(xStart <= xEnd); assert(yStart <= yEnd); for (auto y = yStart; y != yEnd + 1; ++y) for (auto x = xStart; x != xEnd + 1; ++x) SetCell(x, y, cellType); } bool IsXInBounds(int x) const { return x >= 0 && x < xSize; } bool IsYInBounds(int y) const { return y >= 0 && y < ySize; } bool IsAreaUnused(int xStart, int yStart, int xEnd, int yEnd) { assert(IsXInBounds(xStart) && IsXInBounds(xEnd)); assert(IsYInBounds(yStart) && IsYInBounds(yEnd)); assert(xStart <= xEnd); assert(yStart <= yEnd); for (auto y = yStart; y != yEnd + 1; ++y) for (auto x = xStart; x != xEnd + 1; ++x) if (GetCell(x, y) != Tile::Unused) return false; return true; } bool IsAdjacent(int x, int y, Tile tile) { assert(IsXInBounds(x - 1) && IsXInBounds(x + 1)); assert(IsYInBounds(y - 1) && IsYInBounds(y + 1)); return GetCell(x - 1, y) == tile || GetCell(x + 1, y) == tile || GetCell(x, y - 1) == tile || GetCell(x, y + 1) == tile; } void Print() const { // TODO: proper ostream iterator. // TODO: proper lookup of character from enum. for (auto y = 0; y != ySize; y++) { for (auto x = 0; x != xSize; x++) { switch(GetCell(x, y)) { case Tile::Unused: std::cout << " "; break; case Tile::DirtWall: std::cout << "#"; break; case Tile::DirtFloor: std::cout << "."; break; case Tile::Corridor: std::cout << "."; break; case Tile::Door: std::cout << "+"; break; case Tile::UpStairs: std::cout << "<"; break; case Tile::DownStairs: std::cout << ">"; break; }; } std::cout << std::endl; } std::cout << std::endl; } private: int xSize, ySize; std::vector<Tile> data; }; class DungeonGenerator { public: int Seed; int XSize, YSize; int MaxFeatures; int ChanceRoom, ChanceCorridor; DungeonGenerator(): Seed(std::random_device()()), XSize(80), YSize(25), MaxFeatures(100), ChanceRoom(75), ChanceCorridor(25) { } Map Generate() { // TODO: proper input validation. assert(MaxFeatures > 0 && MaxFeatures <= 100); assert(XSize > 3 && XSize <= 80); assert(YSize > 3 && YSize <= 25); auto rng = RngT(Seed); auto map = Map(XSize, YSize, Tile::Unused); MakeDungeon(map, rng); return map; } private: typedef std::mt19937 RngT; int GetRandomInt(RngT& rng, int min, int max) const { return std::uniform_int_distribution<int>(min, max)(rng); } Direction GetRandomDirection(RngT& rng) const { return Direction(std::uniform_int_distribution<int>(0, 3)(rng)); } bool MakeCorridor(Map& map, RngT& rng, int x, int y, int maxLength, Direction direction) const { assert(x >= 0 && x < XSize); assert(y >= 0 && y < YSize); assert(maxLength > 0 && maxLength <= std::max(XSize, YSize)); auto length = GetRandomInt(rng, 2, maxLength); auto xStart = x; auto yStart = y; auto xEnd = x; auto yEnd = y; if (direction == Direction::North) yStart = y - length; else if (direction == Direction::East) xEnd = x + length; else if (direction == Direction::South) yEnd = y + length; else if (direction == Direction::West) xStart = x - length; if (!map.IsXInBounds(xStart) || !map.IsXInBounds(xEnd) || !map.IsYInBounds(yStart) || !map.IsYInBounds(yEnd)) return false; if (!map.IsAreaUnused(xStart, yStart, xEnd, yEnd)) return false; map.SetCells(xStart, yStart, xEnd, yEnd, Tile::Corridor); //std::cout << "Corridor: ( " << xStart << ", " << yStart << " ) to ( " << xEnd << ", " << yEnd << " )" << std::endl; return true; } bool MakeRoom(Map& map, RngT& rng, int x, int y, int xMaxLength, int yMaxLength, Direction direction) const { // Minimum room size of 4x4 tiles (2x2 for walking on, the rest is walls) auto xLength = GetRandomInt(rng, 4, xMaxLength); auto yLength = GetRandomInt(rng, 4, yMaxLength); auto xStart = x; auto yStart = y; auto xEnd = x; auto yEnd = y; if (direction == Direction::North) { yStart = y - yLength; xStart = x - xLength / 2; xEnd = x + (xLength + 1) / 2; } else if (direction == Direction::East) { yStart = y - yLength / 2; yEnd = y + (yLength + 1) / 2; xEnd = x + xLength; } else if (direction == Direction::South) { yEnd = y + yLength; xStart = x - xLength / 2; xEnd = x + (xLength + 1) / 2; } else if (direction == Direction::West) { yStart = y - yLength / 2; yEnd = y + (yLength + 1) / 2; xStart = x - xLength; } if (!map.IsXInBounds(xStart) || !map.IsXInBounds(xEnd) || !map.IsYInBounds(yStart) || !map.IsYInBounds(yEnd)) return false; if (!map.IsAreaUnused(xStart, yStart, xEnd, yEnd)) return false; map.SetCells(xStart, yStart, xEnd, yEnd, Tile::DirtWall); map.SetCells(xStart + 1, yStart + 1, xEnd - 1, yEnd - 1, Tile::DirtFloor); //std::cout << "Room: ( " << xStart << ", " << yStart << " ) to ( " << xEnd << ", " << yEnd << " )" << std::endl; return true; } bool MakeFeature(Map& map, RngT& rng, int x, int y, int xmod, int ymod, Direction direction) const { // Choose what to build auto chance = GetRandomInt(rng, 0, 100); if (chance <= ChanceRoom) { if (MakeRoom(map, rng, x + xmod, y + ymod, 8, 6, direction)) { map.SetCell(x, y, Tile::Door); // Remove wall next to the door. map.SetCell(x + xmod, y + ymod, Tile::DirtFloor); return true; } return false; } else { if (MakeCorridor(map, rng, x + xmod, y + ymod, 6, direction)) { map.SetCell(x, y, Tile::Door); return true; } return false; } } bool MakeFeature(Map& map, RngT& rng) const { auto tries = 0; auto maxTries = 1000; for( ; tries != maxTries; ++tries) { // Pick a random wall or corridor tile. // Make sure it has no adjacent doors (looks weird to have doors next to each other). // Find a direction from which it's reachable. // Attempt to make a feature (room or corridor) starting at this point. int x = GetRandomInt(rng, 1, XSize - 2); int y = GetRandomInt(rng, 1, YSize - 2); if (map.GetCell(x, y) != Tile::DirtWall && map.GetCell(x, y) != Tile::Corridor) continue; if (map.IsAdjacent(x, y, Tile::Door)) continue; if (map.GetCell(x, y+1) == Tile::DirtFloor || map.GetCell(x, y+1) == Tile::Corridor) { if (MakeFeature(map, rng, x, y, 0, -1, Direction::North)) return true; } else if (map.GetCell(x-1, y) == Tile::DirtFloor || map.GetCell(x-1, y) == Tile::Corridor) { if (MakeFeature(map, rng, x, y, 1, 0, Direction::East)) return true; } else if (map.GetCell(x, y-1) == Tile::DirtFloor || map.GetCell(x, y-1) == Tile::Corridor) { if (MakeFeature(map, rng, x, y, 0, 1, Direction::South)) return true; } else if (map.GetCell(x+1, y) == Tile::DirtFloor || map.GetCell(x+1, y) == Tile::Corridor) { if (MakeFeature(map, rng, x, y, -1, 0, Direction::West)) return true; } } return false; } bool MakeStairs(Map& map, RngT& rng, Tile tile) const { auto tries = 0; auto maxTries = 10000; for ( ; tries != maxTries; ++tries) { int x = GetRandomInt(rng, 1, XSize - 2); int y = GetRandomInt(rng, 1, YSize - 2); if (!map.IsAdjacent(x, y, Tile::DirtFloor) && !map.IsAdjacent(x, y, Tile::Corridor)) continue; if (map.IsAdjacent(x, y, Tile::Door)) continue; map.SetCell(x, y, tile); return true; } return false; } bool MakeDungeon(Map& map, RngT& rng) const { // Make one room in the middle to start things off. MakeRoom(map, rng, XSize / 2, YSize / 2, 8, 6, GetRandomDirection(rng)); for (auto features = 1; features != MaxFeatures; ++features) { if (!MakeFeature(map, rng)) { std::cout << "Unable to place more features (placed " << features << ")." << std::endl; break; } } if (!MakeStairs(map, rng, Tile::UpStairs)) std::cout << "Unable to place up stairs." << std::endl; if (!MakeStairs(map, rng, Tile::DownStairs)) std::cout << "Unable to place down stairs." << std::endl; return true; } }; int main() { DungeonGenerator generator; auto map = generator.Generate(); map.Print(); }