基于A*算法的迷宫游戏开发
一、项目概述
1.1项目目标和主要内容
1)迷宫游戏是非常经典的游戏,在该题中要求随机生成一个迷宫,并求解迷宫;
2)要求游戏支持玩家走迷宫,和系统走迷宫路径两种模式。玩家走迷宫,通过键盘方向键控制,并在行走路径上留下痕迹;系统走迷宫路径要求基于A*算法实现,输出走迷宫的最优路径并显示。
3)设计交互友好的游戏图形界面。
1.2项目的主要功能
1)可以随机生成迷宫并使用A*算法求解迷宫
2)并未添加在行走路径上留下痕迹的功能
二、项目分析
1)深度优先遍历生成迷宫地图
2)搭建GUI迷宫地图
3)A*算法寻路
三、算法分析
3.1深度优先遍历初始化地图
主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底…,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点是不撞南墙不回头,先走完一条路,再换一条路继续走。
如树的先序(中序,后序)遍历都是深度优先遍历
public void accLabDFS() {
int[] lab;// 访问队列
int count = row * column;// 所有的迷宫单元数,不包括墙
lab = new int[count];
for (int i = 0; i < count; i++)
lab[i] = 0;// 设置所有单元都为未访问过的,0表示未访问过,1表示已访问过
for (int v = 0; v < count; v++) {// 从第0个点开始遍历
if (lab[v] != 1) {// 如果该单元还未被访问,则递归调用深度优先算法遍历
DFS(lab, v);
}
}
}
3.2深度优先搜索算法生成迷宫
(1)首先找到初始节点A,
(2)依此从A未被访问的邻接点出发,对图进行深度优先遍历
(3)若有节点未被访问,则回溯到该节点,继续进行深度优先遍历
(4)直到所有与顶点A路径想通的节点都被访问过一次,举个例子,在下方 的无向连通图中,假设我们要从起始点A出发,使用深度优先搜索算法进行搜索,首先访问A->B->E,走不通了,回溯到A起始点,走第二个 分支节点B,路径为A->C->F->H->G->D,走不通了,再回溯到起始点A,发现所有的节点都已经走过一次了,因此本次搜索结束。
public void DFS(int[] LabG, int v) {
LabG[v] = 1;// 访问顶点
int[] neighbor = { v + row, v - row, v - 1, v + 1 };// 该点的四个邻居 上下左右
int[] offR = { 0, 0, -1, 1 }, offC = { 1, -1, 0, 0 };// Row上个方向的偏移 Column上各方向的偏移,上下左右
int[] tag = { -1, -1, -1, -1 };// 记录打通位置
int n = 0;// 打通的次数
while (n < 4) {// 上下左右四个方向都遍历,
int i = rand.nextInt(4);// 随机打通一个方向
if (tag[i] == 1)
continue;// 进入下一轮循环
tag[i] = 1;// 打通墙,设为1
n++;
int w = neighbor[i];// 定义一个该方向上的邻居
if (w > LabG.length - 1 || w < 0)
continue; // w不存在,即该方向上没有邻居
// 取出现在的v点的位置
int x = v % row;
int y = v / row;
// 遍历到四个边界时再往边界方向就没有邻居了,进入下一轮循环
if (i == 0 && y == column - 1)
continue;// 上方向
if (i == 1 && y == 0)
continue;// 下方向
if (i == 2 && x == 0)
continue;// 左方向
if (i == 3 && x == row - 1)
continue;// 右方向
// 如果该点有未访问的邻居,则把该点与其邻居间的墙打通,即相邻的格子中间的位置放1
if (LabG[w] == 0) {
LabId[2 * x + 1 + offR[i]][2 * y + 1 + offC[i]] = 1;
DFS(LabG, w);// 递归
}
}
}
A*算法寻路
1)建立开放和关闭列表
private final ArrayList<Node> openList = new ArrayList<Node>();
private final ArrayList<Node> closeList = new ArrayList<Node>();
2)把起点加入 open list
openList.add(startNode);
3)遍历 open list ,查找 F值最小的节点,把它作为当前要处理的节点
从open list中移除
把这个节点移到 close list
遍历四个邻居
如果它不在 open list 中,把它加入 open list ,并且把当前方格设置为它的父亲,记录该方格的 F , G 和 H 值。
如果它已经在 open list 中,检查这条路径 ( 即经由当前方格到达它那里 ) 是否更好,用 G 值作参考。更小的 G 值表示这是更好的路径。如果是这样,把它的父亲设置为当前方格,并重新计算它的 G 和 F 值。如果你的 open list 是按 F 值排序的话,改变后你可能需要重新排序。
当你把终点加入到了 open list 中,此时路径已经找到了,或者
查找终点失败,并且 open list 是空的,此时没有路径。
保存路径。从终点开始,每个方格沿着父节点移动直至起点,这就是你的路径
public Node findPath(Node startNode, Node endNode) {
openList.add(startNode);// 把起点加入 open list
while (openList.size() > 0) {
Node currentNode = findMinFNodeInOpneList();// 遍历 open list ,查找 F值最小的节点,把它作为当前要处理的节点
openList.remove(currentNode);// 从open list中移除
closeList.add(currentNode);// 把这个节点移到 close list
ArrayList<Node> neighborNodes = findNeighborNodes(currentNode);
for (Node node : neighborNodes) {//遍历四个邻居
if (exists(openList, node)) {
foundPoint(currentNode, node);
} else {
notFoundPoint(currentNode, endNode, node);
}
}
if (find(openList, endNode) != null) {
return find(openList, endNode);//找到终点了并返回
}
}
return find(openList, endNode);
}
四、绘制GUI
4.1绘图
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.fillRect(20, 80, 420, 420);
g.drawImage(bj, 0, 0, this);
g.setColor(Color.blue);
g.setFont(new Font("华文正楷", Font.BOLD, 40));
g.drawString("迷宫游戏", 120, 50);
// 画迷宫
for (int i = 0; i < Maez.LabId.length; i++) {
for (int j = 0; j < Maez.LabId[0].length; j++) {
if (Maez.LabId[i][j] == 0) {
g.drawImage(wall, 20 + i * 20, 80 + j * 20, this);
}
}
}
// 画A*路径
if (ans) {
g.setColor(Color.pink);
for (int i = 0; i < A.NODES.length; i++) {
for (int j = 0; j < A.NODES[0].length; j++) {
if (A.NODES[i][j] == 2) {
g.fillOval(20 + 20 * i, 80 + 20 * j, 18, 18);
}
}
}
}
g.setColor(Color.yellow);
g.fillRect(40, 100, 20, 20);// 画起点
g.fillRect(400, 460, 20, 20);// 画终点
g.drawImage(my, 20 + 20 * myx, 80 + 20 * myy, this);// 画角色
// 判断是否到达终点
if (this.myx ==19 && this.myy ==19) {
isVictory = true;
}
// 画游戏胜利界面
if (isVictory) {
g.drawImage(victory, 60,180, this);// 画角色
}
}
4.2监听
4.2.1 鼠标监听
@Override
public void mouseClicked(MouseEvent e) {
if (e.getSource().equals(answer)) {
ans = true;
isVictory=false;
}
if (e.getSource().equals(hide)) {
ans = false;
isVictory=false;
}
if (e.getSource().equals(reset)) {
ans = false;
isVictory=false;
myx = 1;
myy = 1;
new Panel();
}
if (e.getSource().equals(start)) {
ans = false;
isVictory=false;
}
repaint();
}
4.2.2键盘监听
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
int k = e.getKeyCode();
if (k == KeyEvent.VK_SPACE) {
System.out.print("按下空格");
}
if (k == KeyEvent.VK_LEFT && A.NODES[myx - 1][myy] != 0 && myx - 1 >= 1) {
myx--;
}
if (k == KeyEvent.VK_RIGHT && A.NODES[myx + 1][myy] != 0 && myx + 1 <= 21) {
myx++;
}
if (k == KeyEvent.VK_UP && A.NODES[myx][myy - 1] != 0 && myy - 1 >= 1) {
myy--;
}
if (k == KeyEvent.VK_DOWN && A.NODES[myx][myy + 1] != 0 && myy + 1 <= 21) {
myy++;
}
repaint();
}