| 本文介绍一个简单的扫雷游戏例子,屏幕抓图如下。

可执行的jar文件(j2sdk1.4.2_08编译打包,包括源代码):附件:jMine.jar(20K)
『要解决的问题』
1. 地雷,标识棋等图形的绘制;
2. 游戏数据(地雷位置)的产生;
3. 非地雷格子显示数字的计算;
4. 游戏逻辑
『包中源文件列表』
- hysun.minegame
-- ConfigDialog.java
-- FieldCell.java
-- GameFrame.java
-- GamePanel.java
-- GraphicsUtil.java
- ConfigDialog(extends JDialog)是配置游戏数据(雷场行列数,地雷数目)的对话框,就不多说了。
- GameFrame(extends JFrame)只是提供一个应用窗口,也不说了。
- GraphicsUtil提供图形绘制方法。
- FieldCell代表一个格子。
- GamePanel(extends JComponent implements MouseListener)代表整个雷场,并且控制游戏逻辑。
『GraphicsUtil』
该类提供static方法,绘制游戏中各种图形,并且将格子大小设成32x32。详情如下表所列:
未知区域 [蓝色区域] |
....
public static Color ukcolor = new Color(99, 130, 191);
....
public static void drawUnknown(Graphics g, int x, int y) {
g.setColor(ukcolor);
g.fillRect(x, y, 32, 32);
}
|
| 地雷 |
....
public static Color mbcolor = new Color(90, 90, 90);
....
public static void drawMine(Graphics g, int x, int y) {
g.clearRect(x, y, 32, 32);
g.setColor(mbcolor);
g.fillOval(x+5, y+9, 21, 19);
g.setColor(Color.black);
g.fillRect(x+11, y+5, 10, 6);
}
|
地雷标识旗 [小红旗] |
....
public static void drawFlag(Graphics g, int x, int y) {
g.clearRect(x, y, 32, 32);
g.setColor(Color.red);
g.fillRect(x+8, y+8, 16, 10);
g.setColor(Color.black);
g.drawLine(x+8, y+8, x+8, y+24);
g.drawLine(x+9, y+8, x+9, y+24);
}
|
非地雷格数字(0-8) [不同数字使用不同颜色] |
....
public static Color[] colorreg = new Color[] {
null, // 0
Color.blue, // 1
Color.green.darker(), // 2
Color.red, // 3
Color.blue.darker(), // 4
Color.MAGENTA, // 5
Color.CYAN.darker(), // 6
Color.BLACK, // 7
Color.orange.darker() // 8
};
....
public static Font numfont = new Font("Verdana", Font.BOLD, 18);
....
public static void drawNumber(Graphics g, int x, int y, int i) {
g.clearRect(x, y, 32, 32);
if (i == 0)
return;
g.setColor(colorreg[i]);
g.setFont(numfont);
FontMetrics fm = g.getFontMetrics();
String s = String.valueOf(i);
int sx = (32 - fm.stringWidth(s)) / 2;
int sy = (32 - fm.getHeight()) / 2 + fm.getAscent();
g.drawString(s, x+sx, y+sy);
}
|
疑问标识旗 [小蓝旗,带问号] |
....
public static Font qnmfont = new Font("Verdana", Font.PLAIN, 10);
....
public static void drawDoubt(Graphics g, int x, int y) {
g.clearRect(x, y, 32, 32);
g.setColor(colorreg[4]);
g.fillRect(x+8, y+8, 16, 10);
g.setColor(Color.black);
g.drawLine(x+8, y+8, x+8, y+24);
g.drawLine(x+9, y+8, x+9, y+24);
g.setColor(Color.yellow);
g.setFont(qnmfont);
FontMetrics fm = g.getFontMetrics();
String s = "?";
int sx = (14 - fm.stringWidth(s)) / 2;
int sy = (10 - fm.getHeight()) / 2 + fm.getAscent();
g.drawString(s, x+sx+10, y+sy+8);
}
|
叉叉 [game over时标柱错误的判断] |
....
public static void drawCross(Graphics g, int x, int y) {
g.setColor(Color.black);
g.drawLine(x+2, y+2, x+28, y+28);
g.drawLine(x+2, y+3, x+28, y+29);
g.drawLine(x+3, y+2, x+29, y+28);
g.drawLine(x+2, y+28, x+28, y+2);
g.drawLine(x+2, y+27, x+28, y+1);
g.drawLine(x+3, y+28, x+29, y+2);
}
|
好了,问题1圆满解决。
『FieldCell』
该类的关键在于格子相关的属性变量,如下表所列:
| 变量 | 详情 |
| int state | 代表该格子目前所处状态,取值范围是该类所定义的几个常数:UNKNOWN(该格子目前还是未知区域), FLAGGED(已经被标识为地雷), DOUBTED(被怀疑为地雷), REVEALED(已经被挖开), WRONG_F(game over时错误标识为地雷), WRONG_D(game over时错误怀疑为地雷) |
| boolean isMine | 顾名思义,表明该格子是否埋有地雷 |
| int number | 只有当该格子没有地雷时,该变量才被用到,标识该格子周围的地雷数目,数目0-8 |
| int gHint | Graphics Hint,UI利用该信息为该格子画出适当的图形,每当格子状态改变时,gHint的值将根据以上三个变量做出相应的调整。gHint的数值和实际图形的映射可以参看源代码的注释。 |
该类对上述变量进行操作(get/set)的方法除外,还有一个方法public void draw(Graphics g, int x, int y)。此方法根据gHint的值利用GraphicsUtil提供的方法对自身的格子进行绘画,会被GamePanel调用到。
『GamePanel』
最后看看游戏的老大吧 。该类中有一些游戏状态显示的代码,主要是根JLabel,JButton等相关的,就略去不提了。
------
GamePanel里面有一个2D的数组:FieldCell[][] cells。问题2和3是关于游戏前数据的初始化问题,其代码包含在public void setGameParam(int mineNum, int r, int c)方法里面。
地雷数据产生的原理很简单,不断随机产生0到总格子数之间的一个数,只到不重复的数目达到所需地雷数目。如下:
....
int totalNum = r * c;
cells = new FieldCell[r][c];
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
cells[i][j] = new FieldCell();
}
}
int count = 0;
while (count < mineNum) {
int s = (int) (Math.random() * totalNum);
FieldCell fc = cells[s/c][s%c];
if (!fc.isMine()) {
fc.setMine(true);
count++;
}
}
相邻地雷数目的计算就要一个格子一个格子的过一边了。原理也很简单,每个格子有8个相邻的格子(处于边界的格子除外),每个格子都检查一下是不是地雷。如下:
....
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
if (!cells[i][j].isMine()) {
int num = 0;
if (i-1 >= 0) {
if (j-1 >= 0 && cells[i-1][j-1].isMine())
num++;
if (cells[i-1][j].isMine())
num++;
if (j+1 < c && cells[i-1][j+1].isMine())
num++;
}
{ // i
if (j-1 >= 0 && cells[i][j-1].isMine())
num++;
if (cells[i][j].isMine())
num++;
if (j+1 < c && cells[i][j+1].isMine())
num++;
}
if (i+1 < r) { // i+1
if (j-1 >= 0 && cells[i+1][j-1].isMine())
num++;
if (cells[i+1][j].isMine())
num++;
if (j+1 < c && cells[i+1][j+1].isMine())
num++;
}
cells[i][j].setNumber(num);
} // end Non-Mine cell if
} // end for on j
} // end for on i
至此,问题2,3也得到解决。另外需要提到的是GamePanel是一个JComponent的子类,它的图形是通过override下面这个方法实现的:
....
protected void paintComponent(Graphics g) {
g.setColor(Color.lightGray);
g.fillRect(0, 0, w, h);
for (int i = 0; i < r; i++) {
for (int j = 0; j < c; j++) {
//格子大小是32×32,这里用34就在每两个格子间留下2的空隙
cells[i][j].draw(g, j*34, i*34);
}
}
}
代码中的w, h为该面板的长,宽,根据格子数目设置(每两个格子中间留有一段空隙)。上面讲到FieldCell类的draw方法就是在这里被调用的。
------
游戏逻辑的解决是由对鼠标事件的处理完成的。GamePanel实现了MouseListener这个接口,不过这里只用到了mouseClicked这个方法。由于实际代码中牵涉到很多其他更新用户界面的方法,为求简练,这里将用pseudo code来解释:
public void mouseClicked(MouseEvent e) {
根据鼠标事件记录的位置找出相应的格子;
if (该格子已经被挖开: FieldCell.REVEALED)
return;
if (左键点击) {
if (被插了小红旗) //这是用来防止玩家误操作的
return;
if (踩到地雷了) {
game over; //鼠标事件停止响应
显示所有未挖出地雷,并且用叉叉标出错误的红旗和蓝旗;
} else { //挖地雷,标柱蓝旗的格子可以挖开
调用一个叫reveal(int i, int j)方法。
// reveal(int int) 是个递归的方法,首先将自己挖开,
// 然后如果自己是个数字为0的格子,对相邻的8个格子调用reveal方法。
// reveal(int int)每挖开一个格子,挖开格子计数加加。
查看是否满足胜利条件(被挖开格子数+被插小红旗的格子数=总格子数);
if (胜利)
恭喜玩家,停止鼠标事件响应;
}
} else if (右键点击) { //右键用来控制插旗子
if (格子状态==FieldCell.UNKNOWN) {
if (插的小红旗数目 < 总地雷数) {
给格子插上红旗(设置状态);
红旗计数加加;
查看是否胜利(同上);
} else { //sorry, 你的红旗用完了,改用蓝旗吧。
给格子插上蓝旗(设置状态);
}
} else if (格子状态==FieldCell.FLAGGED) { //红旗飘扬
拔掉红旗,换成蓝旗;
红旗计数减减;
} else if (格子状态==FieldCell.DOUBTED) { //蓝旗招摇
回归未知区域;
}
}
调用repaint()将雷场重画一边;//这个很关键,不然游戏对于玩家的操作将“无动于衷”。
}
好了,问题4也解决了,大功告成!
|