作者简介
作者名:编程界明世隐
简介:CSDN博客专家,从事软件开发多年,精通Java、JavaScript,博主也是从零开始一步步把学习成长、深知学习和积累的重要性,喜欢跟广大ADC一起打野升级,欢迎您关注,期待与您一起学习、成长、起飞!
系列目录
1. Java俄罗斯方块
2. Java五子棋小游戏
3. 老Java程序员花一天时间写了个飞机大战
4. Java植物大战僵尸
5. 老Java程序员花2天写了个连连看
6. Java消消乐(天天爱消除)
7. Java贪吃蛇小游戏
8. Java扫雷小游戏
9. Java坦克大战
效果图
实现思路
1.创建运行窗口。 2.创建菜单。 3.绘制迷宫的每个单元。 4.通过算法计算迷宫路径,并打通路径,形成迷宫。 5.绘制起点终点。 6.添加键盘事件控制起点方块移动。 7.收尾。
迷宫算法(网上参考的)
- 将起点作为当前迷宫单元并标记为已访问
- 当还存在未标记的迷宫单元,进行循环
1).如果当前迷宫单元有未被访问过的的相邻的迷宫单元
(1).随机选择一个未访问的相邻迷宫单元
(2).将当前迷宫单元入栈
(3).移除当前迷宫单元与相邻迷宫单元的墙
(4).标记相邻迷宫单元并用它作为当前迷宫单元
2).如果当前迷宫单元不存在未访问的相邻迷宫单元,并且栈不空
(1).栈顶的迷宫单元出栈
(2).令其成为当前迷宫单元
这个算法叫做“深度优先”,简单来说,就是从起点开始走,寻找它的上下左右4个邻居,然后随机一个走,到走不通的时候就返回上一步继续走,直到全部单元都走完。
相关图示说明
- 每个单元的墙,分为上墙、右墙、下墙、左墙,把这些墙用长度为4的数组表示,元素的值为true则表示墙存在,否则墙不存在,代码里数组的下标方式来确定墙是否存在。
- 单元是根据行列来创建的,会用到双循环,类似表格,比如第二行用 i 来表示的话就是 1,第3列用 j 来表示就是2,那第二行第3列的元素组合起来就是(1,2)
- 那同理它的上邻居就是(0,2),右邻居(1,3),下邻居(2,2),左邻居(1,1),也就是上下邻居是 i 减加1,左右邻居是 j 减加1。
- 正方形4个点的坐标分别为(x1,y1)(x2,y2)(x3,y3)(x4,y4),计算坐标的公式为(其中start为相对偏移量,为了让迷宫两边有些空隙):
//i代表行 j代表列 h为单元高度
//左上角坐标
this.x1=start+j*h;
this.y1=start+i*h;
//右上角坐标
this.x2=start+(j+1)*h;
this.y2=start+i*h;
//右下角坐标
this.x3=start+(j+1)*h;
this.y3=start+(i+1)*h;
//左下角坐标
this.x4=start+j*h;
this.y4=start+(i+1)*h;
计算坐标,假如每个正方形的宽高都是40,那么(1,2)这个单元的坐标如下图:
5. 墙的处理,之前说到墙是以一个4个元素的数组来表示的,比如数组为:[true,true,true,true],则图为:
如果数组为[false,true,true,true],则图为:
6. 如果要联通右边的邻居要怎么做呢?当前单元去除右墙,右边单元去除左墙,这样就联通了。
去除后就这样,以此类推
代码实现
创建窗口
首先创建一个游戏窗体类GameFrame,继承至JFrame,用来显示在屏幕上(window的对象),每个游戏都有一个窗口,设置好窗口标题、尺寸、布局等就可以。
import javax.swing.JFrame;
/**
*窗体类
*/
public class GameFrame extends JFrame {
//构造方法
public GameFrame(){
setTitle("迷宫");//设置标题
setSize(420, 470);//设置窗体大小
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//关闭后进程退出
setLocationRelativeTo(null);//居中
setResizable(false);//不允许变大
//setVisible(true);//设置显示窗体
}
}
创建面板容器GamePanel继承至JPanel
import javax.swing.JMenuBar;
import javax.swing.JPanel;
/*
* 画布类
*/
public class GamePanel extends JPanel{
private JMenuBar jmb = null;
private GameFrame mainFrame = null;
private GamePanel panel = null;
private String gameFlag="start";//游戏状态
//构造方法
public GamePanel(GameFrame mainFrame){
this.setLayout(null);
this.setOpaque(false);
this.mainFrame=mainFrame;
this.panel =this;
}
}
再创建一个Main类,来启动这个窗口。
//Main类
public class Main {
public static void main(String[] args) {
GameFrame frame = new GameFrame();
GamePanel panel = new GamePanel(frame);
frame.add(panel);
frame.setVisible(true);
}
}
右键执行这个Main类,窗口建出来了
创建菜单及菜单选项
创建菜单
private Font createFont(){
return new Font("思源宋体",Font.BOLD,18);
}
//创建菜单
private void createMenu() {
//创建JMenuBar
jmb = new JMenuBar();
//取得字体
Font tFont = createFont();
//创建游戏选项
JMenu jMenu1 = new JMenu("游戏");
jMenu1.setFont(tFont);
//创建帮助选项
JMenu jMenu2 = new JMenu("帮助");
jMenu2.setFont(tFont);
JMenuItem jmi1 = new JMenuItem("新游戏");
jmi1.setFont(tFont);
JMenuItem jmi2 = new JMenuItem("退出");
jmi2.setFont(tFont);
//jmi1 jmi2添加到菜单项“游戏”中
jMenu1.add(jmi1);
jMenu1.add(jmi2);
JMenuItem jmi3 = new JMenuItem("操作帮助");
jmi3.setFont(tFont);
JMenuItem jmi4 = new JMenuItem("胜利条件");
jmi4.setFont(tFont);
//jmi13 jmi4添加到菜单项“游戏”中
jMenu2.add(jmi3);
jMenu2.add(jmi4);
jmb.add(jMenu1);
jmb.add(jMenu2);
mainFrame.setJMenuBar(jmb);
//添加监听
jmi1.addActionListener(this);
jmi2.addActionListener(this);
jmi3.addActionListener(this);
jmi4.addActionListener(this);
//设置指令
jmi1.setActionCommand("restart");
jmi2.setActionCommand("exit");
jmi3.setActionCommand("help");
jmi4.setActionCommand("win");
}
实现ActionListener并重写方法actionPerformed 此时GamePanel是报错的,需重写actionPerformed方法。 ==actionPerformed方法的实现==
@Override
public void actionPerformed(ActionEvent e) {
String command = e.getActionCommand();
System.out.println(command);
UIManager.put("OptionPane.buttonFont", new FontUIResource(new Font("思源宋体", Font.ITALIC, 18)));
UIManager.put("OptionPane.messageFont", new FontUIResource(new Font("思源宋体", Font.ITALIC, 18)));
if ("exit".equals(command)) {
Object[] options = { "确定", "取消" };
int response = JOptionPane.showOptionDialog(this, "您确认要退出吗", "",
JOptionPane.YES_OPTION, JOptionPane.QUESTION_MESSAGE, null,
options, options[0]);
if (response == 0) {
System.exit(0);
}
}else if("restart".equals(command)){
restart();
}else if("help".equals(command)){
JOptionPane.showMessageDialog(null, "通过键盘的上下左右来移动",
"提示!", JOptionPane.INFORMATION_MESSAGE);
}else if("win".equals(command)){
JOptionPane.showMessageDialog(null, "移动到终点获得胜利",
"提示!", JOptionPane.INFORMATION_MESSAGE);
}
}
此时的GamePanel代码如下:
package main;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.plaf.FontUIResource;
/*
* 画布类
*/
public class GamePanel extends JPanel implements ActionListener{
private JMenuBar jmb = null;
private GameFrame mainFrame = null;
private GamePanel panel = null;
private String gameFlag="start";//游戏状态
//构造方法
public GamePanel(GameFrame mainFrame){
this.setLayout(null);
this.setOpaque(false);
this.mainFrame=mainFrame;
this.panel =this;
//创建菜单
createMenu();
}
private Font createFont(){
return new Font("思源宋体",Font.BOLD,18);
}
//创建菜单
private void createMenu() {
//创建JMenuBar
jmb = new JMenuBar();
//取得字体
Font tFont = createFont();
//创建游戏选项
JMenu jMenu1 = new JMenu("游戏");
jMenu1.setFont(tFont);
//创建帮助选项
JMenu jMenu2 = new JMenu("帮助");
jMenu2.setFont(tFont);
JMenuItem jmi1 = new JMenuItem("新游戏");
jmi1.setFont(tFont);
JMenuItem jmi2 = new JMenuItem("退出");
jmi2.setFont(tFont);
//jmi1 jmi2添加到菜单项“游戏”中
jMenu1.add(jmi1);
jMenu1.add(jmi2);
JMenuItem jmi3 = new JMenuItem("操作帮助");
jmi3.setFont(tFont);
JMenuItem jmi4 = new JMenuItem("胜利条件");
jmi4.setFont(tFont);
//jmi13 jmi4添加到菜单项“游戏”中
jMenu2.add(jmi3);
jMenu2.add(jmi4);
jmb.add(jMenu1);
jmb.add(jMenu2);
mainFrame.setJMenuBar(jmb);
//添加监听
jmi1.addActionListener(this);
jmi2.addActionListener(this);
jmi3.addActionListener(this);
jmi4.addActionListener(this);
//设置指令
jmi1.setActionCommand("restart");
jmi2.setActionCommand("exit");
jmi3.setActionCommand("help");
jmi4.setActionCommand("win");
}
@Override
public void actionPerformed(ActionEvent e) {
String command = e.getActionCommand();
System.out.println(command);
UIManager.put("OptionPane.buttonFont", new FontUIResource(new Font("思源宋体", Font.ITALIC, 18)));
UIManager.put("OptionPane.messageFont", new FontUIResource(new Font("思源宋体", Font.ITALIC, 18)));
if ("exit".equals(command)) {
Object[] options = { "确定", "取消" };
int response = JOptionPane.showOptionDialog(this, "您确认要退出吗", "",
JOptionPane.YES_OPTION, JOptionPane.QUESTION_MESSAGE, null,
options, options[0]);
if (response == 0) {
System.exit(0);
}
}else if("restart".equals(command)){
restart();
}else if("help".equals(command)){
JOptionPane.showMessageDialog(null, "通过键盘的上下左右来移动",
"提示!", JOptionPane.INFORMATION_MESSAGE);
}else if("win".equals(command)){
JOptionPane.showMessageDialog(null, "移动到终点获得胜利",
"提示!", JOptionPane.INFORMATION_MESSAGE);
}
}
void restart(){
}
}
运行它
绘制迷宫的每个单元
- 初始化相关参数
public final int ROWS=20;//行
public final int COLS=20;//列
public final int H=20;//每一块的宽高
Block[][] blocks = null;//存储每个单元的二维数组
- 创建迷宫单元类(如果对坐标计算不明白,可以往上翻,有图示说明解释)
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.List;
/*
* 迷宫单元类
*/
public class Block {
private GamePanel panel = null;
private int i=0;//二维数组的下标i
private int j=0;//二维数组的下标j
private int h=0;//宽高
private int start=6;//偏移像素
//4个顶点坐标
private int x1=0;//x1坐标
private int y1=0;//y1坐标
private int x2=0;//x2坐标
private int y2=0;//y2坐标
private int x3=0;//x3坐标
private int y3=0;//y3坐标
private int x4=0;//x4坐标
private int y4=0;//y4坐标
//上下左右4个墙是否显示,true:显示,false:隐藏
boolean[] walls=new boolean[4];
private boolean visited=false;//是否被访问
//构造
public Block(int i,int j,int h,GamePanel panel){
this.i=i;
this.j=j;
this.h=h;
this.panel=panel;
//计算4个顶点的坐标
init();
}
//计算4个顶点的坐标
private void init() {
//i代表行 j代表列
//左上角坐标
this.x1=start+j*h;
this.y1=start+i*h;
//右上角坐标
this.x2=start+(j+1)*h;
this.y2=start+i*h;
//右下角坐标
this.x3=start+(j+1)*h;
this.y3=start+(i+1)*h;
//左下角坐标
this.x4=start+j*h;
this.y4=start+(i+1)*h;
//默认上下左右4个墙都显示
walls[0]=true;
walls[1]=true;
walls[2]=true;
walls[3]=true;
}
//绘制指示器的方法
public void draw(Graphics g) {
//绘制迷宫块
drawBlock(g);
}
//绘制迷宫块
private void drawBlock(Graphics g) {
//判断上、右、下、左 的墙,true的话墙就会有,否则墙就没有
boolean top = walls[0];
boolean right = walls[1];
boolean bottom = walls[2];
boolean left = walls[3];
if(top){//绘制上墙
g.drawLine(x1, y1, x2, y2);
}
if(right){//绘制右墙
g.drawLine(x2, y2, x3, y3);
}
if(bottom){//绘制下墙
g.drawLine(x3, y3, x4, y4);
}
if(left){//绘制左墙
g.drawLine(x4, y4, x1, y1);
}
}
}
- 在GamePanel类中创建方法createBlocks
//创建数组内容
private void createBlocks() {
blocks = new Block[ROWS][COLS];
Block block ;
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
block = new Block(i, j,H,this);
blocks[i][j]=block;
}
}
}
- 在构造函数中调用此方法
//构造方法
public GamePanel(GameFrame mainFrame){
this.setLayout(null);
this.setOpaque(false);
this.mainFrame=mainFrame;
this.panel =this;
//创建菜单
createMenu();
//创建数组内容
createBlocks();
}
- 在GamePanel中重新paint方法,绘制这些方块
public void paint(Graphics g) {
super.paint(g);
//绘制网格
drawBlock(g);
}
//绘制迷宫块
private void drawBlock(Graphics g) {
Block block ;
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
block = blocks[i][j];
if(block!=null){
block.draw(g);
}
}
}
}
运行可以看到一个个的方形绘制出来了
计算并打通迷宫
- 给每个单元都增加邻居查找方法(Block类中)
//查找当前单元是否有未被访问的邻居单元
public List<Block> findNeighbors() {
//邻居分为上下左右
List<Block> res= new ArrayList<Block>();//返回的数组
Block top = this.getNeighbor(0,false);
Block right = this.getNeighbor(1,false);
Block bottom = this.getNeighbor(2,false);
Block left = this.getNeighbor(3,false);
if(top!=null){
res.add(top);
}
if(right!=null){
res.add(right);
}
if(bottom!=null){
res.add(bottom);
}
if(left!=null){
res.add(left);
}
return res;//返回邻居数组
}
//根据方向,获得邻居
public Block getNeighbor(int type,boolean lose_visited) {
Block neighbor;
int ti=0,tj=0;
if(type==0){
ti = this.i-1;
tj = this.j;
}else if(type==1){
ti = this.i;
tj = this.j+1;
}else if(type==2){
ti = this.i+1;
tj = this.j;
}else if(type==3){
ti = this.i;
tj = this.j-1;
}
Block[][] blocks = panel.blocks;
if(ti<0 || tj<0 || ti>=panel.ROWS || tj>=panel.COLS){//超出边界了
neighbor = null;
}else{
//首先找到这个邻居
neighbor = blocks[ti][tj];
//判断是否被访问,如果被访问了返回null
if(neighbor.visited && !lose_visited){//lose_visited等于true表示忽略访问
neighbor = null;
}
}
return neighbor;
}
- 计算 跟着算法来写的代码,唯一要注意的是我设置了一个值unVisitedCount,初始值为所有单元的数量,每当一个单元被标记为已访问后,这个值就递减1,当值为0后就终止循环,结束算法。
//线路的计算处理
private void computed(){
/*
1.将起点作为当前迷宫单元并标记为已访问
2.当还存在未标记的迷宫单元,进行循环
1).如果当前迷宫单元有未被访问过的的相邻的迷宫单元
(1).随机选择一个未访问的相邻迷宫单元
(2).将当前迷宫单元入栈
(3).移除当前迷宫单元与相邻迷宫单元的墙
(4).标记相邻迷宫单元并用它作为当前迷宫单元
2).如果当前迷宫单元不存在未访问的相邻迷宫单元,并且栈不空
(1).栈顶的迷宫单元出栈
(2).令其成为当前迷宫单元
*/
Random random = new Random();
Stack<Block> stack = new Stack<Block>();//栈
Block current = blocks[0][0];//取第一个为当前单元
current.setVisited(true);//标记为已访问
int unVisitedCount=ROWS*COLS-1;//因为第一个已经设置为访问了
List<Block> neighbors ;//定义邻居
Block next;
while(unVisitedCount>0){
neighbors = current.findNeighbors();//查找邻居集合(未被访问的)
if(neighbors.size()>0){//如果当前迷宫单元有未被访问过的的相邻的迷宫单元
//随机选择一个未访问的相邻迷宫单元
int index = random.nextInt(neighbors.size());
next = neighbors.get(index);
//将当前迷宫单元入栈
stack.push(current);
//移除当前迷宫单元与相邻迷宫单元的墙
this.removeWall(current,next);
//标记相邻迷宫单元并用它作为当前迷宫单元
next.setVisited(true);
//标记一个为访问,则计数器递减1
unVisitedCount--;//递减
current = next;
}else if(!stack.isEmpty()){//如果当前迷宫单元不存在未访问的相邻迷宫单元,并且栈不空
/*
1.栈顶的迷宫单元出栈
2.令其成为当前迷宫单元
*/
Block cell = stack.pop();
current = cell;
}
}
}
- 移除墙
//移除当前迷宫单元与相邻迷宫单元的墙
private void removeWall(Block current, Block next) {
if(current.getI()==next.getI()){//横向邻居
if(current.getJ()>next.getJ()){//匹配到的是左边邻居
//左边邻居的话,要移除自己的左墙和邻居的右墙
current.walls[3]=false;
next.walls[1]=false;
}else{//匹配到的是右边邻居
//右边邻居的话,要移除自己的右墙和邻居的左墙
current.walls[1]=false;
next.walls[3]=false;
}
}else if(current.getJ()==next.getJ()){//纵向邻居
if(current.getI()>next.getI()){//匹配到的是上边邻居
//上边邻居的话,要移除自己的上墙和邻居的下墙
current.walls[0]=false;
next.walls[2]=false;
}else{//匹配到的是下边邻居
//下边邻居的话,要移除自己的下墙和邻居的上墙
current.walls[2]=false;
next.walls[0]=false;
}
}
}
- 在构造函数中调用computed方法
//构造方法
public GamePanel(GameFrame mainFrame){
this.setLayout(null);
this.setOpaque(false);
this.mainFrame=mainFrame;
this.panel =this;
//创建菜单
createMenu();
//创建数组内容
createBlocks();
//计算处理线路
computed();
}
- 运行效果
绘制起点终点
- 创建Rect类
package main;
import java.awt.Color;
import java.awt.Graphics;
//开始结束方块
public class Rect {
private int i=0;//二维数组的下标i
private int j=0;//二维数组的下标j
private int x=0;//x坐标
private int y=0;//y坐标
private int h=0;//宽高
private int start=6;//偏移像素
private String type="";//start end
public Rect(int i,int j,int h,String type){
this.i=i;
this.j=j;
this.h=h;
this.type=type;
}
//初始化
private void init() {
this.x=start+j*h+2;
this.y=start+i*h+2;
}
//绘制
void draw(Graphics g){
//计算x、y坐标
init();
Color oColor = g.getColor();
if("start".equals(type)){//红色
g.setColor(Color.red);
}else{
g.setColor(Color.blue);
}
g.fillRect(x, y, h-3, h-3);
g.setColor(oColor);
}
//移动
public void move(int type, Block[][] blocks,GamePanel panel) {
//根据当前start方形,获得对应的迷宫单元
Block cur = blocks[this.i][this.j];
boolean wall = cur.walls[type];//得到对应的那面墙
if(!wall){
//得到移动方块对应的单元
Block next = cur.getNeighbor(type,true);
if(next!=null){
this.i = next.getI();
this.j = next.getJ();
panel.repaint();
//判断如果i,j等于终点的,则表示获得成功
if(this.i==panel.end.i && this.j==panel.end.j){
panel.gameWin();
}
}
}
}
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
public int getJ() {
return j;
}
public void setJ(int j) {
this.j = j;
}
}
- 在GamePanel类中创建方法,并在构造中调用。
//创建开始结束的方形
private void createRects() {
start = new Rect(0, 0, H, "start") ;
end = new Rect(ROWS-1, COLS-1, H, "end") ;
}
//构造方法
public GamePanel(GameFrame mainFrame){
this.setLayout(null);
this.setOpaque(false);
this.mainFrame=mainFrame;
this.panel =this;
//创建菜单
createMenu();
//创建数组内容
createBlocks();
//计算处理线路
computed();
//创建开始结束的方形
createRects();
}
- 在paint方法中绘制
@Override
public void paint(Graphics g) {
super.paint(g);
//绘制网格
drawBlock(g);
//绘制开始结束方向
drawRect(g);
}
//绘制开始结束方块
private void drawRect(Graphics g) {
end.draw(g);
start.draw(g);
}
- 运行一下
加入键盘移动监听
- 创建监听方法
//添加键盘监听
private void createKeyListener() {
KeyAdapter l = new KeyAdapter() {
//按下
@Override
public void keyPressed(KeyEvent e) {
if(!"start".equals(gameFlag)) return ;
int key = e.getKeyCode();
switch (key) {
//向上
case KeyEvent.VK_UP:
case KeyEvent.VK_W:
if(start!=null) start.move(0,blocks,panel);
break;
//向右
case KeyEvent.VK_RIGHT:
case KeyEvent.VK_D:
if(start!=null) start.move(1,blocks,panel);
break;
//向下
case KeyEvent.VK_DOWN:
case KeyEvent.VK_S:
if(start!=null) start.move(2,blocks,panel);
break;
//向左
case KeyEvent.VK_LEFT:
case KeyEvent.VK_A:
if(start!=null) start.move(3,blocks,panel);
break;
}
}
//松开
@Override
public void keyReleased(KeyEvent e) {
}
};
//给主frame添加键盘监听
mainFrame.addKeyListener(l);
}
- 在构造中调用
//构造方法
public GamePanel(GameFrame mainFrame){
this.setLayout(null);
this.setOpaque(false);
this.mainFrame=mainFrame;
this.panel =this;
//创建菜单
createMenu();
//创建数组内容
createBlocks();
//计算处理线路
computed();
//创建开始结束的方形
createRects();
//添加键盘事件监听
createKeyListener();
}
- 运行
收尾
此时代码已经基本完成,加入游戏胜利、重新开始等方法即可
//重新开始
void restart() {
/*参数重置
1.游戏状态
2.迷宫单元重置
3.重新计算线路
*/
//1.游戏状态
gameFlag="start";
//2.迷宫单元重置
Block block ;
for (int i = 0; i < ROWS; i++) {
for (int j = 0; j < COLS; j++) {
block = blocks[i][j];
if(block!=null){
block.setVisited(false);
block.walls[0]=true;
block.walls[1]=true;
block.walls[2]=true;
block.walls[3]=true;
}
}
}
//3.计算处理线路
computed();
//开始方块归零
start.setI(0);
start.setJ(0);
//重绘
repaint();
}
看到这里的大佬,动动发财的小手 点赞 + 回复 + 收藏,能【 关注 】一波就更好了。
代码获取方式:
帮忙文章【点赞】 +【 收藏】+【关注】+【评论】 后,加V:qq283582761,我发给你!
更多精彩
1. Java俄罗斯方块
2. Java五子棋小游戏
3. 老Java程序员花一天时间写了个飞机大战
4. Java植物大战僵尸
5. 老Java程序员花2天写了个连连看
6. Java消消乐(天天爱消除)
7. Java贪吃蛇小游戏
8. Java扫雷小游戏
9. Java坦克大战