逆向软件工程-对flappy bird(java)小游戏的简单重构及分析

当然,这是一篇作业博客。

引言

这是一次很有意思的机会,因为近期上海举办了2025全球开发者先锋大会(GDC),我在逛展中看到Fopo折叠三面显示屏延伸器产品时,发现演示笔记本电脑里展示的是使用小浣熊ai生成的flapyy bird小游戏核心代码。有消息表示2025年flappy bird重制版将重新上线。出于好奇,我便在博客园中找到了相关的java项目,便有了这篇博客。

项目出处:https://www.cnblogs.com/ftl1012/p/9347708.htmlhttps://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);
        }
    }
posted @ 2025-02-23 16:07  Saoirr  阅读(92)  评论(0)    收藏  举报