逆向软件工程-对flappy bird(java)小游戏的简单重构及分析
当然,这是一篇作业博客。
引言
这是一次很有意思的机会,因为近期上海举办了2025全球开发者先锋大会(GDC),我在逛展中看到Fopo折叠三面显示屏延伸器产品时,发现演示笔记本电脑里展示的是使用小浣熊ai生成的flapyy bird小游戏核心代码。有消息表示2025年flappy bird重制版将重新上线。出于好奇,我便在博客园中找到了相关的java项目,便有了这篇博客。
项目出处:https://www.cnblogs.com/ftl1012/p/9347708.html,https://www.cnblogs.com/ftl1012/p/flappyBird.html
正文
关于ide,我使用的是eclipse。下面是flappy bird的代码以及运行效果展示。
游戏代码如下:
package testfly;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.util.Random;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class TestBirdFly extends JPanel {
Bird bird; // 创建一个鸟对象
Column column1, column2; // 创建两根柱子
Ground ground; // 创建地面对象
BufferedImage background; // 背景图片
boolean gameOver; // 游戏是否结束
boolean started; // 游戏是否开始
BufferedImage gameoverImg; // 游戏结束的图片
int score; // 记录分数
// 构造方法,初始化游戏元素
public TestBirdFly() throws Exception {
score = 0;
bird = new Bird(); // 初始化鸟对象
column1 = new Column(1); // 初始化第一根柱子
column2 = new Column(2); // 初始化第二根柱子
ground = new Ground(); // 初始化地面
gameOver = false; // 游戏开始时默认没有结束
background = ImageIO.read(
getClass().getResource("bg.png")); // 读取背景图
gameoverImg = ImageIO.read(
getClass().getResource("gameover.png")); // 读取游戏结束图
}
// 重写paint方法,绘制游戏界面
public void paint(Graphics g){
g.drawImage(background, 0, 0, null); // 绘制背景
g.drawImage(column1.image,
column1.x-column1.width/2,
column1.y-column1.height/2, null); // 绘制第一根柱子
g.drawImage(column2.image,
column2.x-column2.width/2,
column2.y-column2.height/2, null); // 绘制第二根柱子
// 设置字体并绘制分数
Font f = new Font(Font.SANS_SERIF,
Font.BOLD, 40);
g.setFont(f);
g.drawString(""+score, 40, 60); // 绘制分数
g.setColor(Color.WHITE);
g.drawString(""+score, 40-3, 60-3); // 绘制分数的阴影
// 绘制地面
g.drawImage(ground.image, ground.x,
ground.y, null);
// 如果游戏结束,显示结束画面
if (gameOver){
g.drawImage(gameoverImg,0,0,null);
return;
}
// 使用Graphics2D进行旋转绘制鸟的图像
Graphics2D g2 = (Graphics2D)g;
g2.rotate(-bird.alpha, bird.x, bird.y); // 根据鸟的角度进行旋转
g.drawImage(bird.image,
bird.x-bird.width/2,
bird.y-bird.height/2, null); // 绘制鸟的图像
g2.rotate(bird.alpha, bird.x, bird.y); // 恢复旋转
}
// 游戏循环控制,处理游戏逻辑
public void action() throws Exception {
MouseListener l = new MouseAdapter(){
// 鼠标按下事件,用来控制鸟的跳跃
public void mousePressed(MouseEvent e){
started = true; // 游戏开始
bird.flappy(); // 让鸟跳跃
}
};
addMouseListener(l); // 给面板添加鼠标事件监听器
// 游戏主循环
while(true){
// 如果游戏未结束或已经开始,更新游戏元素
if(!gameOver || started){
ground.step(); // 更新地面位置
column1.step(); // 更新第一根柱子位置
column2.step(); // 更新第二根柱子位置
bird.step(); // 更新鸟的位置
}
bird.fly(); // 更新鸟的飞行状态
ground.step(); // 再次更新地面位置
// 检查是否发生碰撞
if(bird.hit(ground) || bird.hit(column1) || bird.hit(column2)){
gameOver = true; // 游戏结束
}
// 如果鸟通过了柱子,增加分数
if (bird.x == column1.x || bird.x == column2.x){
score++;
}
repaint(); // 重绘界面
Thread.sleep(1000 / 60); // 限制帧率,控制每秒60帧
}
}
// 程序入口
public static void main(String[] args) throws Exception {
JFrame frame = new JFrame(); // 创建窗口
TestBirdFly game = new TestBirdFly(); // 创建游戏实例
frame.add(game); // 将游戏面板添加到窗口
frame.setSize(440, 670); // 设置窗口大小
frame.setLocationRelativeTo(null); // 窗口居中
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 窗口关闭时退出程序
frame.setVisible(true); // 显示窗口
game.action(); // 启动游戏
}
}
// 地面类,负责地面的移动
class Ground{
BufferedImage image; // 地面图片
int x, y; // 地面的位置
int width, height; // 地面的宽高
// 构造方法,初始化地面
public Ground() throws Exception {
image = ImageIO.read(getClass().getResource("ground.png")); // 读取地面图片
width = image.getWidth(); // 获取地面宽度
height = image.getHeight(); // 获取地面高度
x = 0; // 地面初始x位置
y = 500; // 地面初始y位置
}
// 更新地面的位置
public void step(){
x--; // 向左移动
if(x == -109){ // 当地面移出屏幕时,从右侧重新出现
x = 0;
}
}
}
// 柱子类,负责生成和移动柱子
class Column{
BufferedImage image; // 柱子图片
int x, y; // 柱子的位置
int width, height; // 柱子的宽高
int gap; // 柱子之间的间隔
int distance; // 两根柱子之间的距离
Random random = new Random(); // 随机数生成器
// 构造方法,初始化柱子
public Column(int n) throws Exception {
image = ImageIO.read(getClass().getResource("column.png")); // 读取柱子图片
width = image.getWidth(); // 获取柱子宽度
height = image.getHeight(); // 获取柱子高度
gap = 144; // 设置柱子之间的间隔
distance = 245; // 设置两根柱子之间的距离
x = 550 + (n - 1) * distance; // 设置柱子初始位置
y = random.nextInt(218) + 132; // 随机设置柱子高度
}
// 更新柱子的位置
public void step(){
x--; // 向左移动
if(x == -width / 2){ // 当柱子移出屏幕时,从右侧重新出现
x = distance * 2 - width / 2;
y = random.nextInt(218) + 132; // 随机设置柱子的高度
}
}
}
// 鸟类,负责鸟的飞行和碰撞检测
class Bird{
BufferedImage image; // 鸟的图片
int x, y; // 鸟的位置
int width, height; // 鸟的宽高
int size; // 鸟的大小,用来碰撞检测
// 飞行参数
double g; // 重力加速度
double t; // 时间
double v0; // 初始速度
double speed; // 当前速度
double s; // 飞行的距离
double alpha; // 飞行角度
// 动画帧数组
BufferedImage[] images;
int index; // 当前帧的索引
// 构造方法,初始化鸟的参数
public Bird() throws Exception {
image = ImageIO.read(getClass().getResource("0.png")); // 读取鸟的图片
width = image.getWidth(); // 获取鸟的宽度
height = image.getHeight(); // 获取鸟的高度
x = 132; // 设置鸟的初始x位置
y = 280; // 设置鸟的初始y位置
size = 10; // 设置鸟的大小
g = 1; // 设置重力加速度
v0 = 10; // 设置初始速度
t = 0.25; // 设置时间间隔
speed = v0; // 设置当前速度为初始速度
s = 0; // 设置飞行距离为0
alpha = 0; // 设置飞行角度为0
// 初始化鸟的动画帧
images = new BufferedImage[8];
for (int i = 0; i < 8; i++) {
images[i] = ImageIO.read(getClass().getResource(i + ".png")); // 读取不同的动画帧
}
index = 0;
}
// 更新鸟的飞行状态
public void fly(){
index++; // 增加帧索引
image = images[(index / 12) % 8]; // 根据索引更新鸟的图片
}
// 更新鸟的位置
public void step(){
double v0 = speed;
s = v0 * t + g * t * t / 2; // 根据速度和时间计算鸟的飞行距离
y = y - (int) s; // 更新鸟的y位置
double v = v0 - g * t; // 更新速度
speed = v;
alpha = Math.atan(s / 8); // 根据飞行距离计算角度
}
// 鸟的跳跃
public void flappy(){
speed = v0; // 重置速度为初始速度
}
// 检测是否碰到地面
public boolean hit(Ground ground){
boolean hit = y + size / 2 > ground.y; // 判断鸟是否与地面碰撞
if(hit){
y = ground.y - size / 2; // 如果碰撞,将鸟的位置调整到地面
}
return hit;
}
// 检测是否碰到柱子
public boolean hit(Column column){
// 判断鸟是否进入柱子的碰撞范围
if (x > column.x - column.width / 2 - size / 2 && x < column.x + column.width / 2 + size / 2) {
if (y > column.y - column.gap / 2 + size / 2 && y < column.y + column.gap / 2 - size / 2) {
return false; // 如果鸟在柱子间隙内,不碰撞
}
return true; // 如果鸟与柱子碰撞
}
return false; // 如果鸟不在柱子的范围内
}
}
运行效果如下:

