台球游戏

概述

实现一个桌球游戏。桌上有6个球袋,球可以落入其中并计分。游戏开始时,所有球放在桌上的某个位置,其中有一个白球,玩家只能击打白球,并让白球撞击其他颜色的球。如果球撞击桌子边缘,则反弹。球可以撞击其他球,并传递动量。该游戏为单人游戏,胜利条件为除白球外的所有球均已入袋,失败条件为白球入袋。

需求

  1. 使用工厂模式读取并处理配置文件的各部分
  2. 使用建造者模式创建球
  3. 使用策略模式控制球落入袋之后的行为
  4. 球桌的尺寸,颜色和摩擦力可配置
  5. 球的颜色,初始位置/速度,质量可配置
  6. 实现球在碰撞其他球和桌面边缘的物理效果
  7. 实现球在桌面上由于摩擦力而减速
  8. 实现胜利和失败条件
  9. 击球控制:可以使用鼠标控制击打白球,点击白球并拖拽以控制方向和力度,释放鼠标以击球。只有在白球静止时可以击球。击球控制需有相应的图形指示。
  10. 当前仅考虑红球和蓝球,红球入袋后即消失,蓝球第一次入袋后回到初始位置,第二次入袋后消失
  11. 代码需要有良好的注释

工厂模式处理配置文件

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
public class ConfigReaderFactory {
public ConfigReader createConfigReader(String fileType) {
if ("ball".equalsIgnoreCase(fileType)) {
return new BallConfigReader();
}
else if ("table".equalsIgnoreCase(fileType)) {
return new TableConfigReader();
}
return null;
}
}

public interface ConfigReader<T> {
T readConfig(String filePath);
}

public class BallConfigReader implements ConfigReader<BallConfig> {

public BallConfig readConfig(String filePath) {
BallConfig ballconfig = new BallConfig();
try{
String content = new String(Files.readAllBytes(Paths.get(filePath)));
JSONObject json = new JSONObject(content);
ballconfig.setCueBallColor(JsonUtils.getVector3D(json,"cueBallColor"));
ballconfig.setCueBallPos(JsonUtils.getVector2D(json,"cueBallPos"));
ballconfig.setMass(json.getDouble("mass"));
ballconfig.setBlueBallPos(JsonUtils.getVector2DArray(json,"blueBallPos"));
ballconfig.setRedBallPos(JsonUtils.getVector2DArray(json,"redBallPos"));
} catch (Exception e) {
e.printStackTrace();
}
return ballconfig;
}
}

public class TableConfigReader implements ConfigReader<TableConfig> {

public TableConfig readConfig(String filePath) {
TableConfig tableConfig = new TableConfig();
try{
String content = new String(Files.readAllBytes(Paths.get(filePath)));
JSONObject json = new JSONObject(content);
tableConfig.setColor(JsonUtils.getVector3D(json,"color"));
tableConfig.setSize(JsonUtils.getVector2D(json,"size"));
tableConfig.setFriction(json.getDouble("friction"));
} catch (Exception e) {
e.printStackTrace();
}
return tableConfig;
}
}

建造者模式创建球

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
public class BallBuilder {
private BallConfig ballConfig; // 读取的配置
public BallBuilder(BallConfig ballconfig) {
this.ballConfig = ballconfig;
}

// 读取配置文件,构建母球 母球编号0
public Ball buildCueBall(Group root) {
Ball ball = new Ball();
ball.setColor(ballConfig.getCueBallColor().toColor());
ball.setPos(ballConfig.getCueBallPos());
ball.setID(0);
ball.setHP(1);
root.getChildren().add(ball.getCircle());
return ball;
}

// 为了实现拖拽效果,需要构建一个虚拟的球
public Ball buildVirtualBall(Group root) {
Ball ball = new Ball();
ball.getCircle().setFill(Color.TRANSPARENT);
ball.getCircle().setStroke(Color.BLACK);
ball.getCircle().setVisible(false);
root.getChildren().add(ball.getCircle());
return ball;
}

// 构建红球 红球标号为3-4
public ArrayList<Ball> initRedBalls(Group root) {
ArrayList<Ball> balls = new ArrayList<>();
int blueNum = ballConfig.getBlueBallPos().size();
int redNum = ballConfig.getRedBallPos().size();
for(int i=blueNum+1; i<=1+blueNum+redNum-1; i++) {
Ball ball = buildRedBall(root, i);
balls.add(ball);
}
return balls;
}

// 构建所有蓝球 蓝球标号为1-2
public ArrayList<Ball> initBlueBalls(Group root) {
ArrayList<Ball> balls = new ArrayList<>();
int blueNum = ballConfig.getBlueBallPos().size();
for(int i=1; i<=1+blueNum-1; i++) {
Ball ball = buildBlueBall(root, i);
balls.add(ball);
}
return balls;
}
public Ball buildBlueBall(Group root, int num) {
Ball ball = new Ball();
ball.setColor(Color.BLUE);
ball.setPos(ballConfig.getBlueBallPos().get(num-1));
ball.setID(num);
ball.setHP(1);
root.getChildren().add(ball.getCircle());
return ball;
}
public Ball buildRedBall(Group root, int num) {
Ball ball = new Ball();
ball.setColor(Color.RED);
ball.setPos(ballConfig.getRedBallPos().get(num-3));
ball.setID(num);
root.getChildren().add(ball.getCircle());
return ball;
}
}

