泡泡糖小游戏

本次项目介绍分为若干个章节,供参考学习。

仓库地址:bistutzyy/Bubble

效果展示

第一章 需求分析

本次程序设计实践所做的实验项目主要是通过开发工具和 Jar 指令运行泡泡糖项目,体验泡泡糖项目的显示、消除、移动和积分等业务功能。

1.1 项目设计思路

要设计出一款合格甚至优秀的游戏,在项目设计方面应该围绕游戏的可玩性,界面的可视性两方面出发。

1.1.1 可玩性

以我们所做的项目泡泡糖为例,可玩性体现在它的上手简单(只需要点击相同的泡泡糖便可消除),但复杂的运行逻辑又使它的形式十分丰富,同时我们所加入的特有的 关卡设计 也让这个游戏更符合当下青年想要挑战的心理。

这款泡泡糖的可玩性的突出之处在于简单的消除设计和丰富的关卡设计和积分奖励。

消除设计思路: 点击屏幕上任意一个”泡泡糖”,与之相连的同颜色泡泡糖也会一并消除。因此在写消除方面的代码时,需要找到所有靠近点击目标的同色样式,需要反复利用循环来完成我们的设计。但是在前端界面却能十分简单地展示,这就是泡泡糖的可玩性。

关卡设计和积分奖励思路: 通过计算每局游戏的得分,来判断玩家是否能够通过关卡。同时在每次通关时我们还会给予玩家一定的通关积分奖励,这样一方面降低了玩家的通关难度,另一方面也让玩家更有动力玩我们这款泡泡糖游戏。

1.1.2 可视性

单一的游戏展示界面会让玩家很快感受到无聊,从而降低对泡泡糖游戏的兴趣,因此,我们为泡泡糖的主界面设计了不同的主题风格,而切换主题的方式也相当简便,进一步完善了游戏的整体。

展示如下:

1.2 玩家分析

设计一款游戏首先要明确定位玩家群体,根据玩家对游戏的期望和需求进行游戏的设计和调整。因此,在软件实际开发前的准备工作中,玩家分析是十分关键的一步。

以《消灭泡泡糖》这款游戏时为例,该游戏是为了做出一个耐玩有趣的休闲类消除小游戏,供玩家娱乐消遣。以下是一些包含的功能:

随机泡泡糖 — 完成每一关挑战后,系统自动补全随机颜色的泡泡糖

主题切换功能 — 玩家可选择不同主题,增强游戏个性化元素

目标分数显示 — 玩家可清楚看到当前分数与目标分数

额外奖励积分 — 无可消灭泡泡糖后判断剩余泡泡糖的奖励机制

在后续《消灭泡泡糖》的设计中,也主要围绕这些功能来展开并完善。

第二章 概要设计

2.1 设计思路

任何一个项目在实际开发前,都要有一个明确的思路和整体框架的设计,正如本项目目录所显示的那样,对于《消灭泡泡糖》的设计,我的思路是:

① 设计一个精美的界面和一个完整的游戏框架;

② 显示10×10的泡泡糖矩阵

③ 消除泡泡糖(获取待消除的泡泡糖和封装待消除的泡泡糖)

④ 泡泡糖的移动(移动垂直方向上和水平方向上的泡泡糖)

⑤ 显示关卡积分(更新关卡分数和实现积分规则)

根据上述描述设计的流程图:

设计流程图
设计流程图

2.2 游戏流程与整体框架(以及重点UML类图)

2.2.1 游戏流程

游戏流程

流程图如下所示:

游戏流程图
游戏流程图

2.2.2 整体框架

本消灭泡泡糖游戏项目以Java作为核心开发语言,依托 JavaFX 技术实现游戏功能设计与界面渲染,同时由元素,主题和控制器搭建了整体架构,确保代码模块化、逻辑清晰,便于开发维护与功能扩展。

  • 模型: 泡泡糖元素类Star(包含了泡泡糖的颜色、位置等信息)、MovedStar、得分等等
  • 主题: src-rex.layout-main_layout.fxml 文件
  • 控制器: 主要在MainForm文件中,设计了事件处理对象,负责相应玩家的输入,随之调整模型和视图。后续泡泡糖的消除和移动会广泛应用这方面。

2.2.3 本项目对应的企业级开发流程

企业级开发流程
企业级开发流程

第三章 泡泡糖的显示

3.1 显示泡泡糖(定位PRJ-BU2-JAVA-002)

核心组件:

核心组件
核心组件

UML类图绘制:

UML类图
UML类图

3.1.1 创建《消灭泡泡糖》实体类

本场景需要实现在游戏界面上呈现5个不同颜色的泡泡糖,并且泡泡糖的位置由坐标决定,具体操作需要创建Star,Position等实体类,并设置其中的成员方法和成员变量,同时还需要创建枚举类型StarType(Star类中),并且要添加返回整数类型的公共函数vaule。

以下是关键代码摘取:

Position类
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
package cn.campsg.practical.bubble.entity;


public class Position {

private int row; // 行索引

private int column; // 列索引


public Position() { // 无参构造方法

super();

// TODO Auto-generated constructor stub

}


public Position(int row, int column) { // 有参构造方法,初始化行列位置

super();

this.row = row; // 赋值行索引

this.column = column; // 赋值列索引

}


public int getRow() { // 获取行索引

return row;

}


public void setRow(int row) { // 设置行索引

this.row = row;

}


public int getColumn() { // 获取列索引

return column;

}


public void setColumn(int column) { // 设置列索引

this.column = column;

}

}
Star类
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
public class Star {

private Position position = null; // 泡泡糖在二维坐标系中的位置,初始化为null

private StarType type = null; // 泡泡糖的类型(颜色),初始化为null


public Position getPosition() { // 获取泡泡糖的位置

return position;

}

public void setPosition(Position position) { // 设置泡泡糖的位置

this.position = position;

}

public StarType getType() { // 获取泡泡糖的类型

return type;

}

public void setType(StarType type) { // 设置泡泡糖的类型

this.type = type;

}

}
StarType类
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
public enum StarType{

BLUE(0),GREEN(1),YELLOW(2),RED(3),PURPLE(4); // 定义星星类型枚举值及对应数值

private int value = 0; // 存储枚举值对应的数值


// 枚举构造函数,初始化枚举值对应的数值

private StarType(int value){

this.value = value;

}


// 枚举成员方法,返回当前枚举值对应的数值

public int value(){

return this.value;

}

}
测试类代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public StarList createStars() {

Star star1 = new Star(); // 创建第一个泡泡糖对象

star1.setPosition(new Position(0,0)); // 设置第一个泡泡糖位置为(0,0)

star1.setType(StarType.BLUE); // 设置第一个泡泡糖类型为蓝色

Star star2 = new Star(); // 创建第二个泡泡糖对象

star2.setPosition(new Position(1,1)); // 设置第二个泡泡糖位置为(1,1)

star2.setType(StarType.GREEN); // 设置第二个泡泡糖类型为绿色

// 打印第一个泡泡糖的列、行坐标及类型

System.out.println("("+star1.getPosition().getColumn()+")"+","+"("+(star1.getPosition().getRow()+")"+" "+"-"+" "+star1.getType()));

// 打印第二个泡泡糖的列、行坐标及类型

System.out.println("("+star2.getPosition().getColumn()+")"+","+"("+(star2.getPosition().getRow()+")"+" "+"-"+" "+star2.getType()));


}

运行结果如下所示:

运行结果
运行结果

3.1.2 优化《消灭泡泡糖》实体类

在3.1.1中,Star类中的StarType枚举不够完善,枚举类型只能使用,不能实现类型转换。当前任务主要是为枚举添加转换函数,实现数值向枚举的转换。(0→BLUE)

