1 系统概述
本次扫雷游戏程序的设计参考Windows XP系统提供的扫雷游戏,实现扫雷的基本游戏功能:游戏的失败判定、游戏的胜利判定、游戏中表示周围雷数的数字生成、地雷的随机生成、游戏的重置、剩余雷数的展示功能、标记地雷功能、计时功能、历史最高分展示功能等,并对界面进行个性化美化。其中必要的游戏组件部分,如:地雷、表示地雷数量的数字、标记用的独角兽、控制游戏开始与结束的团子表情、得分展示区等均使用Photoshop绘制。扫雷程序使用Java语言实现,界面部分使用Java中的GUI技术,得分以及游戏逻辑则通过顺序表、二维数组实现,并通过IO流进行分数的保存。
2 需求分析
扫雷游戏是一个小型益智游戏,可用于人们日常的休闲、娱乐等场景。本次游戏设计涉及一维数组、二维数组、Swing中的常用组件、GUI中的事件处理(事件监听器、鼠标监听器)、类的封装与继承、static静态变量、包装类、随机数、IO流等方面的知识。
🐰具体需求概要如下:
(1) 游戏界面应当尽可能美观,大小应当合适并居中显示,窗口应有“扫雷”字样,且窗口大小不可再调整;
(2) 游戏界面应当有必要的说明性图示或文字信息。在窗口顶部区域应当展示剩余雷数、最短用时、当前用时的信息。顶部中间区域应当有组件可以控制游戏的开始和结束;
(3) 游戏界面的雷区部分应当以10×10网格绘制,且雷区应当随机在不同位置生成10个地雷;
(4) 网格应该包含两种状态,被覆盖与未被覆盖。被覆盖时应当不显示格子的内容,未被覆盖则显示该格子的内容(雷或者数字);
(5) 游戏开始与重新开始功能的实现:鼠标左键点击游戏界面顶部中央区域的表情,实现游戏的开始与重新开始的操作;
(6) 数字显示功能的实现:在雷区没有雷的位置应当显示一个整数,该整数范围[0,8],表示其所有邻居方格(该方格周围8个格子)所包含的雷数,当雷数为0时不显示数字,其余情况显示对应的数字;
(7) 翻开功能的实现:点击网格范围内的任一方格,显示该方格的内容(可能为地雷,也可能为数字),若翻开的网格内容为数字“0”,则翻开其周围所有的邻居方格。特别地,当格子已经被翻开(未被覆盖),则不可再覆盖;
(8) 标记功能的实现:鼠标右键单击被覆盖的格子,则该网格显示“独角兽”的图片(无论该区域是否真的有雷,都可以标记),当游戏失败时若该处不是雷,则更换成标记错误的图片;
(9) 单击数字功能的实现:鼠标右键单击已经显示的数字,且周围标记数等于此数字,则翻开其余未翻开的邻居方格;
(10) 剩余雷数展示功能的实现:在界面顶部的剩余雷数功能框中展示剩余雷数(地雷总数-标记数),已标记的区域并不代表一定是雷;
(11) 游戏的失败判定:当标记的方格数=地雷总数时进行判定,如果被标记的方格中有不是地雷的,则游戏失败;当鼠标左键单击网格时进行判定,如果翻开后为地雷,则游戏失败;当鼠标右键单击数字时进行判定,若该数字方格的邻居方格中被标记的数量=该数字显示的雷数则翻开所有邻居格子,若含有未被标记的地雷,则游戏失败;
(12) 游戏的胜利判定:当标记的方格数=地雷总数时进行判定,如果被标记的方格均为地雷,则游戏胜利;
(13) 游戏的计时功能:当游戏运行时即开始计时,时间显示在游戏界面顶部的“当前用时”状态栏,以秒为单位;当游戏胜利或者失败时停止计时。
(14) 最短用时功能的实现:当游戏胜利时判断游戏用时和最短用时状态栏的大小,若游戏用时<最短用时,则更新最短用时的状态栏,否则不进行更新。
🦁操作一览表:
3 总体设计
3.1 系统总体功能设计
此程序大方向上包含游戏规则判断功能、主界面控制功能、鼠标左键翻开功能、鼠标右键标记功能。其中主界面控制功能中包含地雷的初始化、数字的初始化、计时功能、最短用时以及显示剩余雷数功能;游戏规则判断功能包括游戏的胜利判定、游戏的失败判定以及游戏开始与重置;鼠标左键翻开功能包含翻开显示地雷或者翻开显示数字;鼠标右键标记功能包括标记格子以及单击数字功能(依据游戏规则判断是否翻开其周围的邻居格子)。具体功能结构图如下:
3.2 系统总体流程设计
开始进入扫雷程序时则游戏直接开始,计时器开始计时,同时初始化10个地雷并生成数字。而后开始判断是否鼠标左键点击菜单顶部的表情区域位置,如若点击,则计时器归0重新计时,游戏重新开始。否则,程序则监听鼠标单击操作,根据鼠标左键或右键的点击判断该翻开格子还是标记格子。当翻开的格子为地雷时,则判定游戏失败,此时扫雷程序结束,计时器停止。系统总体设计流程图如下:
4 系统实现
在设计扫雷游戏时需要编写7个Java源文件:Bottom.java、Cover.java、GamePanel.java、MinTime.java、PaintBottomArea.java、Properties.java、ShowBottomCount.java。除了需要编写上述java源文件给出的类外,还需要Java系统提供一些必要的类,如:JPanel、JFrame、File、Image等类。
其关系结构如下:
4.1 Properties类
该类为工具类用于存放扫雷程序所需要的各种参数,且所有属性使用静态变量,便于其他类使用。该类中还将游戏所需要的图片组件存储为静态对象,其他类可以直接使用该对象,具体可见代码注释(记得将绝对路径改为自己存储图片的地址)。
🐰 代码如下:
import java.awt.*; /* 工具类 用于存放扫雷界面所需要的各种参数 所有属性使用静态变量 便于其他类使用 */ public class Properties { static int Grid_Width=10;//横着的格子的数量 static int Grid_Heigh=10;//竖着的格子的数量 static int Grid_Offset=45;//网格起点的坐标偏移量(45,45) static int Grid_Length=50;//游戏面板每个格子的宽度 static int Bottom_Count=10;//地雷个数 static int Sign_Count=0;//标记独角兽的数量 //游戏用时相关参数 static long Start_Time; static long End_Time; //二维数组中-1表示雷,0-8表示周围8个格子的雷数 这里扩大了二维数组的范围,避免边界判断 static int[][] Data_Bottom=new int[Grid_Width+2][Grid_Heigh+2]; //游戏状态相关量,0表示游戏中,1表示胜利,2表示失败 static int status=0; //鼠标事件相关参数 static int Mouse_X; static int Mouse_Y; //鼠标被点击则为true static boolean Left_Click=false; static boolean Right_Click=false; /* 游戏需要的图片载入 */ //地雷图片 static Image bottom=Toolkit.getDefaultToolkit().getImage( "C:\\Users\\26510\\IdeaProjects\\黄小黄\\数据结构与算法课程设计_扫雷\\src\\ImagesForGame\\bottom2.jpg"); //数字0到8的图片导入,0为一个灰色背景 static Image[] c=new Image[9]; static { for (int i = 0; i <= 8; i++) { c[i]=Toolkit.getDefaultToolkit().getImage( "C:\\Users\\26510\\IdeaProjects\\黄小黄\\数据结构与算法课程设计_扫雷\\src\\ImagesForGame\\"+i+".png"); } } //覆盖界面的绘制 -1无图片 0为没有点开时的覆盖图片 1为标记独角兽的图片 2为标记错误图片 static int[][] Top=new int[Grid_Width+2][Grid_Heigh+2]; static Image cover=Toolkit.getDefaultToolkit().getImage( "C:\\Users\\26510\\IdeaProjects\\黄小黄\\数据结构与算法课程设计_扫雷\\src\\ImagesForGame\\cover.png"); static Image sign=Toolkit.getDefaultToolkit().getImage( "C:\\Users\\26510\\IdeaProjects\\黄小黄\\数据结构与算法课程设计_扫雷\\src\\ImagesForGame\\sign.jpg"); static Image wrong=Toolkit.getDefaultToolkit().getImage( "C:\\Users\\26510\\IdeaProjects\\黄小黄\\数据结构与算法课程设计_扫雷\\src\\ImagesForGame\\wrong.png"); //游戏中,游戏胜利,游戏失败时顶部中间的表情变化 static Image start=Toolkit.getDefaultToolkit().getImage( "C:\\Users\\26510\\IdeaProjects\\黄小黄\\数据结构与算法课程设计_扫雷\\src\\ImagesForGame\\start.jpg"); static Image win=Toolkit.getDefaultToolkit().getImage( "C:\\Users\\26510\\IdeaProjects\\黄小黄\\数据结构与算法课程设计_扫雷\\src\\ImagesForGame\\win.jpg"); static Image over=Toolkit.getDefaultToolkit().getImage( "C:\\Users\\26510\\IdeaProjects\\黄小黄\\数据结构与算法课程设计_扫雷\\src\\ImagesForGame\\over.jpg"); //当前用时,剩余雷数,最短用时图像组件 static Image time=Toolkit.getDefaultToolkit().getImage( "C:\\Users\\26510\\IdeaProjects\\黄小黄\\数据结构与算法课程设计_扫雷\\src\\ImagesForGame\\time.jpg"); static Image minecount=Toolkit.getDefaultToolkit().getImage( "C:\\Users\\26510\\IdeaProjects\\黄小黄\\数据结构与算法课程设计_扫雷\\src\\ImagesForGame\\minecount.jpg"); }
4.2 Bottom类
地雷类,包含地雷坐标的相关参数以及地雷位置的初始化方法。具体如下:
(1) 地雷的坐标:int 类型的x和y,两者均使用Math类的random方法生成随机数,用于生成地雷的坐标;
(2) 存放地雷位置的数组:int[]类型的locate,大小为两倍的地雷数量长度,因为这是个一维数组,每两个位置存储的是一个地雷的坐标;
(3) 用于避免地雷重合的参数:boolean 类型的flag,初始化为true,若值为true则表示可以存储。在该类初始化地雷坐标的方法中,应该将每次生成的坐标与locate中的值进行比较,若已经包含,则将其值更改为false,不再放置该坐标,重新生成一个新坐标,直到不重复为止;
(4) 随机生成地雷的方法:newStartGame(),该方法中将随机生成的非重复的地雷坐标存储到locate数组中,最后通过该数组确定生成地雷的位置,将二维数组Data_Bottom对应位置的值设为-1
🐱 代码如下:
/* 地雷类 地雷坐标相关参数 地雷位置初始化 */ public class Bottom { //存放地雷位置,相邻两个元素分别为x、y坐标 int[] locate=new int[2*Properties.Bottom_Count]; //地雷的坐标 int x,y; //判断地雷是否重合的标记,若为true则可以放置地雷 boolean flag=true; //游戏重新开始时需要用到重新生成地雷的方法 void newStartGame(){ //随机生成地雷 for (int i = 0; i < Properties.Bottom_Count*2; i+=2) { x=(int)(Math.random()*Properties.Grid_Width+1);//1-11 y=(int)(Math.random()*Properties.Grid_Heigh+1);//1-11 //解决地雷重合问题,bug修复 for (int j = 0; j < i; j+=2) { if(x==locate[j]&&y==locate[j+1]){ i=i-2;//如果随机坐标重合,则回退重新生成坐标 flag=false; break; } } if(flag){ locate[i]=x; locate[i+1]=y; } flag=true; } //将地雷的相应坐标位置的二维数组存值-1表示地雷 for (int i = 0; i < Properties.Bottom_Count*2; i+=2) { //System.out.println(locate[i]+" "+locate[i+1]);//测试生成的随机坐标是否重复 Properties.Data_Bottom[locate[i]][locate[i+1]]=-1; } } }
4.3 ShowBottomCount类
在该类中包含一个newBottomNum()方法,用于计算每一方格周围邻居格子中所含的地雷数目,并将地雷数目存储到对应位置的Data_Bottom数组中。该方法通过遍历邻居格子位置的Data_Bottom数组的值来实现,如果邻居位置值为-1,则记录一次,最后把记录的值存储到Data_Bottom数组中。
🐴 代码如下:
/* 显示方格周围8个格子的雷的个数 */ public class ShowBottomCount { //显示周围雷数量,范围为0-8 void newBottomNum(){ for (int i = 1; i <= Properties.Grid_Heigh; i++) { for (int j = 1; j <= Properties.Grid_Width; j++) { //判断该位置是否是雷,如果该位置是雷,则周围的8个格子存储的数字加1,表示雷数增加1 if(Properties.Data_Bottom[i][j]==-1){ for (int k = i-1; k <= i+1 ; k++) { for (int l = j-1; l <= j+1 ; l++) { if(Properties.Data_Bottom[k][l]>=0){ Properties.Data_Bottom[k][l]++; } } } } } } } }
4.4 PaintBottomArea类
该类主要用于绘制雷区、绘制游戏组件、游戏图片。
🍊 代码如下:
import javax.swing.*; import java.awt.*; /* 绘制雷区 绘制游戏组件 游戏图片 */ public class PaintBottomArea extends JPanel{ Bottom bt=new Bottom(); ShowBottomCount sb=new ShowBottomCount(); { bt.newStartGame(); sb.newBottomNum(); } //游戏重新开始的方法设计(底层地雷和数字) void reStartGame(){ //将地雷的二维数组参数重置为0 for (int i = 1; i <= Properties.Grid_Heigh; i++) { for (int j = 1; j <= Properties.Grid_Width; j++) { Properties.Data_Bottom[i][j]=0; } } //重新生成地雷,重新计算数字 bt.newStartGame(); sb.newBottomNum(); } void myPaint(Graphics g){ //棋盘格绘制 for (int i = 0; i <= Properties.Grid_Width; i++) { g.setColor(Color.orange); g.drawLine(Properties.Grid_Offset+i*Properties.Grid_Length, 3*Properties.Grid_Offset, Properties.Grid_Offset+i*Properties.Grid_Length, 3*Properties.Grid_Offset+Properties.Grid_Heigh*Properties.Grid_Length); } for (int i = 0; i <= Properties.Grid_Heigh; i++) { g.setColor(Color.orange); g.drawLine(Properties.Grid_Offset, 3*Properties.Grid_Offset+i*Properties.Grid_Length, Properties.Grid_Offset+Properties.Grid_Width*Properties.Grid_Length, 3*Properties.Grid_Offset+i*Properties.Grid_Length); } for (int i = 1; i <= Properties.Grid_Width; i++) { for (int j = 1; j <= Properties.Grid_Heigh ; j++) { //地雷绘制 if (Properties.Data_Bottom[i][j] == -1) { g.drawImage(Properties.bottom, Properties.Grid_Offset+(i-1)*Properties.Grid_Length+1, 3*Properties.Grid_Offset+(j-1)*Properties.Grid_Length+1, Properties.Grid_Length-2, Properties.Grid_Length-2, null); } //数字绘制 if (Properties.Data_Bottom[i][j] >= 0) { g.drawImage(Properties.c[Properties.Data_Bottom[i][j]], Properties.Grid_Offset+(i-1)*Properties.Grid_Length+1, 3*Properties.Grid_Offset+(j-1)*Properties.Grid_Length+1, Properties.Grid_Length-2, Properties.Grid_Length-2, null); } } } //顶部中间表情绘制,问号脸为游戏中,哭脸为游戏失败,惊讶脸为胜利 0表示游戏中,1表示胜利,2表示失败 switch (Properties.status){ case 0: Properties.End_Time=System.currentTimeMillis()/1000;//获取结束时间 g.drawImage(Properties.start, Properties.Grid_Offset+Properties.Grid_Length*(Properties.Grid_Width/2-1), 35, null); break; case 1: g.drawImage(Properties.win, Properties.Grid_Offset+Properties.Grid_Length*(Properties.Grid_Width/2-1), 35, null); break; case 2: g.drawImage(Properties.over, Properties.Grid_Offset+Properties.Grid_Length*(Properties.Grid_Width/2-1), 35, null); break; } //顶层其他组件绘制,包含剩余雷数(每标记一个位置,则剩余雷数减1,标记位置并不意味着一定有雷),使用时间,历史最短用时 g.drawImage(Properties.minecount,5,35,null); g.drawImage(Properties.time,Properties.Grid_Offset+300,35,null); //绘制剩余雷数 g.setColor(Color.red); g.setFont(new Font("宋体",Font.BOLD,30)); g.drawString(""+(Properties.Bottom_Count-Properties.Sign_Count),Properties.Grid_Offset+5,125); //绘制使用时间 g.drawString((Properties.End_Time-Properties.Start_Time)+"s",Properties.Grid_Offset+305,125); //绘制最短用时 g.drawString(MinTime.Min_Time+"s",Properties.Grid_Offset+140,125); } }
4.5 Cover类
该类用于游戏顶层的网格绘制,分为四种情况,分别为有覆盖、无覆盖、标记正确、标记错误,具体如下:
(1) 关于鼠标的位置坐标属性:int 类型的girld_x与girld_y,用于存储转化后的鼠标坐标,即鼠标位于横向第几个格子,纵向第几个格子;
(2) reStartGame()方法:该方法用于重置顶层方格的状态参数,游戏需要重新开始的时候调用该方法;
(3) gameLogic()方法:该方法实现游戏的逻辑部分,包括实现鼠标的左键翻开,右键标记功能;游戏胜利与失败的判定等;
(4) open(int x,int y)方法:该方法用于实现鼠标左键翻开(x,y)处网格内容为数字0时,自动打开周围所有邻居格子。即当周围格子未被翻开且处于雷区时,递归翻开,知道邻居格子都被翻开后,递归结束(当Data_Bottom=0时 周围格子一定没有雷);
(5) openNum(int x,int y)方法:实现右键点击(x,y)处数字时,判断该格子周围的邻居格子中已经标记的数目(count)是否与该数字相同,如若相同,则打开该格子周围所有处于覆盖状态下的格子;
(6) isWon()方法:该方法具有boolean类型的返回值,用于判断游戏是否失败。如果胜利则返回true,反之则返回false,该方法被gameLogic()方法调用;
(7) victory()方法:该方法具有boolean类型的返回值,用于判断游戏是否胜利。如果胜利则返回true,反之则返回false,该方法被gameLogic()方法调用;
(8) showBottom()方法:用于游戏失败后显示游戏中所有的地雷;
(9) myPaint(Graphics g)方法:用于绘制顶层的方格,即绘制覆盖状态的方格、标记状态的方格、标记错误状态的方格。