一.项目预习:
思考过程:
迷宫由一个一个格子组成,要求从入口到出口只有一条路径.
通过树实现是比较容易的,从根节点到每一个子节点都只有一条路径。假设入口是根节点,出口是树中某个子节点,那么,从根节点到该子节点的路径肯定是唯一的。
所以构造一棵树把所有的格子都覆盖到,也就能够做出一个迷宫了。
要求树的父节点和子节点必须是界面上相邻的格子。
在界面显示时,父节点和子节点之间共用的边不画,其他的边都画出来,就能画出一个迷宫。
1.树的表示:
要实现这棵树,那么每个树节点里就要存储一个坐标(X,Y)表示一个格子,另外还要存储四个指针。指针中有的为空,有的不为空,不为空的指针指向子节点,子节点保存邻居格子的坐标。
用二维数组表示迷宫的格子。每个数组元素存储一个指向父节点的引用,这样也可以形成一个虚拟的树。于是就用一个N*N的二维数组,表示N*N个格子,每个数组元素(Lattice)中有一个指向父节点的引用(father)。另外,为了能方便的获取格子的坐标,还要保存坐标信息。
2.构造树:
选定一个格子作为根节点。。我选择确定的一个坐标作为根节点。选择随机生成一个坐标作为根节点也可以。
每次扫描在当前树中找一个节点,看它的邻居格子是否在树中,如果还没在树中,就将该邻居格子加入树中,如果已在树中,就看下一个邻居格子,如果该节点所有邻居格子都在树中了,就找下一个节点,继续同样的操作。
3.走迷宫:
选定格子作为根节点,从它开始随机地深度搜索前进,开出一条路来,直到无路可走了,退回一步,换另一条路,再走到无路可走,回退一步,换另一条……如此循环往复,直到完全无路可走。
选择一个格子根节点,将它压进栈里。
然后在栈不为空的时候执行以下循环:
取出一个格子,将它的INTREE标志设置为1,然后将它的所有不在树中的邻居格子压进栈里(顺序随机),并且让这些邻居格子的father指向该格子。
二.遇到的问题:
1.每个格子的坐标都存放在二维数组中;给每一个格子设定四个指针,存放周围未出界且连通的格子。
2.frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);frame的关闭操作有四种:
DO_NOTHING_ON_CLOSE : 不执行任何操作;要求程序处理已注册WindowListener对象的windowClosing方法中的操作。
HIDE_ON_CLOSE : 在调用任何已注册的WindowListener对象后自动隐藏帧。只关闭当前程序。
DISPOSE_ON_CLOSE : 在调用任何已注册的WindowListener对象后自动隐藏和处置帧。只关闭当前程序。
EXIT_ON_CLOSE : 使用系统退出方法退出应用程序。仅在应用程序中使用此选项。关闭由此打开的所有程序。
3.迷宫地图的形成,调用画线函数生成网格,父子节点之间的墙用画背景线覆盖。
4.周围格子的遍历函数,顺序为上右下左。在树中压栈。
三.实现相关代码:
1.开始的界面展示,可选择不同的游戏模式“简单—中等—困难”。详解见注释。
public static void main(String[] args) {
final int LX1 = 800, LY1 = 300;
JPanel pan = new JPanel();//创建容器;
pan.setBounds(20,40,240,280);//设置容器大小;
//pan.setBackground(Color.RED);
JButton pan4=new JButton("请选择难度等级");
JButton pan1=new JButton("简单");//按钮显示;
JButton pan2=new JButton("中等");
JButton pan3=new JButton("困难");
GridLayout grid2 = new GridLayout(4, 1);
pan.setLayout(grid2);//容器分割为四行,用于存放按钮。
pan.add(pan4);
pan.add(pan1);//按钮加入容器中。
pan.add(pan2);
pan.add(pan3);
Cal frame1 = new Cal();//创建Cal类的对象frame1.
frame1.setTitle("Welcome");//设置框架标题。
frame1.add(pan);//pan容器加入框架;
frame1.setLayout(null);//设置容器与框架边界有间隔。
frame1.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//默认关闭;
frame1.setSize(300, 400 );
frame1.setLocation(LX1, LY1);//frame1在屏幕的显示位置;
frame1.setVisible(true);//框架可视化;
frame1.setResizable(false);//大小不可更改;
pan1.addActionListener(frame1);
pan2.addActionListener(frame1);//按钮加入侦听;
pan3.addActionListener(frame1);
}
2.根据选择模式初始化并进入游戏,这里只列举了中等模式的初始化方法。
else if(e.getActionCommand().equals("中等")){
final int n = 30, width = 600, padding = 25, LX = 700, LY = 100;//设定大小;
JPanel p = new Maze(n, (width - padding - padding) / n, padding);//生成迷宫;
JFrame frame = new JFrame("欢迎来到迷宫游戏 中等模式 提示:(按空格键显示或隐藏路径)");
frame.getContentPane().add(p);//迷宫容器加入frame.
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
frame.setSize(width + padding, width + padding + padding);//设置frame的大小。
frame.setLocation(LX, LY);//屏幕上的显示位置;
frame.setVisible(true);//可视化;
}
3.类Lattic初始化每个节点所包含的信息,坐标,是否在树中以及父节点。
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Random;
import java.util.Stack;
import javax.swing.*;
class Lattice {
static final int INTREE = 1;
static final int NOTINTREE = 0;
private int x = -1;
private int y = -1;
private int flag = NOTINTREE;
private Lattice father = null;//根节点为LATTIC的对象,取名father;
public Lattice(int xx, int yy) {//定义格子参数;
x = xx;
y = yy;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getFlag() {
return flag;
}
public Lattice getFather() {
return father;//返回父节点
}
public void setFather(Lattice f) {
father = f;//初始化父节点
}
public void setFlag(int f) {
flag = f;
}
public String toString() {
return "(" + x + "," + y + ")\n";//节点坐标
}
}
4.给定迷宫的参数,生成迷宫。NUM是二维数组的维度,width是每个格子的宽度,padding是迷宫边界与frame边界的距离。用迷宫的每个格子出初始化二维数组。
Maze(int m, int wi, int p) {
NUM = m;
width = wi;
padding = p;
maze = new Lattice[NUM][NUM];//构建二维数组;
for (int i = 0; i <= NUM - 1; i++)
for (int j = 0; j <= NUM - 1; j++)
maze[i][j] = new Lattice(i, j);//二维数组存放每个格子的坐标;
createMaze();//生成新迷宫;
setKeyListener();
this.setFocusable(true);//路径可视化;
}
5..游戏结束的标志,当到达出口,界面显示结束游戏。
private void checkIsWin() {//从左上角走到右下角为结束;
if (ballX == NUM - 1 && ballY == NUM - 1) {
JOptionPane.showMessageDialog(null, "YOU WIN !", "你走出了迷宫。",
JOptionPane.PLAIN_MESSAGE);
init();//重新生成迷宫。
}
}
6游戏完成后的更新部分,二维数组清空,生成根节点回复起始位置。重置标志位,重新生成迷宫 。
private void init() {//结束后游戏更新;
for (int i = 0; i <= NUM - 1; i++)
for (int j = 0; j <= NUM - 1; j++) {
maze[i][j].setFather(null);//清空二维数组
maze[i][j].setFlag(Lattice.NOTINTREE);//重置标志位;
}
ballX = 0;//恢复起始位置。
ballY = 0;
drawPath = false;
createMaze();//生成新迷宫;
this.setFocusable(true);
repaint();
}
7.网格的形成,在容器内画横竖线形成网格;
super.paintComponent(g);
for (int i = 0; i <= NUM; i++) {//从点(x1,y1)到点(x2,y2)之间画竖线。
g.setColor(Color.RED);
g.drawLine(padding + i * width, padding, padding + i * width,
padding + NUM * width);
}
for (int j = 0; j <= NUM; j++) {//从点(x1,y1)到点(x2,y2)之间画横线。
g.setColor(Color.RED);
g.drawLine(padding, padding + j * width, padding + NUM * width,
padding + j * width);
}
8.构造方法用背景色画线覆盖父子节点之间的墙,形成通路,具体方法如下
private void clearFence(int i, int j, int fx, int fy, Graphics g) {//构造迷宫;
int sx = padding + ((Math.max(j, fy)) * width),
sy = padding + ((Math.max(i, fx)) * width),
dx = (i == fx ?sx :sx+ width ),
dy = (i == fx ?sy + width :sy );
if (sx != dx) {
sx++;
dx--;
} else {
sy++;
dy--;
}
g.drawLine(sx, sy, dx, dy);//用背景色覆盖父子节点之间的墙形成通路;
}
9. 形成迷宫的过程,其实就是用上面方法不停覆盖相关墙的过程:
for (int i = NUM - 1; i >= 0; i--) {
for (int j = NUM - 1; j >= 0; j--) {
Lattice f = maze[i][j].getFather();//获得父节点;
if (f != null) {
int fx = f.getX(), fy = f.getY();//获得父节点的坐标;
clearFence(i, j, fx, fy, g);//掩盖墙形成通路;
}
}
}
10.寻找并显示通路,从第一个点开始,直到父节点不为空,一直往下找。
private void drawPath(Graphics g) {
if (drawPath)
g.setColor(Color.blue);
else
g.setColor(this.getBackground());
Lattice p = maze[NUM - 1][NUM - 1];//出口格子为对象;
g.setColor(Color.blue);
g.fillOval(getCenterX(p) - width / 3, getCenterY(p) - width / 3,
width / 2, width / 2);//设置末尾圆点位置;
p = maze[0][0];
while (p.getFather() != null) {
g.drawLine(getCenterX(p), getCenterY(p), getCenterX(p.getFather()),
getCenterY(p.getFather()));//画出父子节点之间的通路;
p = p.getFather();
}
}
11. 键盘操作物体走迷宫,根据空格键显示通路,检测不出界且为父子节点才能更换位置:
synchronized private void move(int c) {//控制物体移动;
int tx = ballX, ty = ballY;
switch (c) {
case KeyEvent.VK_LEFT :
ty--;
break;
case KeyEvent.VK_RIGHT :
ty++;
break;
case KeyEvent.VK_UP :
tx--;
break;
case KeyEvent.VK_DOWN :
tx++;
break;
case KeyEvent.VK_SPACE ://按下空格键
drawPath = !drawPath;//路径消失和路径出现;
break;
default :
}
if (!isOutOfBorder(tx, ty)
&& (maze[tx][ty].getFather() == maze[ballX][ballY]
|| maze[ballX][ballY].getFather() == maze[tx][ty])) {
ballX = tx;
ballY = ty;//坐标未出界且为父子节点,格子才能移动。
}
}
12.检测是否出界
private boolean isOutOfBorder(Lattice p) {
return isOutOfBorder(p.getX(), p.getY());//检测是否出界;
}
private boolean isOutOfBorder(int x, int y) {
return x > NUM - 1 || y > NUM - 1 || x < 0 || y < 0;//出界返回true;
}
13.键盘监听:
private void setKeyListener() {//操作监听器;
this.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
int c = e.getKeyCode();
move(c);//按下按钮做出反应;
repaint();
checkIsWin();
}
});
}
14.生成树的过程,将根节点压入栈中,依次遍历它的子节点,并压入栈中。如此重复,找完所有的节点。:
private void createMaze() {
Random random = new Random();
int rx = NUM-1;//给定根节点位置;
int ry = NUM-1;
Stack<Lattice> s = new Stack<>();
Lattice p = maze[rx][ry];
Lattice[] neis;
s.push(p);//根节点压入栈中;
while (!s.isEmpty()) {//如果栈不是空;
p = s.pop();//推出栈顶;
p.setFlag(Lattice.INTREE);//标志在栈内。
neis = getNeis(p);//遍历周围的格子;放进数组;
int ran = Math.abs(random.nextInt());//随机生成一个数;
for (int a = 0; a <= 3; a++) {//生成树;
ran++;
ran %= 4;//ran的值永远是0,1,2,3。
assert neis != null;//断言neis不为空。
if (neis[ran] == null || neis[ran].getFlag() == Lattice.INTREE)//节点附近的节点有出界或者在树内;
continue;
s.push(neis[ran]);//否则压入栈;
neis[ran].setFather(p);//记录父节点;
}
}
}
15. 对每个节点的周围节点的遍历,四个指针数组,存放父节点的信息:
private Lattice[] getNeis(Lattice p) {
final int[] adds = {-1, 0, 1, 0, -1};// 顺序为上右下左
if (isOutOfBorder(p)) {//如果格子出界;
return null;
}
Lattice[] ps = new Lattice[4];//顺序为上右下左
int xt;
int yt;
for (int i = 0; i <= 3; i++) {
xt = p.getX() + adds[i];
yt = p.getY() + adds[i + 1];
if (isOutOfBorder(xt, yt))//检测周围的四个邻居格子都不出界;
continue;
ps[i] = maze[xt][yt];//四个格子放入数组;
}
return ps;
}
四 源代码
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Random;
import java.util.Stack;
import javax.swing.*;
class Lattice {
static final int INTREE = 1;
static final int NOTINTREE = 0;
private int x = -1; // 格子的位置,在第几行
private int y = -1; // 第几列
private int flag = NOTINTREE; // flag,标识格子是否已加入树中
private Lattice father = null; // 格子的父亲节点
public Lattice(int xx, int yy) {
x = xx;
y = yy;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getFlag() {
return flag;
}
public Lattice getFather() {
return father;
}
public void setFather(Lattice f) {
father = f;
}
public void setFlag(int f) {
flag = f;
}
public String toString() {
return new String("(" + x + "," + y + ")\n");
}
}
class Maze extends JPanel {
//private static final long serialVersionUID = -8300339045454852626L;
private final int NUM;//num是数组和地图的边长,
private final int width;//width 每个格子的宽度和高度;
private final int padding;//地图面板的大小;
private final Lattice[][] maze;//lattic类的数组maze,用于保存每个格子;
private int ballX, ballY;//起始位置坐标;
private boolean drawPath = false; // flag,标识是否画出路径
Maze(int m, int wi, int p) {
NUM = m;
width = wi;
padding = p;
maze = new Lattice[NUM][NUM];
for (int i = 0; i <= NUM - 1; i++)
for (int j = 0; j <= NUM - 1; j++)
maze[i][j] = new Lattice(i, j);
createMaze();
setKeyListener();
this.setFocusable(true);
}
// 初始化游戏,重开一局时使用
private void init() {
for (int i = 0; i <= NUM - 1; i++)
for (int j = 0; j <= NUM - 1; j++) {
maze[i][j].setFather(null);
maze[i][j].setFlag(Lattice.NOTINTREE);
}
ballX = 0;
ballY = 0;
drawPath = false;
createMaze();
// setKeyListener();
this.setFocusable(true);
repaint();
}
// 由格子的行数,得到格子中心点的像素X座标
public int getCenterX(int x) {
return padding + x * width + width / 2;
}
// 由格子的列数,得到格子中心点的像素Y座标
public int getCenterY(int y) {
return padding + y * width + width / 2;
}
public int getCenterX(Lattice p) {
return padding + p.getY() * width + width / 2;
}
public int getCenterY(Lattice p) {
return padding + p.getX() * width + width / 2;
}
// 检查是否到达最后一个格子,若是则走出了迷宫,重开一局游戏
private void checkIsWin() {
if (ballX == NUM - 1 && ballY == NUM - 1) {
JOptionPane.showMessageDialog(null, "你赢了 !", "你走出了迷宫。",
JOptionPane.PLAIN_MESSAGE);
init();
}
}
// 移动小球,c为按键码
synchronized private void move(int c) {
int tx = ballX, ty = ballY;
// System.out.println(c);
switch (c) {
case KeyEvent.VK_LEFT :
ty--;
break;
case KeyEvent.VK_RIGHT :
ty++;
break;
case KeyEvent.VK_UP :
tx--;
break;
case KeyEvent.VK_DOWN :
tx++;
break;
case KeyEvent.VK_SPACE :
drawPath = !drawPath;
break;
default :
}
// 若移动后未出界且格子之间有路径,则进行移动,更新小球位置,否则移动非法
if (!isOutOfBorder(tx, ty)
&& (maze[tx][ty].getFather() == maze[ballX][ballY]
|| maze[ballX][ballY].getFather() == maze[tx][ty])) {
ballX = tx;
ballY = ty;
}
}
private void setKeyListener() {
this.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
int c = e.getKeyCode();
move(c);
repaint();
checkIsWin();
}
});
}
// 是否出界
private boolean isOutOfBorder(Lattice p) {
return isOutOfBorder(p.getX(), p.getY());
}
private boolean isOutOfBorder(int x, int y) {
return x > NUM - 1 || y > NUM - 1 || x < 0 || y < 0;
}
// 获取格子的邻居格子
private Lattice[] getNeis(Lattice p) {
final int[] adds = {-1, 0, 1, 0, -1};
if (isOutOfBorder(p)) {
return null;
}
Lattice[] ps = new Lattice[4]; // 四个邻居格子,顺序为上右下左,出界的邻居为null
int xt;
int yt;
for (int i = 0; i <= 3; i++) {
xt = p.getX() + adds[i];
yt = p.getY() + adds[i + 1];
if (isOutOfBorder(xt, yt))
continue;
ps[i] = maze[xt][yt];
}
return ps;
}
// 构建随机树,创建迷宫
private void createMaze() {
// 随机选一个格子作为树的根
Random random = new Random();
int rx = NUM-1;
int ry = NUM-1;
// 深度优先遍历
Stack<Lattice> s = new Stack<Lattice>();
Lattice p = maze[rx][ry];
Lattice[] neis = null;
s.push(p);//根节点压入栈中;
while (!s.isEmpty()) {//如果栈不是空;
p = s.pop();//推出栈顶;
p.setFlag(Lattice.INTREE);//标志在栈内。
neis = getNeis(p);//遍历周围的格子;放进数组;
int ran = Math.abs(random.nextInt());//随机生成一个数;
for (int a = 0; a <= 3; a++) {//生成树;
ran++;
ran %= 4;//ran的值永远是0,1,2,3。
assert neis != null;
if (neis[ran] == null || neis[ran].getFlag() == Lattice.INTREE)//节点附近的节点有出界或者在树内;
continue;
s.push(neis[ran]);//否则压入栈;
neis[ran].setFather(p);//记录父节点;
}
}
}
// 抹掉两个格子之间的边
private void clearFence(int i, int j, int fx, int fy, Graphics g) {
int sx = padding + ((Math.max(j, fy)) * width),
sy = padding + ((Math.max(i, fx)) * width),
dx = (i == fx ? sx : sx + width),
dy = (i == fx ? sy + width : sy);
if (sx != dx) {
sx++;
dx--;
} else {
sy++;
dy--;
}
g.drawLine(sx, sy, dx, dy);
}
protected void paintComponent(Graphics g) {//画网格
super.paintComponent(g);
g.setColor(Color.blue);
for (int i = 0; i <= NUM; i++) {//画横线
g.drawLine(padding + i * width, padding, padding + i * width,
padding + NUM * width);
}
for (int j = 0; j <= NUM; j++) {//画竖线
g.drawLine(padding, padding + j * width, padding + NUM * width,
padding + j * width);
}
g.setColor(this.getBackground());//用背景色覆盖
for (int i = NUM - 1; i >= 0; i--) {
for (int j = NUM - 1; j >= 0; j--) {
Lattice f = maze[i][j].getFather();
if (f != null) {
int fx = f.getX(), fy = f.getY();
clearFence(i, j, fx, fy, g);
}
}
}
g.drawLine(padding, padding + 1, padding, padding + width - 1);
int last = padding + NUM * width;
g.drawLine(last, last - 1, last, last - width + 1);
g.setColor(Color.RED);
g.fillOval(getCenterX(ballY) - width / 3, getCenterY(ballX) - width / 3,
width / 2, width / 2);
if (drawPath) drawPath(g);
}
private void drawPath(Graphics g) {
if (drawPath)
g.setColor(Color.RED);
Lattice p = maze[0][0];//入口格子为对象
while (p.getFather() != null) {
g.drawLine(getCenterX(p), getCenterY(p), getCenterX(p.getFather()),
getCenterY(p.getFather()));
p = p.getFather();
}
g.fillOval(getCenterX(p) - width / 3, getCenterY(p) - width / 3,
width / 2, width / 2);//设置末尾圆点位置;
p = maze[NUM - 1][NUM - 1];//出口格子为对象
while (p.getFather() != null) {
if (p.getFlag() == 3)
break;
g.drawLine(getCenterX(p), getCenterY(p), getCenterX(p.getFather()),
getCenterY(p.getFather()));//画出父子节点之间的通路;
p = p.getFather();
}
}
}
class Cal extends JFrame implements ActionListener{
public static void main(String[] args) {
final int LX1 = 800, LY1 = 300;
JPanel pan = new JPanel();//创建容器;
pan.setBounds(20,40,240,280);
JButton pan4=new JButton("请选择难度等级");
JButton pan1=new JButton("简单");//按钮显示;
JButton pan2=new JButton("中等");
JButton pan3=new JButton("困难");
GridLayout grid2 = new GridLayout(4, 1);
pan.setLayout(grid2);//容器分割为四行,用于存放按钮。
pan.add(pan4);
pan.add(pan1);//按钮加入容器中。
pan.add(pan2);
pan.add(pan3);
Cal frame1 = new Cal();//创建Cal类的对象frame1.
frame1.setTitle("Welcome");//设置框架标题。
frame1.add(pan);//pan容器加入框架;
frame1.setLayout(null);//设置容器与框架边界有间隔。
frame1.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//默认关闭;
frame1.setSize(300, 400 );
frame1.setLocation(LX1, LY1);//frame1在屏幕的显示位置;
frame1.setVisible(true);//框架可视化;
frame1.setResizable(false);//大小不可更改;
pan1.addActionListener(frame1);
pan2.addActionListener(frame1);//按钮加入侦听;
pan3.addActionListener(frame1);
}
@Override
public void actionPerformed(ActionEvent e) {
if(e.getActionCommand().equals("简单")) {
final int n = 20, width = 600, padding = 30, LX = 700, LY = 100;
JPanel p = new Maze(n, (width - padding - padding) / n, padding);
JFrame frame = new JFrame("欢迎来到迷宫游戏 简单模式 提示:(按空格键显示或隐藏路径)");
frame.getContentPane().add(p);
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
frame.setSize(width + padding, width + padding + padding);
frame.setLocation(LX, LY);
frame.setVisible(true);
frame.setResizable(false);
}
if(e.getActionCommand().equals("中等")) {
final int n = 30, width = 600, padding = 25, LX = 700, LY = 100;//设定大小;
JPanel p = new Maze(n, (width - padding - padding) / n, padding);//生成迷宫;
JFrame frame = new JFrame("欢迎来到迷宫游戏 中等模式 提示:(按空格键显示或隐藏路径)");
frame.getContentPane().add(p);//迷宫容器加入frame.
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
frame.setSize(width + padding, width + padding + padding);//设置frame的大小。
frame.setLocation(LX, LY);//屏幕上的显示位置;
frame.setVisible(true);//可视化;
frame.setResizable(false);
}
if(e.getActionCommand().equals("困难")) {
final int n = 50, width = 800, padding = 20, LX = 400, LY = 50;
JPanel p = new Maze(n, (width - padding - padding) / n, padding);
JFrame frame = new JFrame("欢迎来到迷宫游戏 困哪模式 提示:(按空格键显示或隐藏路径)");
frame.getContentPane().add(p);
frame.setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
frame.setSize(width + padding, width + padding + padding);
frame.setLocation(LX, LY);
frame.setVisible(true);
frame.setResizable(false);
}
}
}
执行结果: