JavaFX扫雷

windows系统上经典扫雷游戏,1992年4月6日,扫雷和纸牌、空当接龙等小游戏搭载在Windows 3.1系统中与用户见面,主要目的是让用户训练使用鼠标。这个游戏的玩法很简单,有初级、中级、高级和自定义等模式,雷区中随机布置一定数量的地雷,玩家需要尽快找出所有不是地雷的方块,但不许踩到地雷。这里我们使用Java和JavaFX实现一个简易版的扫雷程序,先理解扫雷游戏逻辑,练习使用Java中的循环、数组、二维数组、集合、面向对象、Stream、递归等技术实现游戏功能。

游戏逻辑

  1. 需要一个图形界面,使用JavaFX实现
  2. 需要在图形界面中定义一个 N * N 的网格, N可以是任意设置
  3. N * N个格子中有三个内容: 空白、数字(表示周围8个格子有多少个雷)、雷
  4. 在N * N个格子中随机生成
  5. 在N * N个格子中的不是雷的格子中设置相应的数字,这个数字表示当前格子周围8个格子有多少个雷
  6. 玩家在点击某一个格子时显示的是数字,比如说 显示的是2,表示周围八个格子中有两个雷
  7. 如果玩家点击的格子显示为空白,那么会要把周围8个格子全部显示,如果周围8个格子有空白格子,又需要将空白格子周围8个格子全部显示,依次类推
  8. 如果玩家点击的位置是雷,游戏结束

游戏实现

1. 创建JavaFX Module

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//module声明一个模块 加上 opens 关键词表示模块内的所有包都允许通过 Java 反射访问
module example.fxdemo {
//声明模块依赖
requires javafx.controls;
requires javafx.fxml;
requires javafx.swing;

//开放模块内的包, 允许通过java反射访问,一次开放一个包,如果外部使用了open,那么内部将不能使用opens
opens com.example.fxdemo to javafx.fxml,javafx.controls;
opens com.example to javafx.fxml;

//导出这个包com.example.game,以便别的模块可以使用
exports com.example.game;

}

2. 工程结构

1
2
3
4
5
6
src
-main
-java
com.example.game
MinesweeperGame
module-info.java

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
    38
    public 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;
    }

    @Override
    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
    62
    private 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
    38
    private 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, 箭头函数等