StarType类
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
public enum StarType{
BLUE(0),GREEN(1),YELLOW(2),RED(3),PURPLE(4); // 定义星星类型枚举值及对应数值
private int value = 0; // 存储枚举值对应的数值

// 枚举构造函数,初始化枚举值对应的数值
private StarType(int value){
this.value = value;
}

// 枚举成员方法,返回当前枚举值对应的数值
public int value(){
return this.value;
}

// 静态方法,根据数值获取对应的星星类型
public static StarType valueOf(int value){
switch(value){
case 0:
return BLUE;
case 1:
return GREEN;
case 2:
return YELLOW;
case 3:
return RED;
case 4:
return PURPLE;
default:
return null; // 数值不匹配时返回null
}
}
}

测试类代码
1
2
3
4
5
6
7
8
9
10
11
public StarList createStars() {
Star star1 = new Star(); // 创建第一个泡泡糖对象
star1.setPosition(new Position(0,0)); // 设置第一个泡泡糖位置为(0,0)
star1.setType(StarType.BLUE); // 设置第一个泡泡糖类型为蓝色
Star star2 = new Star(); // 创建第二个泡泡糖对象
star2.setPosition(new Position(1,1)); // 设置第二个泡泡糖位置为(1,1)
star2.setType(StarType.GREEN); // 设置第二个泡泡糖类型为绿色
System.out.println(StarType.BLUE.value()); // 打印蓝色泡泡糖类型对应的数值(0)
System.out.println(StarType.valueOf(1)); // 打印数值1对应的泡泡糖类型(GREEN)
}

运行结果如下所示:

运行结果
运行结果

3.1.3 游戏界面呈现泡泡糖

3.1.1和3.1.2均利用了控制台显示了泡泡糖数据,当前任务将会把Star类数据显示在游戏界面上。

测试类代码
1
2
3
4
5
6
7
8
9
10
11
12
public StarList createStars() {
StarList starList = new StarList(); // 初始化泡泡糖列表
// 向列表中添加不同位置和类型的泡泡糖
starList.add(new Star(new Position(0,0),StarType.BLUE)); // 添加位置(0,0)的蓝色泡泡糖
starList.add(new Star(new Position(1,1),StarType.RED)); // 添加位置(1,1)的红色泡泡糖
starList.add(new Star(new Position(2,2),StarType.YELLOW)); // 添加位置(2,2)的黄色泡泡糖
starList.add(new Star(new Position(3,3),StarType.GREEN)); // 添加位置(3,3)的绿色泡泡糖
starList.add(new Star(new Position(4,4),StarType.PURPLE)); // 添加位置(4,4)的紫色泡泡糖

return starList; // 返回创建好的泡泡糖列表
}

运行结果如下所示:

界面呈现5个泡泡糖
界面呈现5个泡泡糖

3.2 随机显示泡泡糖(定位PRJ-BU2-JAVA-003)

在1.1中我们已经实现在界面上的”指定位置显示指定颜色的泡泡糖”,在这一模块将进一步利用for循环的嵌套使用以及随机数的产生等方法在界面上显示随机产生的10×10的泡泡糖矩阵。

核心组件:

核心组件
核心组件

UML类图绘制:

UML类图
UML类图

3.2.1 显示一行泡泡糖

createStars类
1
2
3
4
5
6
7
8
9
10
11
// 循环遍历行中的每一列(从第0列到MAX_ROW_SIZE-1列)
for(int j=0; j<MAX_ROW_SIZE; j++){
// 创建一个新的红泡泡糖对象,位置在第0行第j列
// Position(0,j)表示坐标位置,第一个参数是行索引,第二个参数是列索引
// StarType.RED指定泡泡糖的类型为红色(此处类型名仍为StarType,可根据实际情况调整)
Star star = new Star(new Position(0,j), StarType.RED);

// 将创建的泡泡糖对象添加到泡泡糖列表中
stars.add(star);
}

运行结果如下图所示:

显示一行泡泡糖
显示一行泡泡糖

3.2.2 显示10 × 10泡泡糖矩阵

createStars类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 外层循环遍历每一列(从第0列到MAX_COLUMN_SIZE-1列)
for(int i = 0; i < MAX_COLUMN_SIZE; i++){
// 内层循环遍历当前列中的每一行(从第0行到MAX_ROW_SIZE-1行)
for(int j = 0; j < MAX_ROW_SIZE; j++){
// 创建一个新的泡泡糖对象,位置在第j行第i列
// Position(j,i)表示坐标位置,第一个参数是行索引,第二个参数是列索引
// StarType.valueOf(3)指定泡泡糖的类型为值为3对应的类型(此处类型名仍为StarType,可根据实际情况调整)
Star star = new Star(new Position(j, i), StarType.valueOf(3));

// 将创建的泡泡糖对象添加到泡泡糖列表中
stars.add(star);
}
}

运行结果如下图所示:

10×10固定颜色泡泡糖矩阵
10×10固定颜色泡泡糖矩阵

3.2.3 随机显示10 × 10泡泡糖矩阵

3.2.1和3.2.2实现了在界面上呈现10×10泡泡糖矩阵的效果,但当前游戏界面只能固定显示100个红色泡泡糖。本任务将实现泡泡糖的颜色的随机设置,满足游戏的基本要素。

createStars类
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
public StarList createStars() {
// 创建一个新的泡泡糖列表对象,用于存储所有生成的泡泡糖
StarList stars = new StarList();

// 外层循环遍历每一行(从第0行到MAX_ROW_SIZE-1行)
for(int row = 0; row < MAX_ROW_SIZE; row++) {
// 内层循环遍历当前行中的每一列(从第0列到MAX_COLUMN_SIZE-1列)
for(int column = 0; column < MAX_COLUMN_SIZE; column++) {
// 创建一个新的泡泡糖对象
Star star = new Star();

// 设置泡泡糖的位置为当前行和列对应的坐标(row行,column列)
star.setPosition(new Position(row,column));

// 生成一个随机索引值,范围是0到StarService.STAR_TYPES-1
// 用于随机选择泡泡糖的类型
int typeIndex = (int)(Math.random()*StarService.STAR_TYPES);

// 根据随机生成的索引设置泡泡糖的类型
star.setType(StarType.valueOf(typeIndex));

// 将创建好的泡泡糖添加到泡泡糖列表中
stars.add(star);
}
}

// 返回包含所有生成泡泡糖的列表
return stars;
}

运行结果如下图所示:

随机10×10泡泡糖矩阵
随机10×10泡泡糖矩阵

第四章 泡泡糖的消除

4.1 获得待消除的泡泡糖(定位PRJ-BU2-JAVA-004)

核心组件:

核心组件
核心组件

UML类图绘制:

UML类图
UML类图

4.1.1 泡泡糖克隆函数

克隆流程说明
克隆流程说明
clone函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Star clone(Star star) {
// 创建一个新的泡泡糖对象作为克隆结果
Star ret = new Star();

// 复制原泡泡糖的位置信息,创建新的Position对象以避免引用共享
ret.setPosition(new Position(star.getPosition().getRow(), star.getPosition().getColumn()));

// 复制原泡泡糖的类型信息
ret.setType(star.getType());

// 返回克隆得到的新泡泡糖对象
return ret;
}

测试类代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
// 创建一个位置在(5,5)的红色泡泡糖对象s1
Star s1 = new Star(new Position(5,5), StarType.RED);

// 使用工具类克隆s1得到新的泡泡糖对象s2
Star s2 = StarsUtil.clone(s1);

// 打印原泡泡糖对象信息
System.out.println("原泡泡糖对象为:" + s1);

// 打印克隆得到的泡泡糖对象信息
System.out.println("克隆泡泡糖对象为:" + s2);

// 比较两个泡泡糖对象是否为同一个对象(地址比较)
System.out.println("两对象是否一致:" + s1.equals(s2));

// 比较两个泡泡糖对象的坐标是否相同
System.out.println("两对象的坐标对象是否一致:" + s1.getPosition().equals(s2.getPosition()));
}

运行结果如下图所示:

克隆测试结果
克隆测试结果

4.1.2 查询某个泡泡糖左侧同色泡泡糖

实现流程(右侧,顶部和底部与左侧相似,因此只列举一个作为参考):

