windows系统上经典扫雷游戏,1992年4月6日,扫雷和纸牌、空当接龙等小游戏搭载在Windows 3.1系统中与用户见面,主要目的是让用户训练使用鼠标。这个游戏的玩法很简单,有初级、中级、高级和自定义等模式,雷区中随机布置一定数量的地雷,玩家需要尽快找出所有不是地雷的方块,但不许踩到地雷。这里我们使用Java和JavaFX实现一个简易版的扫雷程序,先理解扫雷游戏逻辑,练习使用Java中的循环、数组、二维数组、集合、面向对象、Stream、递归等技术实现游戏功能。
游戏逻辑
- 需要一个图形界面,使用JavaFX实现
- 需要在图形界面中定义一个 N * N 的网格, N可以是任意设置
- N * N个格子中有三个内容: 空白、数字(表示周围8个格子有多少个雷)、雷
- 在N * N个格子中随机生成雷
- 在N * N个格子中的不是雷的格子中设置相应的数字,这个数字表示当前格子周围8个格子有多少个雷
- 玩家在点击某一个格子时显示的是数字,比如说 显示的是2,表示周围八个格子中有两个雷
- 如果玩家点击的格子显示为空白,那么会要把周围8个格子全部显示,如果周围8个格子有空白格子,又需要将空白格子周围8个格子全部显示,依次类推
- 如果玩家点击的位置是雷,游戏结束
游戏实现
1. 创建JavaFX Module
- 参考 Java模块化编程 修改module-info.java文件,添加javafx模块支持
1 | //module声明一个模块 加上 opens 关键词表示模块内的所有包都允许通过 Java 反射访问 |
2. 工程结构
1 | src |
3. 代码实现
创建游戏界面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38public class MinesweeperGame extends Application {
//Tile数量
private static final int TILE_SIZE = 40;
//界面宽和高
private static final int W = 800;
private static final int H = 600;
//x轴上 20个格子(tile)
private static final int X_TILES = W / TILE_SIZE;
//y轴上 15个格子(tile)
private static final int Y_TILES = H / TILE_SIZE;
//UI 场景对象
private Scene scene;
//此方法用于创建一个面板Pane N*N个格子会绘制在里边
private Parent createContent() {
Pane root = new Pane();
root.setPrefSize(W, H);
return root;
}
public void start(Stage stage) throws Exception {
//根据面板创建场景对象
scene = new Scene(createContent());
//设置舞台Stage标题
stage.setTitle("扫雷游戏");
//设置舞台场景
stage.setScene(scene);
//显示
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}运行结果如下图所示:
创建Tile类
- 此类定义成内部类
- 使用面向对象技术创建一个Tile类,这个类需要绘制到上面所定义的面板Pane上面,而且Tile内部还需要绘制其它内容,比如数字和雷,所以此类继承
- StackPane,每一个Tile类对象都表示一个格子, Tile中文意思是砖块。
- 每一个Tile对象需包含坐标x,y 是否有雷 hasBomb, 是否是打开的isOpen(玩家点击的时候才会打开)
- 每一个Tile对象还需要包含一个文本区域用于设置数字,周围还需要绘制一个矩形边框增强效果,此时需要用到图形界面JavaFX技术
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62private class Tile extends StackPane {
//每个Tile对象的坐标
private int x, y;
//是否有雷
private boolean hasBomb;
//是否打开,默认为false
private boolean isOpen = false;
//定义矩形边框
private Rectangle border = new Rectangle(TILE_SIZE - 2, TILE_SIZE - 2);
//文本对象,用于设置格子中的数字等
private Text text = new Text();
//构造函数,用于初始化坐标位置和是否有雷
public Tile(int x, int y, boolean hasBomb) {
this.x = x;
this.y = y;
this.hasBomb = hasBomb;
//UI 设置格子的边框颜色,颜色使用Colod类静态预定义值
border.setStroke(Color.LIGHTGRAY);
//设置文本字体大小 使用 Font类中中的font静态方法
text.setFont(Font.font(18));
//设置文本框中的内容,此时判断hasBomb值,如果为真则表示有雷,此处用X表示雷,如果为false表示没有雷,文本内容则为空
text.setText(hasBomb ? "X" : "");//X表示雷
//默认不显示文本
text.setVisible(false);
//设置矩形的透明度
border.setOpacity(0.8);
//当前是一个StackPane面板容器,需要将矩形和文本组件添加到面板容器中,而且面板容器是一个Stack(栈),根据栈的特性,最后添加进去的在最上面
getChildren().addAll(border, text);
//设置当前Tile对象的x和y的位置,用处是绘制每一个StackPane也就是Tile, 每一个格子定义了一个值为TILE_SIZE=40
setTranslateX(x * TILE_SIZE);
setTranslateY(y * TILE_SIZE);
//为每一个Tile格子对象注册一个事件,当点击的时候调用open方法,此时需要判断格子是不是打开的,利用isOpen属性
setOnMouseClicked(e -> open());
}
public void open() {
//如果是打开的则直接返回,不再执行后续逻辑
if (isOpen)
return;
//如果点到了雷,则游戏结束
if (hasBomb) {
System.out.println("Game Over");
//重新再创建一个内容,重新设置场景,也可以通过按钮点击时重新设置
scene.setRoot(createContent());
return;
}
//如果前两个判断都没进入则表示Tile格子未翻开,设置isOpen为true,表示已打开,然后显示文本内容,并让矩形的填充内容为空
isOpen = true;
text.setVisible(true);
border.setFill(null);
//如果点击时Text内容是空的,那么需要显示周围8个格子
if (text.getText().isEmpty()) {
//此处需要找到周围的8个Tile对象,遍历每一个,并判断是不是空格,如果是空格则需要再次执行getNeighbors方法
//getNeighbors方法用于查找相邻的8个Tile格子
//forEach语法是Java8之后的新语法,遍历每一个Tile格子对象时都会调用它的open方法
getNeighbors(this).forEach(Tile::open);
}
}
}实现查找空白Tile相邻的8个Tile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42//需要在MinesweeperGame类中定义属性 Tile类型的二维数组 20x15 存储Tile对象
private Tile[][] grid = new Tile[X_TILES][Y_TILES];
/**
* 查找周边八个tile方格是不是雷
* 定义一个二维数组, 数组的每二个值表示一个tile的周围八个格子的坐标
*/
private List<Tile> getNeighbors(Tile tile) {
//存储邻居tile块使用集合ArrayList存储,并用接口List引用,< > 表示泛型, Tile是内部类
List<Tile> neighbors = new ArrayList<>();
//定义一个数组,每两个元素分别表示当前格子和其它8个格子的坐标差
int[] points = new int[] {
-1, -1,
-1, 0,
-1, 1,
0, -1,
0, 1,
1, -1,
1, 0,
1, 1
};
//遍历这个数组
for (int i = 0; i < points.length; i++) {
//每两个元素表示一个坐标,所以dx为points[i], dy则为points[++i]
int dx = points[i];
int dy = points[++i];
//计算新的tile格子坐标,当前格子的坐标值加上dx,dy
int newX = tile.x + dx;
int newY = tile.y + dy;
//此处X_TILES=20, Y_TILES=15
//判断每个新的Tile对象的坐标是否超出范围,如果没有超出范围则加到ArrayList集合中
if (newX >= 0 && newX < X_TILES && newY >= 0 && newY < Y_TILES) {
//grid是一个存储了所有tile对象的二维数组
neighbors.add(grid[newX][newY]);//将周边的Tile添加到集合中
}
}
//返回相邻元素的集合
return neighbors;
}修改createContent方法,创建界面内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38private Parent createContent() {
//创建内容面板,用于放置其它界面元素
Pane root = new Pane();
//设置宽高
root.setPrefSize(W, H);
//嵌套循环遍历
for (int y = 0; y < Y_TILES; y++) {
for (int x = 0; x < X_TILES; x++) {
//创建Tile对象
//第三个参数表示是否有雷,此时用一个简单的方式设置雷,当随机数小于0.3时表示有雷
Tile tile = new Tile(x, y, Math.random() < 0.3);
//将每一个tile对象存储在二维数组中,二维数组中的每一个元素都表示一个tile
grid[x][y] = tile;
//将元素添加到root root是一个Pane
root.getChildren().add(tile);
}
}
//遍历二维数组,判断每一个tile是不是雷,如果是雷则continue,否则需要计算当前tile边上雷的数量
for (int y = 0; y < Y_TILES; y++) {
for (int x = 0; x < X_TILES; x++) {
Tile tile = grid[x][y];
//有雷则循环continue
if (tile.hasBomb)
continue;
//找雷是为了设置tile砖块中的数字
//如果不是雷,则需要计算它周边有多少个雷,调用getNeighbors方法
//此处用到了Java8中的stream以及箭头函数等
long bombs = getNeighbors(tile).stream().filter(t -> t.hasBomb).count();
//如果周边雷的数量大于0则将tile对象中的文本框设置为雷的数量
if (bombs > 0)
tile.text.setText(String.valueOf(bombs));
}
}
return root;
}完成并总结
- 其实扫雷游戏的实现并不难,只需要掌握最基础的Java技术便能实现
- 创建类以及定义属性和行为、
- 使用JavaFX制作图形界面
- 一维数组和二维数组的定义和赋值
- Java8中的新特性 forEach, stream, 箭头函数等