一.前言

学校安排的学年设计,需要交一份基于Java或c++编写的小游戏,我自然是选择Java了,我准备交一份《飞翔的小鸟》游戏上去。在这里记录一下整个项目的过程。

飞翔的小鸟想必大家小时候都玩过,我记得我当时偷偷玩手机的时候还下载过这个游戏,没想到有一天我会亲手写这个游戏。


二.分析

在写代码量较多,结构稍复杂的程序中,我们首先要建立UML类图。首先要明确有哪些对象,从以前我们玩这个游戏的经验来说,第一个必须是要有会飞的小鸟,那么它怎么会飞呢?我们只需要多张小鸟扇动翅膀的图片,然后按顺序在屏幕前闪过,这个小鸟就像实在飞行中的状态了。第二,需要天空类和地面类以及障碍物类。最后根据各自需要实现的功能来编写其类中的方法。

我们来看看具体的UML类图:

我们照着UML类图,一步一步剖析。

1.Column类

这个就是障碍物柱子,我们需要考虑其宽度与高度以及钢管空隙,这样才能判定小鸟是否撞在了柱子上,最后还需要一个能让障碍物无限循环的动画。

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
class Column {
int column_x, column_y; //钢管的中心坐标
int width, height; //宽度高度
int gap = 140; //钢管的空隙
Random random = new Random(); //随机坐标
BufferedImage coBufferImage; //钢管图片

//构造方法,指定障碍物的宽度,并且随机生成障碍物的高度
public Column(int x) {
super();
File coImage = new File("images/column.png");
try {
coBufferImage = ImageIO.read(coImage);
} catch (IOException e) {
e.printStackTrace();
}
column_x = x;
column_y = random.nextInt(180) + 150;
width = coBufferImage.getWidth();
height = coBufferImage.getHeight();
}

// 钢管动画方法
//每次障碍物中心点坐标减1,如果障碍物离开屏
//幕右端则在屏幕左端重新出现,并且障碍物高度随机生成
public void move() {
column_x--;
if (column_x < -width / 2) {
column_y = random.nextInt(180) + 150;
column_x = 432 + width / 2;
}
}
}

2.Ground类

地面我们只需要关注地面的坐标就行了,它和上面的Column类非常相似。

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
class Ground {
int ground_x, ground_y; // 地面的坐标
BufferedImage grBufferImage; // 地面图片

public Ground() {
super();
File grImage = new File("images/ground.png");
try {
grBufferImage = ImageIO.read(grImage);
} catch (IOException e) {
e.printStackTrace();
}
//设置地面图片的位置
ground_y = 500;
}

// 地面动画方法
//每次调用这个方法地面的图片向左移动1,
//如果地面图片到达了尽头就从新使图片回到最初位置继续向左移动,如此反复。
public void move() {
ground_x--;
if (ground_x < -110) {
ground_x = 0;
}
}
}

3.Bird类

