随机迷宫生成
游戏地图的动态生成要追溯到传统的随机迷宫生成。随机迷宫生成可以描述成这样的问题:在一个m*n的网格中,每一个网格的边代表一堵墙,初始时所有网格彼此不联通,现在要随意打穿一些墙,使得特殊的两个网格(起点和终点)能够联通。 以图论来描述就是:初始其实是一个 连通图,每一个网格代表图的节点,墙代表图的边,每一个随机生成的迷宫对应这个图的一个生成树。
完美迷宫
完美迷宫就是没有回路,没有不可达区域的迷宫。在图论中这就是一条最小生成树了。下面介绍的算法都是生成完美迷宫的。
DFS(深度优先的搜索)生成完美迷宫
1. 在网格中随机取一个点作为搜索起始点;
2. 标记当前点为已经访问过,取周围的网格点(上,下,左,右四个方向),如果有一点未访问,则打穿连接这个邻居节点的墙,并选取为当前点重复步骤2,如果不存在任何未访问过的邻居,则表示进到死胡同了,这时候要向上回溯一个节点再重复步骤2;
3. 一直重复步骤2,直到所有节点都被标记为访问过了。
这个算法简单明了,复杂度是网格个数,但是有可能搜索的层次太深了,耗费大量内存,如果是递归的实现就会栈溢出。代码如下:
template
void DFS (unsigned char ver_walls [R][ C+1], unsigned char hor_walls[R +1][C]) {
unsigned char visited[ R][C ] = {0};
stack path;
int rv = rand() % ( R*C );
Node start (rv / C, rv % C);
memset(ver_walls , 1, sizeof( unsigned char )*R*( C+1));
memset(hor_walls , 1, sizeof( unsigned char )*(R+1)* C);
path.push (start);
visited[start ._x][ start._y ] = 1;
unsigned int num_visited = 1;
while ( num_visited != R* C) {
Node cur = path. top();
Node avaliable [4];
BT bts [DIR];
int cnt = 0;
// to left;
if (cur ._y - 1 >= 0 && ! visited[cur ._x][ cur._y -1]) {
avaliable[cnt ]._x = cur._x ;
avaliable[cnt ]._y = cur._y - 1;
bts[cnt ] = LEFT;
cnt++;
}
// to right;
if (cur ._y + 1 < C && !visited [cur. _x][cur ._y+1]) {
avaliable[cnt ]._x = cur._x ;
avaliable[cnt ]._y = cur._y + 1;
bts[cnt ] = RIGHT;
cnt++;
}
// to top;
if (cur ._x - 1 >= 0 && ! visited[cur ._x-1][ cur._y ]) {
avaliable[cnt ]._x = cur._x - 1;
avaliable[cnt ]._y = cur._y ;
bts[cnt ] = TOP;
cnt++;
}
// to bottom;
if (cur ._x + 1 < R && !visited [cur. _x+1][cur ._y]) {
avaliable[cnt ]._x = cur._x + 1;
avaliable[cnt ]._y = cur._y ;
bts[cnt ] = BOTTOM;
cnt++;
}
if (cnt > 0) {
int iselected = rand() % cnt;
path.push (avaliable[ iselected]);
visited[path .top(). _x][path .top(). _y] = 1;
num_visited++;
switch (bts [iselected]) {
case BOTTOM : hor_walls[cur ._x+1][ cur._y ] = 0; break;
case TOP : hor_walls[cur ._x][ cur._y ] = 0; break;
case LEFT : ver_walls[cur ._x][ cur._y ] = 0; break;
case RIGHT : ver_walls[cur ._x][ cur._y +1] = 0; break;
default: break ;
}
} else {
path.pop ();
assert(path .size() > 0);
}
}
}
最小生成树Prim算法
在一个顶点集合为V,边集为E的加权连通图的中,求其最小生成树的Prim算法过程如下。
(1)随机一个顶点置入closed集合中,剩下的顶点集合叫做opened:closed = {x}, x为随机任意顶点,V = closed U opened;
(2)重复下列操作,直到 closed = v:
在所有连接opened和closed的边中找到一条最小权值的边,把在opened中那一端的顶点挪动 到closed中,该边就是最小生成树的一条边;
随机生成迷宫中每一个网格点是一个顶点,网格点与上下左右网格点连通,权值都是1,这样这个网格就也是一个加权连通图了。而且,因为知道最小权值的边就是上下左右的边,所以可以在closed集合中随机一个顶点,然后在4条边中随机一条没有使用过的边当作最小生成树的边,把墙挖通,代码如下:
template
void Prim (unsigned char ver_walls [R][ C+1], unsigned char hor_walls[R +1][C]) {
unsigned char used[ R][C ] = {0};
Node nodes_avaliable [R* C];
BT bts [R* C];
size_t num_avaliable = 0
;
int rv = rand()%( R*C );
nodes_avaliable[num_avaliable ] = Node( rv/C , rv% C);
used[rv /C][ rv%C ] = 1;
bts[num_avaliable ] = EMPTY;
num_avaliable++;
memset(ver_walls , 1, sizeof( unsigned char )*R*( C+1));
memset(hor_walls , 1, sizeof( unsigned char )*(R+1)* C);
while(num_avaliable >0){
int iselected = rand()% num_avaliable;
Node cur = nodes_avaliable[ iselected];
BT from = bts[ iselected];
nodes_avaliable[iselected ] = nodes_avaliable[ num_avaliable-1];
bts[iselected ] = bts[ num_avaliable-1];
num_avaliable--;
switch(from ) {
case LEFT : ver_walls[cur ._x][ cur._y +1]= 0; break;
case RIGHT : ver_walls[ cur._x ][cur. _y] = 0; break ;
case TOP : hor_walls[cur ._x+1][ cur._y ] = 0; break;
case BOTTOM : hor_walls[ cur._x ][cur. _y] = 0; break ;
default:break ;
}
// LEFT
if(cur ._y - 1 >=0 && ! used[cur ._x][ cur._y -1]) {
nodes_avaliable[num_avaliable ] = Node( cur._x , cur. _y-1);
bts[num_avaliable ] = LEFT;
num_avaliable++;
used[cur ._x][ cur._y -1]=1;
}
// RIGHT
if(cur ._y + 1 < C && !used [cur. _x][cur ._y+1]) {
nodes_avaliable[num_avaliable ] = Node( cur._x , cur. _y + 1);
bts[num_avaliable ] = RIGHT;
num_avaliable++;
used[cur ._x][ cur._y +1]=1;
}
// TOP
if (cur ._x- 1 >= 0 && ! used[cur ._x-1][ cur._y ]) {
nodes_avaliable[num_avaliable ] = Node( cur._x -1, cur. _y);
bts[num_avaliable ] = TOP;
num_avaliable++;
used[cur ._x-1][ cur._y ]=1;
}
// BOTTOM
if (cur ._x+1 < R && !used [cur. _x+1][cur ._y]) {
nodes_avaliable[num_avaliable ] = Node( cur._x +1, cur. _y);
bts[num_avaliable ] = BOTTOM;
num_avaliable++;
used[cur ._x+1][ cur._y ]=1;
}
}
}
当然最小生成树算法还有kruskal,另外还有一些有趣的随机迷宫生成算法,维基百科描述的最清楚详尽了:http://en.wikipedia.org/wiki/Maze_generation_algorithm,以下是生成的15*15迷宫地图效果:
细胞自动机生成游戏地图
相对于传统的随机迷宫生成,这种方式更加注重模拟自然状态,如下是生成20*50地图的效果:
关于细胞自动机算法,具体描述google cellular automata,这里主要看我们是怎么用 cellular automata生成地图的:
在一个m*n的网格中,每一个cell有且有两个状态(WALL, FLOOR),每一个cell有8个邻居(上,下,左,右,左上,左下,右上,右下)。初始时把每一个cell随机置为WALL或者FLOOR,然后对每一个cell使用这样的规则,若周围是WALL的邻居个数大于5,则把自己置为WALL,若个数小于4,则把自己置为4,否则自己保持原样不变,过程中应保证边框总是WALL。这个叫做4-5规则,4和5是 cellular automata规则应用的两个参数,可以调整。这样生成出来的图就是如下效果:
看起来与真实地理环境比较像了,暂且把挖空的空间叫做cave,图中出现了7个彼此不连通的cave,现在需要把7个cave打通,让其不存在不可达的区域。算法思想就是,让每个cave朝图的中间延伸,最终所有cave在中间聚合,具体实现就是采用并查集这样的数据结构,每一个身为FLOOR的Cell归属于一个cave集合,初始时把FLOOR cell归属到各自的cave集合中,之后针对每一个cave,取其中一点向中心移动,遇到是WALL的Cell则挖空成FLOOR,直到遇到一个是FLOOR的Cell且和自己不是一个Cave的(代表两个Cave相聚了),或者到达了中心点就停止延伸。
细胞自动算法生成游戏地图的整个流程,代码如下:
void generation (double init_open_ratio, int low_rule_param, int up_rule_param ) {
if (_w <= 2 || _h <=2) return;
init_map(init_open_ratio );
cellular_automata(low_rule_param , up_rule_param);
make_cave();
connection();
}
以上代码在这里可以得到。