用qt实现一个迷宫,玩家通过键盘方向键移动,从左上角走到右下角即为成功。
先来看下最终实现效果:
其实重点在于随机迷宫的生成,而迷宫的生成靠的是并查集算法。
思路参考自bigsai(感谢授权):
游戏总结如下:
游戏规则为若两个小方块之间没有线,说明是互通的,可以移动,否则两个方块之间被“墙”阻隔不能移动。因此我们先画一个由多个小方块组成的初始地图,然后随机擦除其中的一些线,就可以生成一个迷宫了。就像这样:
而将两个方格之间的线擦除,是不是也就意味着这两个方格是一个集合了(他们是一家子了),所以擦除线的过程实际上就是两个原本不相交的集合合并的过程,而并查集解决的正是合并的问题。知道了使用并查集,那么对于地图而言,该如何实现这个合并过程呢?我们可以将地图中每个方格抽象成一个个点,比如对于5 x 5大小的地图,就有25个小方格,按顺序排个号,这时我们可以发现,其实数值和数值在地图中的坐标是存在关系的,比如:11 / 5 == 2;11 % 5 == 1,而(2, 1)正是11所在地图的位置,利用这种关系就可以把地图当做一个一维数组来看待。
这样一来就好办了,直接套用并查集的模板即可。
游戏的目的是从左上角走到右下角,也就意味着左上角的点和右下角的点一定是可以连通的。为了生成随机迷宫,我们在地图中随机找一些点,判断它和它上下左右四个方向的点是否连通,不连通则将两个集合连通,一直重复,直到左上角和右下角连通了,说明游戏是有解的了,这时就不再合并了。总结一下就是:我们为了让游戏是有解的,就在地图中随机找点合并一些集合,当发现有解时,就不再合并。当把两个点连通时,就将它们之间的线擦除,也就形成了视觉上的随机地图。
迷宫生成了,但是在玩家走的时候,如何判断当前所走方向有没有被阻隔(有没有线)呢?这很好办,通过一个二维数组来记录两个点之间有没有线,比如:在合并的过程中,合并1和2,则让arr[1][2] = 1,这样当1走向2或2走向1时判断arr[1][2]是否为1即可。
这样的话,随机地图产生了,并且左上角到右下角是连通的,判断两格之间是否能走的问题也解决了,迷宫也就没什么难的了。
下面看一下一些关键的代码片段:
初始化地图(画方格):
int x1 = BEGINX, y1 = 0, x2 = ENDX, y2 = 0; for (int i = 0; i < LINENUM; i++) { y1 += LINEHEIGHT; y2 = y1; //画横线 line.setLine(x1, y1, x2, y2); m_pPainter->drawLine(line); //画竖线 line.setLine(y1, x1, y2, x2); m_pPainter->drawLine(line); }
生成随机地图:
//当左上角的点和右下角的点不连通 while (find(0) != find(SQUARESNUM - 1)) { //在合法范围内随机生成一个点 int randnum = rand() % SQUARESNUM; //随机找一个邻居(上下左右) int neighbor = getNeighbor(randnum); if (find(randnum) != find(neighbor)) { m_qvectorIsUnicom[randnum][neighbor] = m_qvectorIsUnicom[neighbor][randnum] = 1; //将两个点之间的线擦除 myDrawLine(randnum, neighbor); merge(randnum, neighbor); } }
这里的画线只是为了满足视觉上的效果,因此,我只是把画笔的颜色改成了白色,覆盖了之前的黑色,也就形成了擦除的效果。
找邻居:
int Widget::getNeighbor(int n) { //计算得到点n在地图中的坐标 int x = n / ROWNUM; int y = n % ROWNUM; QVector<int> arr; //判断是否出界 if (y > 0) arr.push_back(n - 1);//左邻居 if (y < ROWNUM - 1) arr.push_back(n + 1);//右邻居 if (x > 0) arr.push_back((x - 1) * ROWNUM + y);//上邻居 if (x < ROWNUM - 1) arr.push_back((x + 1) * ROWNUM + y);//下邻居 //取一个随机的邻居 int tmp = rand() % arr.size(); return arr[tmp]; }
擦除线:
void Widget::myDrawLine(int n, int m) { int x1 = n / ROWNUM; int y1 = n % ROWNUM; int x2 = m / ROWNUM; int y2 = m % ROWNUM; //取两点中间线位置 int x3 = (x1 + x2) / 2 + 1; int y3 = (y1 + y2) / 2 + 1; QPen pen; pen.setColor(QColor(Qt::white)); pen.setWidth(3); QLineF line; m_pPainter->begin(m_pPixmap); m_pPainter->setPen(pen); //上下方向的点,画横线 if (x1 - x2 == 1 || x1 - x2 == -1) { line.setLine(y1 * LINEWIDTH + BEGINX, x3 * LINEHEIGHT + BEGINX, (y1 + 1) * LINEWIDTH + BEGINX, (x3 + 1) * LINEHEIGHT); } else { line.setLine(y3 * LINEWIDTH + BEGINX, x1 * LINEHEIGHT + BEGINX, (y3 + 1) * LINEWIDTH, (x1 + 1) * LINEHEIGHT + BEGINX); } m_pPainter->drawLine(line); m_pPainter->end(); }
并查集的合并和查询:
int Widget::find(int n) { if (m_qvectorArray[n] == n) return n; return find(m_qvectorArray[n]); } void Widget::merge(int n, int m) { int fn = find(n), fm = find(m); if (fn == fm) return; if (m_qvectorSize[fn] > m_qvectorSize[fm]) std::swap(fn, fm); m_qvectorArray[fn] = fm; m_qvectorSize[fm] += m_qvectorSize[fn]; return; }
至此就完成了迷宫的核心部分,剩下的就是通过qt完成控制,画线以及对游戏体验的优化了,见源代码。最后放上操作指南:按下方向键开始游戏,方向键控制走向,超过一分钟游戏自动结束,按下重置按钮更换地图,游戏就绪。
效果图:
源代码(包含可执行程序):
https://gitee.com/gao-yuelong/qtdemo/tree/master/mazes
最后还是感谢bigsai提供的思路,非常感谢。