左侧查找流程
左侧查找流程
左侧消除代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 检查左侧是否有可匹配的泡泡糖
if (column > 0) {
// 获取当前位置左侧相邻(同一行,前一列)的泡泡糖
Star star = sList.lookup(row, column - 1);

// 判断左侧泡泡糖是否存在、未被标记清除且与当前泡泡糖类型相同
if ((star != null) && (!clearStars.existed(star)) && (star.getType() == type)) {
// 将左侧泡泡糖的克隆添加到待清除列表中
clearStars.add(StarsUtil.clone(star));
// 递归查找与左侧泡泡糖相连的其他可清除泡泡糖
lookupByPath(star, sList, clearStars);
}
}

运行结果如下图所示:

左侧消除结果
左侧消除结果

4.1.3 查询某个泡泡糖右侧同色泡泡糖

右侧消除代码
1
2
3
4
5
6
7
8
9
10
// 检查右侧是否有可匹配的泡泡糖
if (column < StarService.MAX_COLUMN_SIZE - 1) {
Star star = sList.lookup(row, column + 1);

if ((star != null) && (!clearStars.existed(star)) && (star.getType() == type)) {
clearStars.add(StarsUtil.clone(star));
lookupByPath(star, sList, clearStars);
}
}

运行结果如下图所示:

右侧消除结果
右侧消除结果

4.1.4 查询某个泡泡糖顶部泡泡糖

顶部消除代码
1
2
3
4
5
6
7
8
9
// 检查顶部是否有可匹配的泡泡糖
if(row > 0) {
Star star = sList.lookup(row - 1, column);
if((star != null)&&(!clearStars.existed(star))&&(star.getType() == type)) {
clearStars.add(StarsUtil.clone(star));
lookupByPath(star,sList,clearStars);
}
}

运行结果如下图所示:

顶部消除结果
顶部消除结果

4.1.5 查询某个泡泡糖底部泡泡糖

底部消除代码
1
2
3
4
5
6
7
8
9
// 检查底部是否有可匹配的泡泡糖
if(row < StarService.MAX_ROW_SIZE - 1) {
Star star = sList.lookup(row + 1, column);
if((star != null)&&(!clearStars.existed(star))&&(star.getType() == type)) {
clearStars.add(StarsUtil.clone(star));
lookupByPath(star,sList,clearStars);
}
}

运行结果如下图所示:

底部消除结果
底部消除结果

4.2 封装待移动的泡泡糖(定位PRJ-BU2-JAVA-005)

核心组件:

核心组件
核心组件

UML类图绘制:

UML类图
UML类图

4.2.1 创建待移动泡泡糖实体类

创建一个用于描述【待移动泡泡糖】实体类,不但具有泡泡糖的所有特性(位置,类型),同时还具有自己独立的属性:待移动位置(坐标),两个实体类(待移动泡泡糖和泡泡糖)之间存在继承关系。

MoveStar类
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
// MovedStar类继承是Star的子类,用于表示有移动轨迹的泡泡糖
public class MovedStar extends Star {
// 记录泡泡糖移动后的位置
private Position movedPosition;

// 构造方法:创建有初始位置、类型和移动后位置的MovedStar对象
// 参数分别为:初始位置、泡泡糖类型、移动后的位置
public MovedStar(Position position, StarType type, Position movedPosition) {
// 调用父类构造方法,初始化泡泡糖的初始位置和类型
super(new Position(position.getRow(), position.getColumn()), type);
// 设置泡泡糖移动后的位置
this.movedPosition = movedPosition;
}

// 无参构造方法:创建默认的MovedStar对象
public MovedStar() {
super();
}

// 获取泡泡糖移动后的位置
public Position getMovedPosition() {
return movedPosition;
}

// 设置泡泡糖移动后的位置
public void setMovedPosition(Position movedPosition) {
this.movedPosition = movedPosition;
}
}

测试类代码
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
// 创建泡泡糖的初始位置对象(坐标为(0,0))
Position position = new Position(0, 0);
// 创建泡泡糖的移动目标位置对象(坐标为(1,1))
Position movedPosition = new Position(1, 1);
// 设置泡泡糖的颜色类型为红色
StarType type = StarType.RED;
System.out.println("泡泡糖原位置:" + position);
System.out.println("泡泡糖移动的目标位置:" + movedPosition);
System.out.println("泡泡糖的颜色为:" + type);
}

运行结果如下图所示:

测试结果
测试结果

4.2.2 实体类的文本化输出函数

本节将利用面向对象三要素中的多态重写特性体验实体继承的优势。

重写Position类的toString方法
1
2
3
4
5
public String toString() {
// TODO Auto-generated method stub
return "position:"+"("+getRow()+","+getColumn()+")";
}

重写Star类的toString方法
1
2
3
4
5
public String toString() {
// TODO Auto-generated method stub
return position.toString()+","+"type:"+getType();
}

重写MovedStar类的toString方法
1
2
3
4
5
public String toString() {
// TODO Auto-generated method stub
return super.toString()+"\nnew"+ movedPosition.toString();
}

测试类代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] a) {
// 创建一个可移动泡泡糖对象
MovedStar movedStar = new MovedStar();

// 设置泡泡糖的初始位置为(0,0)
movedStar.setPosition(new Position(0, 0));

// 设置泡泡糖的移动后位置为(1,1)
movedStar.setMovedPosition(new Position(1, 1));

// 设置泡泡糖的类型为蓝色
movedStar.setType(StarType.BLUE);

// 打印输出该可移动泡泡糖的信息(具体内容取决于toString()方法的实现)
System.out.println(movedStar.toString());
}

运行结果如下图所示:

多态测试结果
多态测试结果

4.2.3 实现移动泡泡糖封装

本节用于计算并获取垂直方向的【待移动泡泡糖】。

getYMovedStars类
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
public StarList getYMovedStars(StarList clearStars, StarList currentStarList) {
// 创建一个用于存储移动泡泡糖的列表
StarList movedStarList = new StarList();

// 从第6行开始向上遍历至第0行
for(int row = 6; row >= 0; row--) {
// 获取当前行第0列的泡泡糖
Star star = currentStarList.lookup(row, 0);

// 计算泡泡糖移动后的位置(当前行+2,列不变仍为0)
Position movedPosition = new Position(row + 2, 0);

// 获取泡泡糖的原始位置
Position position = new Position(star.getPosition().getRow(), star.getPosition().getColumn());

// 创建移动泡泡糖对象,包含原始位置、类型和移动后位置
MovedStar movedStar = new MovedStar(position, star.getType(), movedPosition);

// 将移动泡泡糖添加到列表中
movedStarList.add(movedStar);
}

// 返回包含所有移动泡泡糖的列表
return movedStarList;
}

运行结果如下图所示:

4.3 体验接口解耦特性(定位PRJ-BU2-JAVA-006)

核心组件:

核心组件
核心组件

UML类图:

UML类图
UML类图

4.3.1 创建服务测试类

StarServiceTester类
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
63
64
65
66
67
68
69
70
71
public class StarServiceTester implements StarService {
public static void main(String[] args) {
// 创建StarServiceTester实例作为测试服务对象
StarService serviceTester = new StarServiceTester();

// 调用createStars方法创建泡泡糖列表
StarList starList = serviceTester.createStars();

// 打印输出创建的泡泡糖列表信息
System.out.println(starList);
}
@Override
public StarList createStars() {
// 创建一个新的泡泡糖列表
StarList starList = new StarList();

// 创建位置在(0,0)的蓝色泡泡糖并添加到列表
Star star1 = new Star(new Position(0,0), StarType.BLUE);
starList.add(star1);

// 创建位置在(0,1)的绿色泡泡糖并添加到列表
Star star2 = new Star(new Position(0,1), StarType.GREEN);
starList.add(star2);

// 创建位置在(1,0)的紫色泡泡糖并添加到列表
Star star3 = new Star(new Position(1,0), StarType.PURPLE);
starList.add(star3);

// 创建位置在(1,1)的黄色泡泡糖并添加到列表
Star star4 = new Star(new Position(1,1), StarType.YELLOW);
starList.add(star4);

// 创建位置在(0,2)的红色泡泡糖并添加到列表
Star star5 = new Star(new Position(0,2), StarType.RED);
starList.add(star5);

// 返回创建好的泡泡糖列表
return starList;
}

@Override
public StarList tobeClearedStars(Star base, StarList sList) {
// 待实现:获取需要清除的泡泡糖列表
return null;
}

@Override
public StarList getYMovedStars(StarList clearStars, StarList currentStarList) {
// 待实现:获取在Y轴方向移动的泡泡糖列表
return null;
}

@Override
public StarList getXMovedStars(StarList currentStarList) {
// 待实现:获取在X轴方向移动的泡泡糖列表
return null;
}

@Override
public boolean tobeEliminated(StarList currentStarList) {
// 待实现:判断是否有需要消除的泡泡糖
return false;
}

@Override
public StarList getAwardStarList(StarList currentStarList) {
// 待实现:获取奖励泡泡糖列表
return null;
}
}