图一 运行开始
左上角为分数记录器,每安全通过一对柱子障碍,分数便会+1.操作方式相当简单,仅可使用鼠标左键进行短暂跳跃式飞升。

图二 获得分数

图三 game over
当player不进行任何操作时,bird会加速度坠落。触底则游戏结束。
鉴于自身水平与时间有限,原项目框架本身十分优秀,在进行简单重构的情况下,对游戏进行优化。
在对源代码进行分析后,我总结出几点原项目的问题和优化思路:
1.原项目游戏模式简单,需要增加游戏难度,在while(true){};的主页面循环中Thread.sleep(1000 / 120);这串代码控制每秒帧率,通过改变分母数字可以控制游戏进程的速度进而实现难度转变;

图四 ui难度选择
解决方案是在游戏开始时选择难度ui界面(标准,困难,地狱)。我找到了一张start图片,添加ui(三个难度选择按钮),新增文件Index.java,就这样想让index.java和TestBirdFly.java文件联系起来,需要改变难度增加逻辑,增加了startGame(int difculty)函数。
// 按钮点击事件
standardBtn.addActionListener(e -> startGame(1)); // 选择标准难度
hardBtn.addActionListener(e -> startGame(2)); // 选择困难难度
hellBtn.addActionListener(e -> startGame(3)); // 选择地狱难度
// 根据难度调整速度
if (difficulty == 1) columnSpeed = 4; // 标准模式
else if (difficulty == 2) columnSpeed = 7; // 困难模式
else columnSpeed = 10; // 地狱模式
couumnSpeed参数是代表着柱子向左移动的速度。
2.这里颠覆了场景移动的逻辑
原项目逻辑是让地面背景不断移动,当图片边缘出现时,将图片重置,不断循环跑动。
新逻辑则是让柱子移动。这样好处虽然不多,但是确实让移动逻辑更明晰了。这样也许有利于管理难度变换。
3.原项目存在二个漏洞,在bird触碰外物gg时,无法便捷重新开始游戏,并且背景仍在不断刷新,左上角分数按照原项目加分逻辑会一直+1,无法看到定格的最后玩家取得的分数。
这边选择增加一个机制使得在player结束游戏时按“enter”可重新开始游戏,并且更新积分逻辑,优化左上角的积分榜,增添“Score:”字样,便于识别分数。