策略者模式控制球入袋后行为

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 interface BallBehaviorStrategy {
void handleBallInPocket(Group root, Ball ball, BallConfig ballConfig);
}

public class RemoveBallStrategy implements BallBehaviorStrategy{
@Override
public void handleBallInPocket(Group root, Ball ball, BallConfig ballConfig) {
root.getChildren().remove(ball.getCircle());
}
}

public class ResetBallStrategy implements BallBehaviorStrategy{
@Override
public void handleBallInPocket(Group root, Ball ball, BallConfig ballConfig) {
int id = ball.getID();
if(id == 0) {
ball.setPos(ballConfig.getCueBallPos());
ball.setVelocity(new Vector2D(0, 0));
ball.setHP(ball.getHP()-1);
}
else {
Vector2D pos = ballConfig.getBlueBallPos().get(id-1);
ball.setPos(pos);
ball.setVelocity(new Vector2D(0, 0));
ball.setHP(ball.getHP()-1);
}
}
}

碰撞物理效果

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
public class CollisionPhysics {

// 计算两个球的碰撞
public static Pair<Vector2D, Vector2D> calculateBallCollision(Vector2D positionA, Vector2D velocityA, double massA, Vector2D positionB, Vector2D velocityB, double massB) {
Vector2D collisionVector = positionA.subtract(positionB);
collisionVector = collisionVector.normalize();
double vA = collisionVector.dotProduct(velocityA);
double vB = collisionVector.dotProduct(velocityB);
if (vB <= 0 && vA >= 0) {
return new Pair<>(velocityA, velocityB);
}
double optimizedP = (2.0 * (vA - vB)) / (massA + massB);
Vector2D velAPrime = velocityA.subtract(collisionVector.multiply(optimizedP).multiply(massB));
Vector2D velBPrime = velocityB.add(collisionVector.multiply(optimizedP).multiply(massA));
return new Pair<>(velAPrime, velBPrime);
}

// 计算球和边界之间的碰撞
public static Vector2D calculateEdgeCollision(Ball ball, Table table) {
Circle circle = ball.getCircle();
Vector2D v = ball.getVelocity();
double r = circle.getRadius();
Vector2D pos = ball.getPos();
if (circle.getCenterX() - r < 0) {
v = new Vector2D(-ball.getVelocity().x, ball.getVelocity().y);
ball.setPos(new Vector2D(r, pos.y));
}
if (circle.getCenterX() + r > table.getSize().x) {
v = new Vector2D(-ball.getVelocity().x, ball.getVelocity().y);
ball.setPos(new Vector2D(table.getSize().x - r, pos.y));
}
if (circle.getCenterY() - r < 0) {
v = new Vector2D(ball.getVelocity().x, -ball.getVelocity().y);
ball.setPos(new Vector2D(pos.x, r));
}
if (circle.getCenterY() + r > table.getSize().y) {
v = new Vector2D(ball.getVelocity().x, -ball.getVelocity().y);
ball.setPos(new Vector2D(pos.x, table.getSize().y - r));
}
return v;
}
}

摩擦力效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FrictionPhysics {

// 根据摩擦力计算速度
public static Vector2D calculateVelocity(Ball ball, double friction) {
Vector2D velocity = ball.getVelocity();
double v = ball.calculateVelocity();
if(v > 0.05) {
double unitX = velocity.x / v;
double unitY = velocity.y / v;
velocity.x -= friction * unitX;
velocity.y -= friction * unitY;
}
else {
velocity.x = 0;
velocity.y = 0;
}
return velocity;
}
}

胜利失败条件

母球HP=2,蓝球HP=2,红球HP=1,入袋后HP-1,HP=0则清除。

胜利:非母球全部清除

失败:母球清除

重置:按下空格重开游戏

击球控制

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
   private void handleMouseDragged(MouseEvent event) {
// 如果正在拖拽 且 选中了球
if (isDragging && selectedBall != null) {
double tmpX = event.getX();
double tmpY = event.getY();
// 计算虚拟球的位置 并 显示虚拟球
virtualBall.getCircle().setVisible(true);
virtualBall.setPos(new Vector2D(tmpX, tmpY));
}
}
private void handleMousePressed(MouseEvent event) {
mouseX = event.getX();
mouseY = event.getY();
Ball ball = cueBall; // 若开启作弊模式 则将此行注释掉

//for(Ball ball : allBalls) { // 作弊模式 快速通关测试
// 如果鼠标点击的位置在球的范围内 且 球的速度为0
if (ball.contains(new Vector2D(mouseX, mouseY)) && ball.getVelocity().equals(new Vector2D(0, 0))) {
mouseX = ball.getPos().x;
mouseY = ball.getPos().y;
selectedBall = ball;
isDragging = true;
return;
}
//}

}

// 松开鼠标的函数
private void handleMouseReleased(MouseEvent event) {
// 如果正在拖拽 且 选中了球
if (isDragging && selectedBall != null) {
// 隐藏虚拟球
virtualBall.getCircle().setVisible(false);

double deltaX = mouseX - event.getX();
double deltaY = mouseY - event.getY();

// 根据拖拽的距离计算速度 和 角度
double speed = Math.sqrt(deltaX * deltaX + deltaY * deltaY) * BALL_SPEED;
double angle = Math.atan2(deltaY, deltaX);

// 设置球的速度
selectedBall.setVelocity(new Vector2D(speed * Math.cos(angle), speed * Math.sin(angle)));

isDragging = false;
selectedBall = null;
}
}

项目源码

PoolGame

游戏截图