运行结果如下图所示:

服务测试结果
服务测试结果

4.3.2 实现界面泡泡糖显示及通过动态接口切换

因两节关联性较高,所以放在一起进行整理。

initGameStars类
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
private void initGameStars(AnchorPane root) {
// 获取窗体中用于显示泡泡糖的区域(游戏面板)
mStarForm = (AnchorPane) root.lookup("#game_pane");

// 获取泡泡糖服务实例,用于创建和管理泡泡糖
StarService service = this.getStarService();
// 通过服务创建当前游戏的泡泡糖列表
mCurretStars = service.createStars();
// 遍历泡泡糖列表,为每个泡泡糖创建并配置显示控件
for (int i = 0; i < mCurretStars.size(); i++) {
// 从集合中获取当前泡泡糖对象
Star star = mCurretStars.get(i);

// 创建用于显示泡泡糖的标签控件
Label starFrame = new Label();
// 设置标签的宽高为48x48像素
starFrame.setPrefWidth(48);
starFrame.setPrefHeight(48);

// 获取当前泡泡糖的行列坐标
int row = star.getPosition().getRow();
int col = star.getPosition().getColumn();

// 设置标签的ID,格式为"s+列号+行号"
starFrame.setId("s" + col + row);
// 存储用户数据为"列号;行号"格式
starFrame.setUserData(col + ";" + row);

// 设置标签在面板中的位置(根据行列计算坐标)
starFrame.setLayoutX(col * 48);
starFrame.setLayoutY(row * 48);
// 根据泡泡糖的类型设置不同的样式类(控制显示颜色)
switch (star.getType().value()) {
case 0:
starFrame.getStyleClass().add("blue_star"); // 蓝色泡泡糖样式
break;
case 1:
starFrame.getStyleClass().add("green_star"); // 绿色泡泡糖样式
break;
case 2:
starFrame.getStyleClass().add("yellow_star");// 黄色泡泡糖样式
break;
case 3:
starFrame.getStyleClass().add("red_star"); // 红色泡泡糖样式
break;
case 4:
starFrame.getStyleClass().add("purple_star");// 紫色泡泡糖样式
break;
}
// 将配置好的泡泡糖显示控件添加到游戏面板中
mStarForm.getChildren().add(starFrame);
}
}

通过动态接口切换(定位 bean.conf):

4.4 体验接口隔离性(定位PRJ-BU2-JAVA-007)

核心组件:

核心组件
核心组件

UML类图:

UML类图
UML类图

4.4.1 处理泡泡糖点击事件

创建Lable控件【点击事件处理类】,并实现【点击事件】处理接口EventHandler。

StartEventHandler类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class StartEventHandler implements EventHandler<MouseEvent>{
public void handle(MouseEvent event) {
// TODO Auto-generated method stub
//获取被点击的泡泡糖显示控件
Label starFrame = (Label) event.getTarget();
//将试图转换为泡泡糖对象
Star base = StarFormUtils.convert(starFrame);
System.out.println(base);
StartEventHandler sta = new StartEventHandler(getStarService());
starFrame.setOnMouseClicked(sta);
// 将泡泡糖加入到窗体中显示泡泡糖的区域
mStarForm.getChildren().add(starFrame);
}
}

运行结果如下图所示:

点击事件测试结果
点击事件测试结果

4.4.2 实现点击-消除效果(不考虑移动)

点击界面上任意泡泡糖,界面实现消除泡泡糖操作。

StartEventHandler类
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
// 泡泡糖点击事件处理器
class StartEventHandler implements EventHandler<MouseEvent> {
// 泡泡糖业务服务对象
private StarService starService = null;

// 构造方法:传入泡泡糖业务服务
public StartEventHandler(StarService starService) {
this.starService = starService;
}

@Override
public void handle(MouseEvent event) {
// 获取被点击的泡泡糖显示控件
Label starFrame = (Label)event.getTarget();
// 转换为泡泡糖数据对象
Star base = StarFormUtils.convert(starFrame);
// 打印被点击的泡泡糖信息
System.out.println(base);

// 调用服务获取待清除的泡泡糖列表
StarList starlist = this.starService.tobeClearedStars(base, mCurretStars);
// 无待清除泡泡糖则返回
if (starlist == null || starlist.size() == 0) {
return;
}
// 遍历处理待清除泡泡糖
else {
for(int i=0;i<starlist.size();i++){
Star star = starlist.get(i);
// 找到对应的显示控件
Label lable = StarFormUtils.findFrame(star, mStarForm);
// 执行清除动画
StarAnimation.clearStarLable(mStarForm, lable);
// 在数据列表中标记为空
mCurretStars.setNull(star.getPosition().getRow(), star.getPosition().getColumn());
}
}
}
}

运行结果如下图所示:

第五章 泡泡糖的移动

5.1 移动垂直方向的泡泡糖(一)(定位PRJ-BU2-JAVA-010)

核心组件:

核心组件
核心组件

UML类图:

UML类图
UML类图

5.1.1 交换两个泡泡糖

本场景采用冒泡排序算法来实现,在实现前我们需要先完成排序算法的基础——交换函数。实现方法可参考下图:交换对象时务必交换属性

交换函数参考图
交换函数参考图
swap类
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
public static void main(String[] args) {
/*
Star star1 = new Star(new Position(0,0),Star.StarType.BLUE);
Star star2 = new Star(new Position(1,1),Star.StarType.GREEN);

System.out.println("交换前:preStar:" + star1.toString() + " nextStar:" + star2.toString());

swap(star1,star2);

System.out.println("交换后:preStar:" + star1.toString() + " nextStar:" + star2.toString());
*/

// 创建5个不同位置和类型的泡泡糖对象
Star star1 = new Star(new Position(2,0), Star.StarType.BLUE);
Star star2 = new Star(new Position(5,0), Star.StarType.GREEN);
Star star3 = new Star(new Position(9,0), Star.StarType.PURPLE);
Star star4 = new Star(new Position(3,0), Star.StarType.RED);
Star star5 = new Star(new Position(8,0), Star.StarType.YELLOW);

// 创建泡泡糖列表并添加上述泡泡糖
StarList sList = new StarList();
sList.add(star1);
sList.add(star2);
sList.add(star3);
sList.add(star4);
sList.add(star5);

// 打印排序前的泡泡糖列表
System.out.println("排序前:starList:");
for(int i = 0; i < 4; i++) {
System.out.print(sList.get(i).toString() + " ,");
}
System.out.print(sList.get(4).toString());

// 对泡泡糖列表进行排序
sort(sList);

// 打印排序后的泡泡糖列表
System.out.println("\n排序后:starList:");
for(int i = 0; i < 4; i++) {
System.out.print(sList.get(i).toString() + " ,");
}
System.out.print(sList.get(4).toString());
}

