基于A*算法的迷宫游戏开发
实验要求
1.迷宫随机生成;
2.玩家走迷宫,留下足迹;
3.系统用A算法寻路,输出路径
解决问题:1.如何显示迷宫的图形界面;
2.如何生成随机的迷宫;
3.怎样移动游戏中走迷宫的“玩家”;
4.用A算法求解迷宫
实验准备
1.如何显示迷宫的图形界面
Java Swing GUI图形界面窗口开发
Swing是Java为图形界面应用开发提供的一组工具包,是Java基础类的一部分。它包含了构建图形界面(GUI)的各种组件,如:窗口、标签、按钮、文本框等。
2.如何生成随机的迷宫
本实验采用深度优先遍历(DFS)
可以这样描述深度遍历:
①访问顶点v;
②从v的未被访问的邻接点中选取一个顶点w,重复第一步,如果v没有未访问的邻接点,回溯至上一顶点;
③重复上述两步,直至图中所有和v有路径相通的顶点都被访问到。
将这些节点放到迷宫这样的矩阵图里,把这些节点全部遍历,就会在任意两个节点之间形成一个通路。
整理思路:在遍历过程中将遍历路径通路节点之间的墙“抹掉”-换成空白的格子,即打通为通路,这样就可以找到一条路。
通过图片可以看出,深度优先算法只适用于行数和列数均为奇数的迷宫生成,因此在算法中做了一定修改来满足深度优先算法可以适用于任何情况。
3.怎样移动游戏中走迷宫的“玩家”
通过事件驱动处理程序,添加按键响应来实现“玩家”的移动。
4.用A星算法求解迷宫
在计算机科学中,A*算法作为Dijkstra(迪杰斯特拉)算法的扩展,是一种静态路网中求解最短路径有效的直接搜索方法,因其高效性被广泛应用于寻路及图的遍历中。
搜索区域(The Search Area):搜索区域被划分为简单的二维数组,数组每个元素对应一个结点。
开放列表(Open List):将寻路过程中待检测的节点存放于Open List中,而已检测锅的节点则存放于Close List中。
路径排序(Path Sorting):下一步怎么移动由以下公式确定:F(n)=G+H。F(n)为估价函数,G代表的是从初始位置Start沿着已生成的路径到指定待检测节点移动开销。H标识待检测节点到目标节点B的估计移动开销。
启发函数(Heuristics Function):H为启发函数,可以看做是一种试探,由于在找到唯一路径前,不确定在前面会出现什么障碍物,因此用了一种计算H的算法。具体可以根据实际情况决定。为了简化问题,H采用的是传统的曼哈顿距离,也就是横纵向走的距离之和。
A星算法流程:
重复以下步骤,直到遍历到终点 End:
1.选取当前 open 列表中评价值 F 最小的节点,将这个节点称为 S;
2.将 S 从 open 列表移除,然后添加 S 到 closed 列表中;
3.对于与 S 相邻的每一块可移动的相邻节点 T:如果 T 在 closed 列表中,忽略;如果 T 不在 open 列表中,添加它然后计算出它的评价值 F;如果 T 已经在 open 列表中,当我们从 S 到达 T 时,检查是否能得到 更小的 F 值,如果是,更新它的 F 值和它的前继(parent = S)。
由于A星算法要求节点的属性过多,与我创建迷宫时所用的属性相差过多,因此本实验采用了一种和A星算法相类似的算法实现迷宫自动寻路——路径深度求解。
首先引入两个概念:
路径深度:从某位置走到出口的最短路径的长度,设每一块方块为单位路径长度。假定出口处路径深度为0,障碍物处路径深度为-1。
路径深度图:与迷宫对应的图,每一个节点值为与该节点对应的迷宫单元格的路径深度。
基于上述概念,不难证明,迷宫最短路径必然是一条从入口处开始、到出口处结束的路径深度逐次递减1的序列。因此,如果能求出整个迷宫每个单元格的路径深度,那么求解迷宫最短路径就简单了。于是,问题的重点就在于如何求解路径深度图。
稍作思考,我们不难理解,对于迷宫上任意一个非障碍物的点,它的路径深度值必然不大于其周围四个方向上的非障碍物点中最小路径深度值+1。该方法就是在这个结论的基础下设计的算法。算法每一个循环动态地更新所有点的路径深度直至整张图达到稳态。再通过得到的路径深度图求得最短路径。
实验过程
本次实验依然采用Java来完成。该项目包含四个类,分别为map类、Operations类、StartUI类、wrmPane类。部分核心代码如下。
map类
迷宫界面的设计
剩余时间的显示
static Thread timeThread; //时间控制线程
static int timelimit, remaintime;
static JPanel timePanel = new JPanel() {
//剩余时间显示面板
public void paintComponent(Graphics g) {
super.paintComponent(g);
String rt;
if (timelimit == 0) {
rt = "无限制";
setForeground(Color.GREEN); //绿色表示无时间限制
} else {
rt = remaintime / 3600 + " : " + (remaintime - (remaintime / 3600) * 3600) / 60 + " : " + remaintime % 60;
if (remaintime > 10)
setForeground(Color.BLUE); //剩余时间充足时为蓝色
else
setForeground(Color.RED); //剩余时间很少时为红色
}
g.drawString("剩余时间: " + rt, 220, 16);
}
};
重写run方法实现对时间的控制
public void run() {
if (timelimit > 0) {
while (true) {
try {
Thread.sleep(1000);
if (remaintime > 0)
remaintime--;
timePanel.repaint();
if (timelimit > 0 && remaintime == 0) {
if (Operations.m_currex != m - 1 || Operations.m_currey != n - 1) {
Object[] options = {
"新游戏", "重来一次"};
int response = JOptionPane.showOptionDialog(this, " 很遗憾,你没有在限制的时间里完成任务,可怜的小老鼠已经饿死了\n请选择开始新的游戏,或重玩此游戏", "游戏超时!", JOptionPane.YES_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]);
if (response == 0) {
Operations.restart = true;
Operations.start();
} else {
remaintime = timelimit;
tp[Operations.m_currex][Operations.m_currey].change(1);
Operations.m_currex = Operations.m_startx;
Operations.m_currey = Operations.m_starty;
tp[Operations.m_currex][Operations.m_currey].change(2);
}
}
}
} catch (Exception e) {
}
}
}
}
该类还包括菜单事件和键盘按键的处理。
Operations类
核心算法实现
迷宫最短路径深度图算法
public static void findPath() {
map.timeThread.suspend(); //时间控制线程休眠
changeable_key = false; //不可用键盘控制老鼠
setEditable(false); //不可编辑
m = map.m;
n = map.n;
int max = m * n; //任意一点到奶酪的最短路径长度不会超出m*n。
int[] depthGraph = new int[m * n]; //路径深度图
//路径深度图初始化
depthGraph[m * n - 1] = 0; //奶酪到自己的距离自然是0
for (int i = 0; i < m * n - 1; i++) {
if (map.tp[i / n][i % n].isWall())
depthGraph[i] = -1; //墙表示为-1,表示无通路
else
depthGraph[i] = max; //未确定距离时已max表示
}
boolean flag = true; //循环过程中是否有某点的路径深度被修改
int currex, currey; //记录当前访问点的坐标
int aroundmin; //周围可行方向的最小路径深度 + 1
//动态更新路径深度图直至其达到稳态(即最后一次循环过程中不再有路径深度被修改)
while (flag) {
flag = false;
for (int s = m * n - 1; s >= 0; s--) {
if (depthGraph[s] != -1) {
aroundmin = depthGraph[s];
currex = s / n;
currey = s % n;
if (currey + 1 < n && depthGraph[s + 1] != -1 && depthGraph[s + 1] + 1 < aroundmin)
aroundmin = depthGraph[s + 1] + 1;
if (currex + 1 < m && depthGraph[s + n] != -1 && depthGraph[s + n] + 1 < aroundmin)
aroundmin = depthGraph[s + n] + 1;
if (currey - 1 >= 0 && depthGraph[s - 1] != -1 && depthGraph[s - 1] + 1 < aroundmin)
aroundmin = depthGraph[s - 1] + 1;
if (currex - 1 >= 0 && depthGraph[s - n] != -1 && depthGraph[s - n] + 1 < aroundmin)
aroundmin = depthGraph[s - n] + 1;
if (aroundmin < depthGraph[s]) {
depthGraph[s] = aroundmin;
flag = true;
}
}
}
}
利用生成的路径深度图,实现最短路径的寻找
int[] path = new int[m * n]; //用于存放最短路径的数组
int currePoint = m_startx * n + m_starty; //当前访问点,初始值为老鼠位置
int depth = depthGraph[currePoint]; //老鼠位置的路径深度值
int step = depth - 1; //当前要查找的路径深度
while (step > 0) {
currex = currePoint / n;
currey = currePoint % n;
if (currey + 1 < n && depthGraph[currePoint + 1] == step) {
currePoint += 1;
} else if (currex + 1 < m && depthGraph[currePoint + n] == step) {
currePoint += n;
} else if (currey - 1 >= 0 && depthGraph[currePoint - 1] == step) {
currePoint -= 1;
} else if (currex - 1 >= 0 && depthGraph[currePoint - n] == step) {
currePoint -= n;
}
path[step--] = currePoint;
}
int s; //临时存放位置
for (int i = 1; i < depth; i++) {
s = path[i];
map.tp[s / n][s % n].change(2); //显示最短路径
}
restart = true; //可开始新游戏
}
深度优先遍历随机生成迷宫
public static void creatMaze() {
m = map.m;
n = map.n;
//遍历前初始化工作
isBeVisit = new boolean[m * n];
for (int i = 0; i < m * n; i++) isBeVisit[i] = false; //是否已被访问
//迷宫初始化
for (int i = 0; i < m; i++) {
//防止发生两边上全为墙的情况
map.tp[i][0].change(Math.random() * 3 > 1 ? 0 : 1);
map.tp[i][n - 1].change(Math.random() * 3 > 1 ? 0 : 1);
}
for (int i = 0; i < n; i++) {
map.tp[0][i].change(Math.random() * 3 > 1 ? 0 : 1);
map.tp[m - 1][i].change(Math.random() * 3 > 1 ? 0 : 1);
}
for (int i = 1; i < m - 1; i++)
for (int j = 1; j < n - 1; j++)
map.tp[i][j].change(0); //内部的位置初始化全为墙
m_startx = (int) (Math.random() * m / 2);
m_starty = 0; //随机生成老鼠位置
//从老鼠位置开始深度优先遍历与它x 、y坐标相差均为偶数的点构成的图
DFS(m_startx * n + m_starty);
//这一步在 tp[m-2][n-2]与老鼠位置x 、y坐标相差均为偶数时非常重要,保证有到达粮仓的路径
if (Math.random() * 2 > 1)
map.tp[m - 2][n - 1].change(1);
else
map.tp[m - 1][n - 2].change(1); //两者只要有一个为路即可,故随机取其一
//老鼠和奶酪的位置作另作处理
map.tp[m_startx][m_starty].change(2); //老鼠
map.tp[m - 1][n - 1].change(3); //奶酪
changeable_key = false; //键盘不可控制老鼠移动
m_currex = m_startx;
m_currey = m_starty; //开始新游戏前老鼠当前位置与开始位置相等
restart = false;
}
//从S点开始深度优先遍历与它x 、y坐标相差均为偶数的点构成的图,并打通每一步需要通过的墙
public static void DFS(int s) {
map.tp[s / n][s % n].change(1);
isBeVisit[s] = true;
int[] direction = new int[4]; //用于以随机顺序存储方向 右0下1左2上3
boolean[] isStored = new boolean[4];
for (int i = 0; i < 4; i++) isStored[i] = false; //方向是否已被存储
int currex = s / n, currey = s % n; //当前点对应的实际坐标