游戏地图动态生成
随机迷宫生成
游戏地图的动态生成要追溯到传统的随机迷宫生成。随机迷宫生成可以描述成这样的问题:在一个m*n的网格中,每一个网格的边代表一堵墙,初始时所有网格彼此不联通,现在要随意打穿一些墙,使得特殊的两个网格(起点和终点)能够联通。 以图论来描述就是:初始其实是一个 连通图,每一个网格代表图的节点,墙代表图的边,每一个随机生成的迷宫对应这个图的一个生成树。
完美迷宫
完美迷宫就是没有回路,没有不可达区域的迷宫。在图论中这就是一条最小生成树了。下面介绍的算法都是生成完美迷宫的。
DFS(深度优先的搜索)生成完美迷宫
1. 在网格中随机取一个点作为搜索起始点;
2. 标记当前点为已经访问过,取周围的网格点(上,下,左,右四个方向),如果有一点未访问,则打穿连接这个邻居节点的墙,并选取为当前点重复步骤2,如果不存在任何未访问过的邻居,则表示进到死胡同了,这时候要向上回溯一个节点再重复步骤2;
3. 一直重复步骤2,直到所有节点都被标记为访问过了。
这个算法简单明了,复杂度是网格个数,但是有可能搜索的层次太深了,耗费大量内存,如果是递归的实现就会栈溢出。代码如下:
template <size_t R, size_t C > void DFS (unsigned char ver_walls [R][ C+1], unsigned char hor_walls[R +1][C]) { unsigned char visited[ R][C ] = {0}; stack<Node > 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<size_t R, size_t C > 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(); }
以上代码在这里可以得到。