测试类代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] a) {
// 创建第一个泡泡糖:位置(0,0)、类型蓝色
Star preStar = new Star(new Position(0, 0), StarType.BLUE);
// 创建第二个泡泡糖:位置(1,1)、类型绿色
Star nextStar = new Star(new Position(1, 1), StarType.GREEN);

// 打印交换前第一个泡泡糖的信息
System.out.println(preStar);
// 打印交换前第二个泡泡糖的信息
System.out.println(nextStar);

// 调用工具类方法,交换两个泡泡糖的数据(如位置、类型)
StarsUtil.swap(preStar, nextStar);

// 打印交换后第一个泡泡糖的信息
System.out.println(preStar);
// 打印交换后第二个泡泡糖的信息
System.out.println(nextStar);
}

运行结果如下图所示:

交换测试结果
交换测试结果

5.1.2 泡泡糖集合的排序

对”待消除泡泡糖”进行排序操作。

sort类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 对泡泡糖列表进行排序(按泡泡糖所在行号升序排序)
public static void sort(StarList starList) {
// 外层循环:控制排序轮次(共需 size-1 轮,每轮确定一个最大行号的泡泡糖位置)
for (int i = 0; i < starList.size() - 1; i++) {
// 内层循环:每轮比较相邻泡泡糖,将大的行号往后移(每轮少比较 i 个已排好的元素)
for (int j = 0; j < starList.size() - i - 1; j++) {
// 获取当前位置和下一个位置的泡泡糖
Star preStar = starList.get(j);
Star nextStar = starList.get(j + 1);

// 比较两个泡泡糖的行号:若前一个行号大于后一个,交换两者数据
if (preStar.getPosition().getRow() > nextStar.getPosition().getRow()) {
swap(preStar, nextStar);
}
}
}
}

sort算法说明:

sort算法说明
sort算法说明
测试类代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
// 创建泡泡糖列表对象,用于存储多个泡泡糖
StarList starList = new StarList();

// 向列表中添加5个不同行位置(列均为0)、不同颜色的泡泡糖
starList.add(new Star(new Position(2, 0), StarType.BLUE)); // 行2、列0,蓝色泡泡糖
starList.add(new Star(new Position(5, 0), StarType.GREEN)); // 行5、列0,绿色泡泡糖
starList.add(new Star(new Position(9, 0), StarType.PURPLE)); // 行9、列0,紫色泡泡糖
starList.add(new Star(new Position(3, 0), StarType.RED)); // 行3、列0,红色泡泡糖
starList.add(new Star(new Position(8, 0), StarType.YELLOW)); // 行8、列0,黄色泡泡糖

// 打印排序前的泡泡糖列表(展示初始顺序)
System.out.println(starList);

// 调用工具类方法,按泡泡糖的行号升序排序
StarsUtil.sort(starList);

// 打印排序后的泡泡糖列表(展示按行号从小到大的顺序)
System.out.println(starList);
}

运行结果如下图所示:

排序测试结果
排序测试结果

5.1.3 移动垂直方向的泡泡糖

通过getYMovedStars函数实现获取”垂直方向待移动泡泡糖”的集合。并在界面上实现泡泡糖的消除与移动。

具体流程:

垂直移动流程
垂直移动流程
GetYMovedStars类
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
public StarList getYMovedStars(StarList clearStars, StarList currentStarList) {
// 若无可清除的泡泡糖,直接返回null
if (clearStars == null || clearStars.size() == 0)
return null;

// 对可清除的泡泡糖列表进行排序
StarsUtil.sort(clearStars);

// 创建存储移动泡泡糖的列表
StarList moveStars = new StarList();
// 记录移动跨度(受清除泡泡糖影响的偏移量)
int span = 0;
// 处理的列号(固定为0列)
int column = 0;
// 从可清除泡泡糖的最大行号开始处理
int startPosition = clearStars.lastElement().getPosition().getRow();

// 从最大行号向上遍历至第0行
for (int row = startPosition; row >= 0; row--) {
// 获取当前行列的泡泡糖
Star star = currentStarList.lookup(row, column);
// 若当前位置无泡泡糖,退出循环
if (star == null)
break;

// 若当前泡泡糖是待清除的,增加跨度并跳过
if (clearStars.existed(star)) {
span++;
continue;
}

// 创建移动泡泡糖对象
MovedStar movedStar = new MovedStar();
// 设置原始位置
movedStar.setPosition(new Position(star.getPosition().getRow(), star.getPosition().getColumn()));
// 设置泡泡糖类型
movedStar.setType(star.getType());
// 设置移动后的位置(原始行号+跨度)
movedStar.setMovedPosition(new Position(star.getPosition().getRow() + span, star.getPosition().getColumn()));

// 将移动泡泡糖添加到列表
moveStars.add(movedStar);
}

return moveStars;
}

运行结果如下图所示:

5.2 移动垂直方向的泡泡糖(二)(定位PRJ-BU2-JAVA-011)

核心组件:

核心组件
核心组件

UML类图:

UML类图
UML类图

5.2.1 根据坐标和位置查找泡泡糖

本节对ArrayList进行扩展,实现根据行,列值查找泡泡糖的方法 & 利用Java的重载机制,实现根据位置【Position】查找泡泡糖的方法。

根据坐标寻找的lookup函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 根据行号和列号查找对应的泡泡糖
public Star lookup(int row, int column) {
// 遍历列表中的所有泡泡糖
for (int i = 0; i < super.size(); i++) {
// 跳过空元素
if (super.get(i) == null) {
continue;
}

// 获取当前泡泡糖的位置信息
Star currentStar = super.get(i);
Position pos = currentStar.getPosition();

// 若行列号匹配,返回找到的泡泡糖
if (pos.getRow() == row && pos.getColumn() == column) {
return currentStar;
}
}

// 未找到匹配的泡泡糖,返回null
return null;
}

根据位置寻找的lookup函数
1
2
3
4
5
6
// 根据位置对象查找查找对应的泡泡糖
// 调用重载方法,传入位置的行号和列号进行查找
public Star lookup(Position position) {
return lookup(position.getRow(), position.getColumn());
}

5.2.2 判断泡泡糖是否存在

利用上一节的 lookup 函数,实现判断泡泡糖是否存在于集合的函数 existed

existed类
1
2
3
4
5
6
7
8
9
10
11
12
13
// 判断指定泡泡糖是否存在于列表中
public boolean existed(Star star) {
// 若传入的泡泡糖为null,直接返回不存在
if (star == null)
return false;

// 根据泡泡糖的位置查找列表中是否有匹配的泡泡糖
Star ret = lookup(star.getPosition());

// 若找到匹配的泡泡糖则返回true,否则返回false
return ret == null ? false : true;
}

运行结果如下图所示:

5.3 移动垂直方向上的泡泡糖(三)(定位PRJ-BU2-JAVA-012)

核心组件:

核心组件
核心组件

UML类图:

UML类图
UML类图

5.3.1 更新集合的排序算法

本节在前两个任务的基础上针对”待消除泡泡糖”的排序由单列变多列。

实现流程:

多列排序流程
多列排序流程
sort类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 对泡泡糖列表进行排序(先按列号升序,列号相同则按行号升序)
public static void sort(StarList starList) {
// 外层循环:控制排序轮次(共需 size 轮)
for (int i = 0; i < starList.size(); i++) {
// 内层循环:每轮比较相邻元素,将较大元素后移(每轮少比较 i 个已排好的元素)
for (int j = 0; j < starList.size() - i - 1; j++) {
// 获取当前位置和下一个位置的泡泡糖
Star preStar = starList.get(j);
Star nextStar = starList.get(j + 1);

// 比较列号:若前一个列号大于后一个,交换两个泡泡糖
if (preStar.getPosition().getColumn() > nextStar.getPosition().getColumn()) {
swap(preStar, nextStar);
}
// 若列号相同,比较行号:若前行号大于后行号,交换两个泡泡糖
else if (preStar.getPosition().getColumn() == nextStar.getPosition().getColumn()) {
if (preStar.getPosition().getRow() > nextStar.getPosition().getRow()) {
swap(preStar, nextStar);
}
}
}
}
}

