一种 Roguelike 地牢生成算法

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# 代码实现


原文地址:链接


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();
}

  • 1
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在开发一款名为Tyrant的Roguelike游戏中,作者使用了一种模块化的易于使用的Java RogueLike库,该库提供了多种算法的视野、视线和投影。即将推出的功能包括基于噪声的世界生成地牢生成和路径查找。这个库的详细信息可以在它的网站上找到。 对于地牢生成,有一种常见的方法是从一个小的地牢开始,然后慢慢向四周扩散,直到整个地牢形成。这种方法可以满足一些地下城主想要用吊桥、陷阱等来守护房间的需求。通过结合这些算法和方法,可以实现Java算法Roguelike地牢生成。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [一种 Roguelike 地牢生成算法](https://blog.csdn.net/weixin_33027875/article/details/114039219)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Roguelike library for Java-开源](https://download.csdn.net/download/weixin_42156940/18157263)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值