到了Bird类这里就稍稍有点复杂了,我们需要考虑的属性也变得多了起来。首先最容易想到的就是图片的宽高和中心坐标了,其次我们需要考虑小鸟在x轴和y轴的移动速度

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
72
73
74
75
76
class Bird {
int bird_x = 60, bird_y = 300; // 鸟的中心点坐标
int bird_width, bird_height; // 鸟的宽度,高度
double speed = 20; // 速度
double g = 4; // 加速度
double s; // 运动距离
double t = 0.3; // 运动时间
BufferedImage biBufferImage; // 鸟图片
BufferedImage[] images = new BufferedImage[8];

public Bird() {
super();
//反复按顺序绘制图片以达到动画的效果
for (int i = 0; i < images.length; i++) {
File biImage = new File("images/" + i + ".png");
try {
images[i] = ImageIO.read(biImage);
} catch (IOException e) {
e.printStackTrace();
}
}
biBufferImage = images[0];
bird_width = biBufferImage.getWidth();
bird_height = biBufferImage.getHeight();
}

//小鸟展翅动画方法
//每次点击鼠标展翅后就换一张图片进行绘制
int index = 0;
public void change() {
index++;
biBufferImage = images[index / 3 % 8];
}

//小鸟移动的方法
double ratation; //倾斜角度
public void move_go() {
double v0 = speed;
s = v0 * t - 0.5 * g * t * t;
double vt = v0 - g * t;
speed = vt;
bird_y = bird_y - (int) s;//小鸟下落后的在y轴方向的位置
ratation = s / 16;
if (bird_y <= bird_height / 2) {//这里限制小鸟最高只能飞到了屏幕的最上方
bird_y = bird_height / 2;
}
}

//重新飞翔
public void refly() {
speed = 20;
}

//撞击地面
//撞到了地面就返false
public boolean hit(Ground ground) {
return bird_y + bird_height / 2 >= ground.ground_y;
}

//撞击钢管
//如果碰到了钢管的左侧、右侧、上方和下方都返回false,否则返回true
public boolean hit(Column column) {
int left_x = column.column_x - column.width / 2 - bird_width / 2;
int right_x = column.column_x + column.width / 2 + bird_width / 2;
int top_y = column.column_y - column.gap / 2 + bird_height / 2 - 5;
int down_y = column.column_y + column.gap / 2 - bird_height / 2 + 5;
if (bird_x > left_x && bird_x < right_x) {
if (bird_y > top_y && bird_y < down_y) {
return false;
} else {
return true;
}
} else {
return false;
}
}

4.Sky类

Sky类是本程序中最复杂的一类,因为它是其他三类的组合,并且它自身要包含绘制图片以及游戏逻辑的等功能。对于游戏逻辑这一部分,我们的想法是程序每经历很小的一段时间就循环进行图形的绘制,并且要执行Column,Ground两类中的move方法且判断一次游戏是否结束,整个过程中都要监听键盘和鼠标点击事件来相应Bird中的move_go方法。

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
class Sky extends JPanel {
private static final long serialVersionUID = 1L;//游戏版本号
BufferedImage bgBufferImage; //背景图片
Ground ground = new Ground(); //地面
Column column = new Column(350); //钢管
Column column2 = new Column(600); //钢管
static Bird bird = new Bird(); //小鸟
int score = 0; //游戏得分
BufferedImage startBufferImage; //开始准备界面
boolean isStart; //是否开始游戏
BufferedImage overBufferImage; //游戏结束界面
boolean isOver; //游戏是否结束

public Sky() {
super();
//读取图片
File bgImage = new File("images/bg.png");
File starImage = new File("images/start.png");
File overImage = new File("images/gameover.png");
try {
bgBufferImage = ImageIO.read(bgImage);
startBufferImage = ImageIO.read(starImage);
overBufferImage = ImageIO.read(overImage);
} catch (IOException e) {
e.printStackTrace();
}
}

//绘制界面方法
//绘制图片都是从左上角开始绘制到右下角结束
@Override
public void paint(Graphics graphics) {
//画背景
graphics.drawImage(bgBufferImage, 0, 0, null);
//获取新的画笔对象
Graphics2D gg = (Graphics2D) graphics;
gg.rotate(-bird.ratation, bird.bird_x, bird.bird_y);
//画小鸟
graphics.drawImage(bird.biBufferImage, bird.bird_x - bird.bird_width
/ 2, bird.bird_y - bird.bird_height / 2, null);
gg.rotate(bird.ratation, bird.bird_x, bird.bird_y);
//画钢管
graphics.drawImage(column.coBufferImage, column.column_x - column.width
/ 2, column.column_y - column.height / 2, null);
graphics.drawImage(column2.coBufferImage, column2.column_x
- column2.width / 2, column2.column_y - column2.height / 2,
null);
//画地面
graphics.drawImage(ground.grBufferImage, ground.ground_x,
ground.ground_y, null);
//画文字
graphics.setColor(Color.BLUE);
graphics.setFont(new Font("楷体", Font.ITALIC, 30));
graphics.drawString("分数:" + score, 100, 600);
//画开始准备图片
if (!isStart && !isOver) {
graphics.drawImage(startBufferImage, 0, 0, null);
}
//画结束界面
if (isOver) {
graphics.drawImage(overBufferImage, 0, 0, null);
}
}

//游戏启动逻辑
public void action() {
//定义鼠标监听器
//用鼠标可以控制小鸟在y轴上的位置
MouseAdapter adapter = new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
// System.out.println("点击了鼠标");
/*
* 若游戏结束重新开始游戏,游戏恢复初始状态
* 若未结束:鸟飞起来
*/
if (isOver) {
bird = new Bird();
ground = new Ground();
column = new Column(350);
column2 = new Column(600);
score = 0;
isOver = false;
isStart = false;
} else {
bird.refly();
isStart = true;
}
}
};
//添加鼠标监听事件
this.addMouseListener(adapter);
//定义键盘监听器
//这里用键盘可以控制小鸟在x轴和y轴上的位置
KeyAdapter keyAdapter = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
char charA = e.getKeyChar();
if (charA == 'w') {
if (bird.bird_y > 20) {
bird.bird_y -= 20;
}
} else if (charA == 's') {
if (bird.bird_y < 465) {
bird.bird_y += 20;
}
} else if (charA == 'a') {
if (bird.bird_x > 20) {
bird.bird_x -= 20;
}
} else if (charA == 'd') {
if (bird.bird_x < 395) {
bird.bird_x += 20;
}
} else if (charA == ' ') {
/*
* 若游戏结束重新开始游戏,游戏恢复初始状态
* 若未结束:鸟飞起来
*/
if (isOver) {
bird = new Bird();
ground = new Ground();
column = new Column(350);
column2 = new Column(600);
score = 0;
isOver = false;
isStart = false;
} else {
bird.refly();
isStart = true;
}
}
super.keyPressed(e);
}
};
//将上面的键盘监听事件加入到程序中
this.addKeyListener(keyAdapter);
this.requestFocus();

while (true) {
//循环执行下面方法
//判断游戏是否开始
if (isStart && !isOver) {
ground.move();
column.move();
column2.move();
bird.change();
bird.move_go();
}
//判断撞击障碍
if (bird.bird_x - bird.bird_width / 2 == column.column_x
+ column.width / 2
|| bird.bird_x - bird.bird_width / 2 == column2.column_x
+ column2.width / 2) {
score++;
}
if (bird.hit(ground) || bird.hit(column) || bird.hit(column2)) {
isStart = false;
isOver = true;
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
//再次绘制一次图形
repaint();
}
}
}

5.main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
// 定义画框
JFrame jf = new JFrame("bird_game");
jf.setSize(432, 674);
jf.setAlwaysOnTop(false);
jf.setLocationRelativeTo(null);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setResizable(false);
Sky sky = new Sky();
jf.add(sky);
// 显示画框
jf.setVisible(true);
//执行程序逻辑
sky.action();
}

三.结果演示

1.游戏开始界面

2.游戏进行界面

3.游戏结束界面

整个飞翔的小鸟到这里就结束了,我这个学年设计也差不多了(#^.^#)。最后附上源代码和素材,github仓库地址:https://github.com/cofbro/FlyingBird/tree/main