测试类代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(String[] args) {
// 创建泡泡糖列表
StarList starlist = new StarList();

// 添加不同位置和类型的泡泡糖
starlist.add(new Star(new Position(2, 3), StarType.BLUE)); // 位置(2,3),蓝
starlist.add(new Star(new Position(1, 5), StarType.GREEN)); // 位置(1,5),绿
starlist.add(new Star(new Position(0, 9), StarType.PURPLE)); // 位置(0,9),紫
starlist.add(new Star(new Position(0, 3), StarType.RED)); // 位置(0,3),红
starlist.add(new Star(new Position(0, 8), StarType.YELLOW)); // 位置(0,8),黄

// 打印排序前的列表
System.out.println(starlist);

// 按规则排序(先列号升序,列号相同则行号升序)
sort(starlist);

// 打印排序后的列表
System.out.println(starlist);
}

运行结果如下图所示:

多列排序结果
多列排序结果

5.3.2 待消除泡泡糖的排序与分组

为确保界面能够按照从左向右,从上到下的顺序移动所有”待移动的泡泡糖”,我们需要对已经排序完毕的”待移动泡泡糖”进行分组,分组后原本挤压在一个集合中的所有”待消除的泡泡糖”将会被按(列)组成多个”待消除的泡泡糖”集合,每个集合中的”待消除的泡泡糖”仅行号不同,列号均相同(如下)。

分组示意
分组示意
group类
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
// 按列号对泡泡糖列表进行分组,返回列号到对应列泡泡糖列表的映射
public static Map<Integer, StarList> group(StarList starList) {
// 创建存储分组结果的Map(键:列号,值:该列的泡泡糖列表)
Map<Integer, StarList> ret = new HashMap<Integer, StarList>();

// 先对泡泡糖列表排序(确保同列泡泡糖连续)
sort(starList);

// 遍历所有泡泡糖进行分组
for (int i = 0; i < starList.size(); i++) {
Star star = starList.get(i);
int column = star.getPosition().getColumn();

// 若当前列号在Map中不存在,创建新列表并添加当前泡泡糖
if (!ret.containsKey(column)) {
StarList list = new StarList();
list.add(star);
ret.put(column, list);
}
// 若已存在该列号,直接将泡泡糖添加到对应列表
else {
ret.get(column).add(star);
}
}

return ret;
}

5.3.3 获取垂直方向待移动泡泡糖

本节是在前面任务的基础上实现获得多列”垂直方向待移动泡泡糖”的功能。

getYMovedStars类
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
// 获取垂直方向需要移动的泡泡糖列表(处理因清除泡泡糖导致的下落逻辑)
public StarList getYMovedStars(StarList clearStars, StarList currentStarList) {
// 存储需要移动的泡泡糖
StarList movedStars = new StarList();
// 按列号分组存储待清除的泡泡糖
Map<Integer, StarList> groupStars = StarsUtil.group(clearStars);
// 遍历所有列
Iterator<Integer> keys = groupStars.keySet().iterator();
while (keys.hasNext()) {
int column = keys.next();
// 获取当前列中待清除的泡泡糖列表
StarList starList = groupStars.get(column);
// 从当前列中最下方(最大行号)的待清除泡泡糖开始处理
int startPosition = starList.lastElement().getPosition().getRow();
// 记录下落跨度(受清除泡泡糖数量影响的偏移量)
int span = 0;

// 从最下方待清除泡泡糖的行号向上遍历至第0行
for (int row = startPosition; row >= 0; row--) {
// 查找当前行列位置的泡泡糖
Star star = currentStarList.lookup(row, column);
// 若当前位置无泡泡糖,退出循环
if (star == null)
break;

// 若当前泡泡糖是待清除的,增加下落跨度
if (clearStars.existed(star)) {
span++;
}
// 若不是待清除的,计算其移动后的位置并添加到列表
else {
MovedStar movedStar = new MovedStar(
star.getPosition(),
star.getType(),
new Position(row + span, column) // 移动后的位置 = 原行号 + 下落跨度
);
movedStars.add(movedStar);
}
}
}
return movedStars;
}

运行结果如下图所示:

5.4 移动水平方向的泡泡糖(定位PRJ-BU2-JAVA-013)

核心组件:

核心组件
核心组件

UML类图:

UML类图
UML类图

5.4.1 获取被整列清空的泡泡糖集合

本节是根据界面泡泡糖矩阵,获取”被清空所有泡泡糖的列”集合。

getNullColumns类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 获取所有底部为空的列(即最底行无泡泡糖的列)
private List<Integer> getNullColumns(StarList currentStarList) {
// 存储底部为空的列号
List<Integer> ret = new ArrayList<>();

// 遍历所有列
for (int column = 0; column < StarService.MAX_COLUMN_SIZE; column++) {
// 查找当前列最底行(最大行号)的泡泡糖
Star star = currentStarList.lookup(StarService.MAX_ROW_SIZE - 1, column);

// 若最底行无泡泡糖,记录该列号
if (star == null) {
ret.add(column);
}
}

return ret;
}

测试类代码
1
2
3
4
ArrayList<Integer>nullIntegers=(ArrayList<Integer>)getNullColumns(currentStarList);
System.out.println(nullIntegers);
return null;

运行结果如下图所示:

空列检测结果
空列检测结果

5.4.2 获取水平待移动泡泡糖

本任务利用上一节计算获得的”整列被清空泡泡糖”的列号集合,获取水平待移动的泡泡糖的数量和移动步长。

getXMovedStars类
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
// 获取水平方向需要移动的泡泡糖列表(处理底部为空列导致的左移逻辑)
public StarList getXMovedStars(StarList currentStarList) {
/**************PRJ-BU2-JAVA-013 Task2 【2/2 Start】*************/
// 存储需要水平移动的泡泡糖
StarList movedStars = new StarList();

// 获取所有底部为空的列(最底行无泡泡糖的列)
List<Integer> nullColumns = getNullColumns(currentStarList);

// 若没有底部为空的列,无需移动,返回null
if (nullColumns == null || nullColumns.size() == 0)
return null;

// 记录水平移动跨度(左侧空列的数量,决定右列泡泡糖左移的距离)
int span = 0;

// 从第一个底部空列开始,遍历所有列
for (int column = nullColumns.get(0); column < StarService.MAX_COLUMN_SIZE; column++) {
// 若当前列是底部空列,增加移动跨度
if (nullColumns.contains(column)) {
span++;
}
// 若当前列不是底部空列,处理该列所有泡泡糖的左移
else {
// 从当前列最底行向上遍历所有行
for (int row = StarService.MAX_ROW_SIZE - 1; row >= 0; row--) {
// 查找当前行列的泡泡糖
Star star = currentStarList.lookup(row, column);

// 若当前行无泡泡糖,向上行继续查找(避免处理空行)
if (star == null)
break;

// 将普通泡泡糖转为移动泡泡糖对象
MovedStar mStar = StarsUtil.toMovedStar(star);
// 设置左移后的位置:原行号不变,原列号减去移动跨度
mStar.setMovedPosition(new Position(star.getPosition().getRow(), star.getPosition().getColumn() - span));

// 将移动泡泡糖添加到结果列表
movedStars.add(mStar);
}
}
}

return movedStars;
}

运行结果如下图所示:

第六章 通关分数与积分

6.1 更新关卡通关分数(定位PRJ-BU2-JAVA-014)

核心组件:

核心组件
核心组件

UML类图:

UML类图
UML类图

6.1.1 获取初始通关分数

获取第一关的通关目标分数,该分数需要从配置文件中动态读取。

