☰
使用Java Swing实现简单的扫雷游戏
2026/10/7 2:46:14 网站建设 项目流程

Java Swing实现扫雷游戏

前言

这是使用Java Swing制作扫雷游戏的笔记。本篇记录怎么让系统自动布雷,绘制基础面板,面板重绘、以及使用鼠标监听器实现翻开和插旗。

环境

  • JDK 17
  • IntelliJ IDEA
  • 纯 Java 项目(未使用 Maven)

一、整体设计

整个游戏只有一个类,但它同时扮演两个角色:

publicclasshomework0927_2extendsJPanelimplementsMouseListener{
  • extends JPanel:它本身就是一块面板,负责把雷区画出来,其中extends继承,可以使用JPanel的所有方法,同时拥有其全部属性;
  • implements MouseListener:它同时是鼠标监听器,负责处理点击,其中implements表示实现,其后接接口,表示将会实现接口所规定的所有方法。

入口:

publicstaticvoidmain(String[]args){homework0927_2 ms=newhomework0927_2();ms.initUI();}

二、两个核心数组

2.1 thunderArr:存"雷和数字"

publicint[][]thunderArr=newint[LINE-1][LINE-1];

约定:-1为雷、0为空白、>0为周围雷数。

2.2 state:存"每格的状态"

publicint[][]state=newint[LINE-1][LINE-1];

约定:0表示未翻开、1表示已翻开、2表示插旗。

使用两个数组,分开记录底层绘制的雷和数字属性,以及顶层的是否绘制“盖子”以及旗帜这两个两件独立的事,
同时,Java 用new创建的数组会自动全填 0,
不像C++是随机值,因此state不用手动初始化,刚好与初始所有格子均盖上符合。

三、绘制:继承 JPanel + 重写 paint

3.1 为什么不用 getGraphics

getGraphics()拿画笔"画一次",但这样窗口一改变大小或出现其他操作时,画面就无法即时更新,而paint()方法则可以在需要重绘时被自动调用(窗口显示、改变大小、重新露出……都会)。所以改成重写 paint,把绘制的内容写进去,界面需要变化时自动重绘。

3.2 paint 的内容

publicvoidpaint(Graphicsg){super.paint(g);//先调用父类完成基础绘画工作for(inti=0;i<LINE;i++){//循环绘制网格g.drawLine(X0,Y0+i*40,(LINE-1)*SIZE+X0,Y0+i*SIZE);g.drawLine(X0+i*SIZE,Y0,X0+i*SIZE,(LINE-1)*SIZE+Y0);}if(start){//仅在第一次绘制时生效,也就是创建随机雷组randomThunder();thunderNumber();start=false;//布尔值变为false,此后不再调用}drawThunder(g);//绘制底层雷和数字}

3.3 start 标记的作用

paint会被反复调用,而雷区则会在第一次绘制前就被确定,因此仅在第一次时start设为true设置雷区,此后设置雷区的代码将不再被调用,保证布雷和算数字只做一次,做完就把它设成false。

四、布雷与算数字

4.1 随机布雷(避免重复)

publicvoidrandomThunder(){intnum=0;//记录当前雷数while(num<LINE){//要放LINE 颗雷intx=(int)(Math.random()*thunderArr.length);//随机雷坐标inty=(int)(Math.random()*thunderArr.length);if(thunderArr[y][x]!=-1){//验证是否位置重复thunderArr[y][x]=-1;num++;//只有不重复时才增加计数}}}

4.2 给雷周围填数字

publicvoidthunderNumber(){for(inti=0;i<thunderArr.length;i++){for(intj=0;j<thunderArr[0].length;j++){if(thunderArr[i][j]==-1){//判定地雷markThunder(j,i);//让雷周围8格数字+1}}}}publicvoidmarkThunder(intx,inty){//x=列,y=行if(x+1<thunderArr.length&&thunderArr[y][x+1]!=-1)thunderArr[y][x+1]+=1;if(x-1>=0&&thunderArr[y][x-1]!=-1)thunderArr[y][x-1]+=1;...其余6个方向...}

关键:每一格的判定都有两个条件①不越界 ②相邻格不是雷(即!= -1)。
防止相邻的雷互相+1导致变为0,雷被抹掉

五、把每格画出来:drawThunder

采用的是"先画内容,再盖盖子"的思路:

publicvoiddrawThunder(Graphicsg){for(inti=0;i<thunderArr.length;i++){for(intj=0;j<thunderArr[0].length;j++){if(thunderArr[i][j]>0){//数字格g.setFont(newFont("Arial",Font.BOLD,24));g.drawString(thunderArr[i][j]+"",j*SIZE+X0+17,(i+1)*SIZE+Y0-17);//此处原本需要一个字符串,然而我们能读到的只有数组中的int类型,因此使用java中任何类型与string拼接都会转变为string,加上一个空白字符串转化为string}if(thunderArr[i][j]==-1){//雷:黑圆g.setColor(Color.black);g.fillOval(j*SIZE+X0+10,i*SIZE+Y0+10,SIZE-20,SIZE-20);}if(state[i][j]!=1){//没翻开则盖盖子g.setColor(Color.LIGHT_GRAY);g.fillRect(j*SIZE+X0+1,i*SIZE+Y0+1,SIZE-2,SIZE-2);}if(state[i][j]==2){//插旗 → 红矩形g.setColor(Color.red);g.fillRect(j*SIZE+X0+12,i*SIZE+Y0+12,SIZE-24,SIZE-24);}}}}
  • 数字和雷总是先画,然后没翻开的格子会被灰色盖子盖住,默认状态下看不见;
  • 翻开后state==1,不再画盖子,下面的数字/雷就露出来了,若插旗,则在盖子上再插旗;
  • 坐标换算:第 j 列、第 i 格左上角是X0 + j*SIZE、Y0 + i*SIZE。

六、鼠标交互

6.1 注册监听器

类声明加了implements MouseListener,然后在窗口里把"自己"注册为监听器:

this.addMouseListener(this);

(第一个this是被监听的面板,第二个this是负责处理的监听器——自己监听自己。)

6.2 坐标 → 行列换算

intx=(e.getX()-X0)/SIZE;// 列inty=(e.getY()-Y0)/SIZE;// 行

整数除法直接得到第几格;再用e.getX() >= X0 && ...把网格外的点击挡掉,仅接受格子范围内的点击。

6.3 左键翻开、右键插旗、踩雷重开

publicvoidmousePressed(MouseEvente){intx=(e.getX()-X0)/SIZE;inty=(e.getY()-Y0)/SIZE;if(e.getX()>=X0&&e.getY()>=Y0&&x<thunderArr.length&&y<thunderArr.length){if(e.getButton()==MouseEvent.BUTTON1){//左键if(state[y][x]==0){if(thunderArr[y][x]==-1){//踩雷state[y][x]=1;// 展示踩中的雷repaint();JOptionPane.showMessageDialog(this,"失败,点击确认重新开始");restart();}else{openCell(y,x);//安全格 → 翻开}}check();}if(e.getButton()==MouseEvent.BUTTON3){//右键if(state[y][x]==0){state[y][x]=2;//插旗}elseif(state[y][x]==2){state[y][x]=0;//取消旗}check();}}repaint();//重新绘制}

6.4 空白格自动展开:openCell(递归)

点开的格子如果是空白(周围没雷),应该把它周围的格子也翻开,一层层扩散:

publicvoidopenCell(introw,intcol){if(row<0||row>=thunderArr.length||col<0||col>=thunderArr[0].length)return;//越界停,仅在格子范围内递归if(state[row][col]!=0)return;//已翻开/已插旗也触发停止state[row][col]=1;//先翻开自己if(thunderArr[row][col]==0){//自己是空白,才向四周扩散openCell(row-1,col-1);openCell(row-1,col);openCell(row-1,col+1);openCell(row,col-1);openCell(row,col+1);openCell(row+1,col-1);openCell(row+1,col);openCell(row+1,col+1);}}

这里用到了递归。边界和"已翻开"两个return
保证了不会无限递归。

6.5 失败与胜利

踩雷时弹窗提示并restart()重开:

publicvoidrestart(){for(inti=0;i<thunderArr.length;i++){for(intj=0;j<thunderArr[0].length;j++){thunderArr[i][j]=0;//清空雷数组state[i][j]=0;//清空状态数组}}start=true;//重置start状态,下次绘制时重新随机雷组repaint();}

胜利判断:所有雷都插了旗、且所有非雷格都翻开了:

publicvoidcheck(){for(inti=0;i<LINE-1;i++){for(intj=0;j<LINE-1;j++){if(thunderArr[i][j]==-1){if(state[i][j]!=2)return;//若还有没插旗的雷则不触发操作}else{if(state[i][j]!=1)return;//有非雷格没翻开,不触发操作}}}JOptionPane.showMessageDialog(this,"恭喜你成功了!点击确认重新开始");//前面没有触发return说明所有格子验证通过restart();}

七、错误记录

  1. 相邻雷被"抹掉":markThunder一开始漏了!= -1,两颗雷相邻时,一颗会给另一颗+1,把它从-1变成0。加上判断才对。
  2. 空白展开会无限递归:一开始没加"已翻开就停"的判断,相邻空白互相调用,出现无限递归错误。

八、总结

本篇用 Swing 做了一个基本能够游玩的扫雷:二维数组存雷区、paint重绘保证画面正确、
MouseListener处理左键翻开和右键插旗、递归实现空白自动展开,并添加了胜负判断。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询