实验准备(超级玛丽闯迷宫)
经过了前面两个项目的实践,我对python和pyqt5已经有了一定的了解,除了能够把实验要求全部完成,还加入了自己的一些想法在游戏里,让游戏更加有趣。
第三个项目主要的需求是能随机生成一个迷宫,并且能够自动寻路来破解迷宫。迷宫的随机生成主要采用Prim算法来生成迷宫,一方面是因为Prim算法生成的迷宫比较自然,另一方面是之前了解过Kurskal最小生成树的思想,二者有共通性,故选用此算法。自动寻路则使用实验要求里的A*算法来实现,它相比广度优先搜索的效率更高,因为它有启发函数,能够在边权为1的图里面提供额外的启发信息,能够更快的收敛,搜索到目标结点。
目录
一、迷宫定义(Map类)
要实现随机生成迷宫第一步当然是定义迷宫,定义迷宫的大小以及对迷宫进行获取、设置、重置、判断、打印等操作的方法。
1. 成员变量
(1)width,height:定义迷宫的行数和列数。
(2)map:二维列表用来存放迷宫地图,坐标定义与数组索引相似,x为纵向,y为横向(与一般坐标系不同)。
二维列表中每一个元素的值代表不同的含义,分别如下:
① map[i][j] == 0:当前格为空格。
② map[i][j] == 1:当前格为墙壁。
③ map[i][j] == 2:当前格为起点。
④ map[i][j] == 3:当前格为终点。
⑤ map[i][j] == 4:当前格为自动寻路路径上的结点。
2. 成员方法
(1)getMap(self):返回当前对象的map[n][m]二位迷宫列表。
(2)resetNode(self, value):设置map[n][m]二位迷宫列表的每一个元素为value值。
(3)setNode(self, x, y, value):设置map[x][y] = value。
(4)isWall(self, x, y):判断map[x][y] = 1 ?也就是判断是否是墙壁。
(5)showMap(self):打印map[n][m]整个迷宫。
二、随机生成迷宫(Prim算法)
定义了迷宫后我们来实现Prim算法,用它来实现迷宫的随机生成,Prim算法可在加权连通图里搜索最小生成树,那么首先我们来了解什么是最小生成树。
1. 最小生成树
百度百科给出的定义是:一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。
我的理解是:在一个带权图中,有n个结点,用n-1条边使得这n个点相互之间可达,这也就是生成树,所谓最小即为这n-1条边的权值之和是最小的。所以最小生成树其实就是在n个顶点的图中选n-1条边使得这个图为联通图(强连通图)并且这n-1条边的权值之和最小。
如下是我用3D画图画的无向赋权图
它的生成树:
最小生成树:
2. Prim算法流程
(1) 输入:一个加权连通图,其中顶点集合为V,边集合为E;
(2) 初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;
(3) 重复下列操作,直到Vnew = V:
① 在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
②将v加入集合Vnew中,将<u, v>边加入集合Enew中;
(4) 输出:使用集合Vnew和Enew来描述所得到的最小生成树。
3. prim算法和迷宫生成的关系
假设迷宫为7×7的正方形,9个白格子为初始空结点,黑色格子为墙壁。
接下来用Prim算法即可生成这个图的一个最小生成树,这个最小生成树就是迷宫的连通的部分,也就是玩家可以走的部分。并且因为每一个边权都是1,所以只要边权之和一定是n-1也就是 9-1 = 8 。
最后生成如上图,也就是我们所需要的迷宫,途中一共有8条边,边权之和也就是 8*1 = 8。
4. prim算法生成迷宫流程
预处理部分:
(1)定义一个检查列表checklist存放为划分顶点集。
(2)把初始起点加入checklist列表中。
主循环部分:
(3)重复下面几个步骤直到检查列表checklist为空:
① 随机取出checklist列表中的一个结点node1。
② 检查这个结点node1周围有没有墙wall。
③ 有墙:从这个结点node1周围的墙中随机选一个,假设是wall_left,把这个墙置为空;并且把这个墙与node1之间的结点也置为空;最后把这个墙的位置坐标加入checklist中。
④ 无墙:把node1从checklist中删除。
(4)通过上述步骤后,迷宫即可随机生成。
三、迷宫自动寻路(A*算法)
1. 算法简介
A* 算法是一种静态路网中求解最短路径最有效的直接搜索方法,我认为A*算法与BFS广度优先搜索算法有很多相似之处,不过它相比广度优先搜索的效率更高,因为它有启发函数,能够在边权为1的图里面提供额外的启发信息,能够更快的收敛,搜索到目标结点。
2. 路径选择
那么我们如何在若干条路径中选出最优的一条呢?
这就需要定义路径代价:F
它衡量了当前路径的优劣程度,对是否继续采用改路径具有决定作用。
同时它满足下列等式:
F = G + H
G是移动代价,即从起点沿当前路径到当前结点的路径的长度。
H是估算代价,我们这里使用曼哈顿距离,即当前结点到目标结点的最短路径长度(忽略地形)。
3. 定义结点
为什么要定义结点:因为最后输出路径的时候需要从最后一个目标结点不断访问父节点来进行路径追溯,最后才能输出从源节点到目标结点的一条完整路径。
主要变量有:
- x、y坐标
- 沿当前路径到map[x][y]的路径长度(G值)
- 当前路径的F值(F = G + H)
- 父节点(用于路径回溯)
4. 子方法
(1)pathEvaluate(val, fx, fy, ex, ey) :
计算当前路径代价(F = G + H)。
(2)findMin(open) :
返回open表中代价最小的结点。
(3)find_node(node, list):
判断某个结点在不在某个表中。
(4)AStar(map, row, col, sx, sy, ex, ey)方法:
完成A* 算法的主逻辑,执行过程中调用前面三个子方法。
5. 算法流程
定义与预处理部分:
(1)定义一个open开放列表,这里面的结点可能更新可能移除。
(2)定义一个close封闭列表 ,这里面的结点不需要处理。
(3)往open表中加入起点。
主循环部分:
(4)重复下面几个步骤直到检查列表checklist为空:
① 找到open表中代价最小的结点,把它从open表中取出并放入close表中。
② 判断这个结点是否是目标结点
③ 是:通过这个结点不断回溯到源节点生成一条路径,返回这个路径并退出整个AStar函数。
④ 否:往相邻结点nx拓展搜索。
⑤ 如果相邻结点nx:1.在close表里。2.是墙壁。3. 超出地图边界。就跳过这个相邻结点nx,直到这个结点的4个相邻结(n1、n2、n3、n4)点都判断过。
⑥ 如果相邻结点nx在open表中(注意只要 (x,y) 坐标相同就算相同),比较相邻结点nx和open表中结点的G移动代价,如果相邻结点n的G值比open表中相同结点的G值小,则更新open表中的相同结点的G值和F值,并且把open表中的相同结点的父节点设置为当前结点。
⑦ 如果相邻结点nx不在open表中,直接把相邻结点nx加入open表中。
(4)通过上述步骤后,从源结点到目标结点的路径就生成了。
四、游戏界面(PyQt5)
1. 成员变量
① 窗口变量:设置窗口大小
② 地图变量:设置地图行列数、起点、终点、地图列表
③ 玩家变量:定义玩家坐标
④ 游戏标志:定义各种游戏标志,如到第几关、是否通关、人物左右。
⑤ 音乐播放器:音乐播放器的初始化以及音乐的导入。
2. 构造函数
- 设置窗口标题
- 设置图标
- 设定游戏背景颜色
- 游戏窗口大小
- 让游戏窗口大小固定
- 初始化游戏
3. 子方法
- init(self): 初始化游戏
- update_bgm(self): 更新背景音乐
- update_level(self): 更新游戏关卡
- showMap(self): 打印地图
- paintEvent(self, event): 绘图事件
- sound(self): 播放音效
- checkPass(self): 判断通关
- findpath(self): 自动寻路
- keyPressEvent(self, event): 键盘事件
这次迷宫游戏学习了Prim算法以及A*算法收获了很多,同时发掘经过前两个项目的实践,这次游戏编写及调试的速度有了提升,整个游戏2天完成,第3天写博客收尾。
———2020.12.21(1:25)(罗涵)
THE END