Configuration类
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
public class Configuration {
// 存储泡泡糖游戏的分数配置信息
private Score score = null;
// 分数配置文件路径,固定为"score.conf"
private final String CONF_PATH = "score.conf";

public Configuration() {
// 初始化分数对象
score = new Score();
BufferedReader bf = null;
try {
// 获取配置文件输入流并创建缓冲读取器
bf = new BufferedReader(new InputStreamReader(this.getClass().getClassLoader().getResourceAsStream(CONF_PATH)));
// 按行读取配置文件内容,依次设置泡泡糖游戏的各项分数参数
// 配置文件内容依次为:等级分数、步数、增量、长度
score.setLevelScore(Integer.parseInt(bf.readLine()));
score.setStep(Integer.parseInt(bf.readLine()));
score.setIncrement(Integer.parseInt(bf.readLine()));
score.setLength(Integer.parseInt(bf.readLine()));
} catch (IOException e) {
// 读取配置文件发生IO异常时,将score置为null
// TODO Auto-generated catch block
score = null;
} finally {
// 确保缓冲读取器关闭,释放资源
if (bf != null)
try {
bf.close();
} catch (IOException e) {
// 关闭流发生异常时打印堆栈信息
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

/**
* 获取泡泡糖游戏的分数配置对象
* @return 分数配置对象,若加载失败则返回null
*/
public Score getScore() {
return score;
}
}

ScoreServiceImpl类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ScoreServiceImpl implements ScoreService {
// 配置对象,用于获取泡泡糖游戏的分数相关配置
private Configuration mConfiguration = null;

//构造方法,初始化配置对象
public ScoreServiceImpl(){
mConfiguration = new Configuration();
}

@Override
public int getCurrentLevelScore() {
// 通过配置对象获取分数配置,并返回当前关卡的通关分数
return mConfiguration.getScore().getLevelScore();
}
}

运行结果如下图所示:

初始分数结果
初始分数结果

6.1.2 更新关卡通关分数

数据实现:

分数更新数据流程
分数更新数据流程
nextScoreByLevel类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public int nextScoreByLevel(int nextLevel) {
// 获取泡泡糖游戏的分数配置信息
Score score = mConfiguration.getScore();

// 若配置信息获取失败(score为null),返回0
if(score == null){
return 0;
}

// 计算下一关的通关分数:
// 公式 = 当前等级分数 + ((下一关等级-1)/关卡长度) * 分数增量 + 步数
// 其中(下一关等级-1)/关卡长度用于根据关卡进度计算递增倍数
score.setLevelScore(score.getLevelScore() + (nextLevel - 1) / score.getLength() * score.getIncrement() + score.getStep());

// 返回计算后的下一关通关分数
return score.getLevelScore();
}

运行结果如下图所示:

6.2 实现泡泡糖的积分规则(定位PRJ-BU2-JAVA-015)

核心组件:

核心组件
核心组件

UML类图:

UML类图
UML类图

6.2.1 获取消除奖励分数

根据消除的泡泡糖个数,计算单次消除所获得的奖励分数,如下所示:

消除奖励分数规则
消除奖励分数规则
getScoreByStars类
1
2
3
4
5
6
7
@Override
public int getScoreByStars(int stars) {
// 计算规则:泡泡糖数量的平方乘以游戏基础分数(LOWER_SCORE)
int score = stars * stars * ScoreService.LOWER_SCORE;
return score;
}

运行结果如下图所示:

消除奖励测试结果
消除奖励测试结果

6.2.2 获取结算时奖励分数

当界面无可消除泡泡糖时,若剩余泡泡糖数量小于【限定值】,则根据剩余泡泡糖数量进行结算奖励。如下:

结算奖励规则
结算奖励规则
getAwardScore类
1
2
3
4
5
6
7
8
9
10
11
@Override
public int getAwardScore(int leftStarNum) {
int award = 0;
// 当剩余泡泡糖数量小于奖励限制时,计算奖励分数
// 奖励规则:基础奖励分数乘以(奖励限制-剩余泡泡糖数量)的平方
if(leftStarNum < ScoreService.AWARD_LIMIT){
award = ScoreService.LOWER_AWARD_SCORE * (ScoreService.AWARD_LIMIT-leftStarNum) * (ScoreService.AWARD_LIMIT-leftStarNum);
}
return award;
}

运行结果如下图所示:

结算奖励测试结果
结算奖励测试结果

6.2.3 显示通关提示

当游戏积分达到通关分数目标时,显示通过提示。

通关提示流程
通关提示流程
isChangeLevel类
1
2
3
4
5
6
7
8
9
10
11
12
public boolean isChangeLevel(int score) {
// 获取泡泡糖游戏的分数配置信息,包含当前关卡的通关分数要求
Score s = this.mConfiguration.getScore();
// 若当前分数达到或超过当前关卡的通关分数,则满足条件
if (score >= s.getLevelScore()) {
return true;
}

// 未达到条件,返回false
return false;
}

isNoticePassLevel类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean isNoticePassLevel(int currentLevel, int score) {
// 先判断当前分数是否达到关卡升级条件(即通关条件)
if (isChangeLevel(score)) {
// 若关卡计数器与当前关卡一致(表示首次达到该关卡通关条件)
if (this.mLevelCounter == currentLevel) {
// 将关卡计数器+1,标记为已处理过该关卡的通关提示
this.mLevelCounter++;
// 返回true,表示需要显示通关提示
return true;
} else {
// 非首次触发该关卡通关,无需重复提示
return false;
}
}
// 未达到通关条件,无需提示
return false;
}

运行结果如下图所示:

通关提示测试结果
通关提示测试结果

第七章 泡泡糖体验

7.1 泡泡糖的体验(定位PRJ-BU2-JAVA-001)

7.1.1 泡泡糖换肤体验

定位 src/res.layout/main_layout.fxml

7.1.2 Jar 命令运行项目

A. 导出项目 Jar 包,实现游戏的打包与发布

a. 选中项目 PRJ_BU2_JAVA_001,右键 → Export。

b. 选择 Java → Runnable JAR file,点击 Next。

c. 在 Launch configuration 栏中指定启动类:MainClass - PRJ_BU2_JAVA_001

d. 在 Export destination 一栏,选择需要导出的路径,这里推荐:导出到桌面。

e. 导出的文件名称推荐使用:Bubble.jar

f. 点击 Finish,导出 Bubble.jar 包。

B. 启动命令提示符窗口

a. Windows 开始菜单 → 所有程序 → 附件 → 命令提示符,点击启动。

C. 运行 Jar

a. 在命令提示符中输入(注意:尾部有一个空格)java -jar

b. 将存放于桌面的 Bubble.jar 直接拖入【命令提示符】。

c. 点击回车运行 jar 文件。

如果运行不了,切以下命令:

java --module-path "D:\cxdownload\javafx-sdk-21.0.2\lib" --add-modules javafx.controls,javafx.fxml -jar "C:\Users\lenovo\Desktop\Bubble.jar"

效果如下图所示:

Jar运行效果
Jar运行效果

第八章 项目实践的问题与解答(Q&A)

Q1: 构造函数的作用是什么?项目中你会如何定义构造函数?

构造函数用于初始化类中的重要成员变量。

构造函数可以多次重载,默认类具有零参构造函数。

构造函数如为私有作用域,表示该类不能实例化(例如:Utils 类)。

【例如:private MovedStar () {}

构造函数初始化的变量往往非常重要,并不是所有成员变量都需要在构造函数中初始化。

【例如:人类创建时,需要首先初始化脑袋成员对象,理由是没有脑袋,人存在没有意义。】

Q2: 谈谈随机函数 Math.random 的作用与价值

Math 类的 random 函数会得到一个【大于等于 0】且【小于 1】的【小数】。

我们一般会将 random 产生的随机结果 * 随机极限值,确保随机上限。

我们一般会将 random 产生的随机结果 + 随机初始值,确保随机下限。

例如:产生 3~10 之间的整数随机值

int result = (int) (Math.random * 8) + 3

随机结果默认为小数,我们可以通过强制类型转换调整随机数的返回结果。

Q3: 结合项目谈谈递归函数的运用场景

递归函数是指函数对其本身的重复调用,递归函数在使用时需设计明确的 “调用点”“退出点”

a. 调用点:何时对函数本身执行调用操作,一般调用都是在判断下执行,防止死循环。

b. 退出点:递归过程退出点,一般都是调用点条件不满足的情况下,执行退出点。

PRJ-BU2-JAVA-004 中,通过搜索同类”泡泡糖”执行递归调用,函数名:lookupByPath

  • 调用点:用户点击的”泡泡糖”左侧、右侧、上方、下方存在同类”泡泡糖”。
  • 退出点:当前”泡泡糖”左侧、右侧、上方、下方无任何同类”泡泡糖”。
Q4: 请结合项目说明何时使用继承比较妥当?

继承一般有以下三种情况:

a. 继承 JDK 或第三方组件类,实际项目中大多是为了修改父类函数(因父类函数无法满足要求)。

【例如:Java 的 JDK 为程序员提供了集合,集合可以存放任意数据,但当前游戏中,集合需要存放泡泡糖,JDK 的集合并不是为”泡泡糖”而生的,所以我们编写了 StarList 继承了 JDK 的集合,并修改了 List 中无法满足要求的函数(例如:indexOf 等),如感兴趣可以查看 StarList 类。】

b. 继承 JDK 或第三方组件类,完全获取父类的功能。

【例如:当前游戏中,还记得《消灭泡泡糖》的界面吗?我们无需开发如此复杂的 Windows 窗体界面,只需要在代码中继承 JDK 为我们提供的 Application 类就可以实现一个完整窗体。】

c. 如 PRJ-BU2-JAVA-005,实现类间属性扩展。

【说明:Star 具有两个描述属性:位置,颜色(类型),当 Star 因为消除操作需要移动时,Star 将产生一个新位置,此处 MovedStar 的出现就是为了确定移动后 Star 的位置,同时它具有 Star 的所有属性。】

Q5: 谈谈接口的作用,并说明在您所开发的项目中如何定义与设计接口?

接口有两大作用:定标准解耦合

a. 定标准,业务调用方允许设定业务规范,只有满足业务标准的组件才可以与调用方交互(多用于系统架构设计)。

b. 解耦合,原本只能一人开发的系统,降耦合后可以由多人同时开发,最后组装。

【例如:当前场景通过接口StarService将界面和业务彻底隔离,界面层只负责显示”泡泡糖”,”泡泡糖”的消除、移动、计算全部由业务层负责。】

由于界面层需要调用获取业务层计算结果后才可以显示”泡泡糖”,因此界面层扮演接口调用方,业务层扮演接口实现方,具体为:

接口分层架构
接口分层架构

StarService使原本一人自顶向下的开发模式,变成了多人并行开发,1 人负责界面,1 人负责业务。

扩展:层与层之间的数据交互,一般需要通过对象来实现,当前系统的数据交互对象是StarList,1 个StarList中可以存放多个Star。

Q6: 谈谈集合泛型的作用

由于 Java 集合中的 ArrayList 和 HashMap 都允许存放任意数据类型的数据,因此为集合配套泛型可以规范集合数据类型,保证集合中的数据类型一致(实际项目不可能允许集合中的数据类型不一致)。

泛型有语法约束性,强制限定数据类型的完整性。

扩展:泛型不仅仅可以在集合中使用,您自己编写的类也可以配套泛型,例如:

1
2
3
4
5
6
public class ExcelManager<S> {
public void saveDataToSheet(S datasources) {}
}

ExcelManager<ArrayList<String>> manager = new ExcelManager<ArrayList<String>>();
ExcelManager<DataBean> manager = new ExcelManager<DataBean>();
Q7: 以 PRJ-BU2-JAVA-015 为例,谈谈 Java 中按值传递数据与按引用传递数据的区别?

Java 中八种原始数据类型都是按值传递(byte、short、int、long、float、double、boolean、char)。

按值传递就是,原始数据类型利用 “=” 赋值时,变量与变量之间只通过数据交换。例如:

1
2
int row = 10;
int r = row;

以上语句的含义是创建一个整型变量内存空间,取名 row 并赋值 10,再创建一个整型变量内存空间,取名 r。将 row 空间的数据赋值给 r 空间,row 和 r 完全是两个不同的内存空间。

按值传递的场景应用效果如下:

按值传递示意
按值传递示意

上图一共有三个 row 变量和三个 col 变量,分别存在于 createStars、move、setPosition 中。当createStars 调用 move 时,只是将自己的 row 变量值和 col 变量值传送给了 move 函数。虽然move 对 row 和 col 执行了 ++ 操作,但是 createStars 的 row 和 col 并没有受到影响。createStars 再次调用 setPosition 时,row 和 col 仍然等于 0,”泡泡糖”肯定不会移动。


Java中的三种引用数据类型都是按引用传递的(对象、接口、数组)。

按引用传递就是,原始引用类型利用 “=” 赋值时,多个引用对象名指代的是同一个内存空间。例如:

1
2
Position p = new Position();
Position pos = p;

以上语句的含义是:new Position();创建的内存空间,分别被取名为 “p” 和 “pos”,操作 “p” 或 “pos” 其实访问的是同一块内存空间,这个与值类型差别是非常大的。

按引用传递的场景应用效果如下:

按引用传递示意
按引用传递示意

上图只有一个position对象,虽然分散在createStars、move、setPosition中,其实指代的是同一块内存。

【重要提示】:函数的”形式”、”实际”、”参数间”隐藏了”个 =”,不理解时,可假设”=”,再理解表达式。

【重要提示】:变量名在不同作用域中可以完全相同。

第九章 实践项目总结

9.1 递归函数的作用及注意点

在PRJ-BU2-JAVA-004中,递归函数核心为 lookupByPath 函数,作用是实现”查找被点击泡泡糖四周同色泡泡糖”的核心需求:以被点击泡泡糖为基准,向左侧、右侧、顶部、底部四个方向逐层查找同色泡泡糖,找到同色泡泡糖后,会以其为新基准继续调用自身查找,直至无同色泡泡糖,确保将所有相连的同色泡泡糖纳入”待消除泡泡糖集合”,满足游戏消除逻辑,同时避免重复编写多方向查找代码,简化实现逻辑。

使用时需注意两点:

  • 一是必须明确”调用点”与”退出点”,调用点为”被点击泡泡糖的左/右/上/下存在同色且未在待消除集合中的泡泡糖”,退出点包括超10×10矩阵边界、目标位置无泡泡糖、泡泡糖不同色、泡泡糖已在待消除集合,防止死循环;
  • 二是需控制递归范围,仅在矩阵内查找相连同色泡泡糖,避免无意义递归影响性能。

9.2 枚举类型的作用及应用

作用:

提升代码可读性 — 将无意义的数值转化为有业务含义的常量,降低软件后期维护成本

保障类型安全 — 枚举取值为预定义固定常量,无法随意创建新实例,确保变量赋值始终在合法范围内

应用(PRJ-BU2-JAVA-002):

a. 在Star类中定义StarType枚举,通过私有构造函数为枚举常量赋值value(如BLUE(0)”蓝色空心圆”、GREEN(1)”绿色圆角” 等 5 种,对应泡泡糖外观),并添加 value() 方法实现 “枚举值→数值” 转换,添加静态 valueOf(int num) 方法(通过switch-case)实现 “数值→枚举值” 转换。

b. 将StarType作为Star类的type成员变量类型,描述泡泡糖外观,配合get/set访问器赋值获取,如创建star2时指定type为StarType.GREEN,同时支持界面根据枚举值渲染泡泡糖外观、业务层处理数据交互。

9.3 JavaFX 实现游戏界面

(PRJ-BU2-JAVA-006)围绕JavaFX实现泡泡糖游戏界面展开,让学习者掌握了相关核心流程。

  • 首先需搭建开发环境,创建JavaFX应用程序类
  • 接着运用布局管理、控件使用和图形绘制等UI技术设计游戏界面
  • 关键是将业务层的泡泡糖数据(Star对象)通过属性映射转换为Label控件,并设置其尺寸、位置、样式等
  • 然后集成游戏逻辑与界面
  • 之后进行测试调试以验证显示效果,包括测试阶段用固定数据、真实阶段用随机数据
  • 最后完成打包分发

整个过程体现了JavaFX图形与UI组件的应用,具体实现需依游戏需求等灵活调整。