图五 可重开游戏机制
// 柱子通过屏幕,重置柱子并加分
if (columnX + column.getWidth() < 0) {
resetColumn();
score++;
}
我令每个关键函数在使用时都会进行一个类if (!gameOver) {}判定。
这样将保证game over 时,所有循环函数停止使用。修复原项目漏洞。
最后,在逆向 Flappy Bird 的过程中,遇到了多个挑战,其中物理引擎的实现是核心难点之一。小鸟的跳跃与重力模拟需要合理调整参数,如重力加速度、跳跃力度等,以确保手感接近原版游戏。同时,旋转角度的计算也需要精细调整,既要让小鸟的飞行角度符合视觉习惯,又要保证游戏的流畅性。
此外,键盘事件处理 也是优化重点。由于游戏依赖 KeyListener 监听空格键进行跳跃,但 Swing 的焦点管理机制可能会导致窗口失去焦点后无法响应按键,因此需要确保 setFocusable(true) 并正确处理事件监听。同时,为了增强用户体验,加入了“回车键重新开始游戏”的功能,避免每次失败都要手动重启程序。
从逆向工程的角度来看,通过分析 Flappy Bird 的核心机制,我们拆解了游戏的关键组件,如物理引擎、动画系统、事件响应、碰撞检测等,并在实现过程中不断优化,使代码结构更清晰,便于扩展和维护。例如,加入了难度选择功能,调整了柱子的移动速度,让游戏更具挑战性。
这次逆向开发不仅提升了对游戏开发的理解,也锻炼了Java GUI 编程、事件驱动逻辑、游戏优化等能力,为后续进一步开发更加复杂的游戏奠定了基础。
下面给出Index.java:
package testfly;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
public class Index {
public static void main(String[] args) throws Exception {
JFrame frame = new JFrame("Flappy Bird - Choose Difficulty");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(480, 640);
frame.setLocationRelativeTo(null);
// 背景图
BufferedImage background = ImageIO.read(Index.class.getResource("index_bg.png"));
JPanel panel = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(background, 0, 0, null);
}
};
panel.setLayout(null);
// 标题
JLabel title = new JLabel("Flappy Bird", SwingConstants.CENTER);
title.setFont(new Font("Arial", Font.BOLD, 40));
title.setForeground(Color.RED);
title.setBounds(-25, 20, 480, 50);
panel.add(title);
// 难度按钮
JButton standardBtn = new JButton("Standard");
JButton hardBtn = new JButton("Hard");
JButton hellBtn = new JButton("Hell");
// 按钮样式
for (JButton btn : new JButton[]{standardBtn, hardBtn, hellBtn}) {
btn.setFont(new Font("Arial", Font.PLAIN, 20));
btn.setForeground(Color.WHITE);
btn.setBackground(Color.BLACK);
btn.setFocusPainted(false);
}
// 按钮位置
standardBtn.setBounds(125, 400, 180, 40);
hardBtn.setBounds(125, 450, 180, 40);
hellBtn.setBounds(125, 500, 180, 40);
// 按钮点击事件
standardBtn.addActionListener(e -> startGame(1)); // 选择标准难度
hardBtn.addActionListener(e -> startGame(2)); // 选择困难难度
hellBtn.addActionListener(e -> startGame(3)); // 选择地狱难度
// 添加按钮到面板
panel.add(standardBtn);
panel.add(hardBtn);
panel.add(hellBtn);
frame.add(panel);
frame.setVisible(true);
}
// 启动游戏
private static void startGame(int difficulty) {
JFrame gameFrame = new JFrame("Flappy Bird");
gameFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
gameFrame.setSize(432, 644 + 30);
gameFrame.setLocationRelativeTo(null);
try {
TestBirdFly game = new TestBirdFly(difficulty);
gameFrame.add(game);
gameFrame.setVisible(true);
game.action(); // 启动游戏循环
} catch (Exception e) {
e.printStackTrace();
}
}
}
一些关键的代码:
key code
public TestBirdFly(int difficulty) throws Exception {
// 读取图片
background = ImageIO.read(getClass().getResource("bg.png"));
birdFrames = new BufferedImage[8]; // 初始化鸟的动画帧数组
for (int i = 0; i < 8; i++) {
birdFrames[i] = ImageIO.read(getClass().getResource(i + ".png")); // 读取不同的动画帧
}
bird = birdFrames[0]; // 默认初始帧
ground = ImageIO.read(getClass().getResource("ground.png"));
column = ImageIO.read(getClass().getResource("column.png"));
// 根据难度调整速度
if (difficulty == 1) columnSpeed = 4; // 标准模式
else if (difficulty == 2) columnSpeed = 7; // 困难模式
else columnSpeed = 10; // 地狱模式
// 监听键盘按键
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SPACE && !gameOver) {
velocity = jumpStrength; // 按空格键让小鸟跳跃
birdAngle = 0; // 跳跃时立即将小鸟角度摆正
}
if (e.getKeyCode() == KeyEvent.VK_ENTER && gameOver) {
restartGame(); // 按回车键重新开始
}
}
});
setFocusable(true);
setDoubleBuffered(true);
// 生成随机柱子高度
resetColumn();
}
if (gameOver) {
g.setFont(new Font("Arial", Font.BOLD, 50));
g.setColor(Color.RED);
g.drawString("Game Over", 100, 250);
g.setFont(new Font("Arial", Font.BOLD, 20));
g.setColor(Color.WHITE);
g.drawString("Press ENTER to Restart", 120, 300);
}
}

浙公网安备 33010602011771号