简介:这是一份面向Java初学者与游戏编程爱好者的「飞翔的小鸟」完整源码工程,适合想通过经典小游戏理解面向对象设计与游戏循环机制的开发者练手。压缩包共32个文件,约139KB,以18个png图片资源、5个java源文件、6个class编译文件为主,另含classpath、project、prefs等Eclipse工程配置,导入后即可直接运行调试。项目围绕Bird、ScoreSystem、PoleGenerator、StarGenerator等核心类展开,完整实现了重力与碰撞检测、柱子与星星的周期生成、动态难度调整以及计分奖励逻辑:分数低于15时正常速度过柱得1分,15至30之间提速并得2分,超过30再次提速得3分,吃到星星额外加2分。目前已有2952人学习下载,读者可借此掌握Swing或JavaFX界面绘制、帧率与像素速度调节、游戏状态管理等实用技巧,并在此基础上自行扩展玩法。
1. 从一份 Java 版「飞翔的小鸟」源码说起:它到底能跑出什么
很多人第一次接触游戏开发,不是从引擎开始的,而是从一份能直接跑起来的完整源码开始的。这份 Java 版「飞翔的小鸟」代码完整版,就是这样一个入口:它把经典的下落式躲避玩法用纯 Java 实现了一遍,包含窗口初始化、游戏主循环、小鸟物理运动、管道随机生成、碰撞检测、分数统计和结束重开这一整套闭环逻辑。你拿到手不是一堆散装片段,而是一个能编译、能运行、能改的完整工程。
它适合三类人:刚学完 Java 基础语法、想找个能看见画面的练手项目的新手;想搞懂游戏主循环和碰撞检测到底怎么落地、而不是停留在概念层面的进阶者;以及需要一份干净骨架、准备往上加音效、加道具、加排行榜的开发者。下面我不讲空泛的编程思想,只讲这份源码怎么读、怎么跑、参数怎么调、哪里最容易翻车。
2. 拆解游戏主循环:Timer 与 repaint 的配合逻辑
2.1 为什么这类小游戏离不开固定帧循环
游戏和普通桌面程序最大的区别,是它必须持续地「更新状态 + 重绘画面」。这份源码采用的是 Swing 体系里最常见的做法:用一个定时器周期性触发逻辑更新,再调用重绘把最新状态画到面板上。理解这条主线,后面所有代码你都能对上号。
核心思路是:定时器每隔一个固定间隔触发一次,在回调里做三件事——更新小鸟的纵向速度与位置、移动所有管道、检测是否发生碰撞或越界。做完之后请求重绘,Swing 会在合适的时机调用绘制方法,把当前这一帧画出来。这个「更新」和「绘制」分离的结构,是后面你能顺利加功能的前提。
// 游戏主循环:定时器驱动逻辑更新 Timer timer = new Timer(1000 / 60, new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // 1. 更新小鸟物理状态 bird.update(); // 2. 移动并回收管道 for (Pipe pipe : pipes) { pipe.move(); } // 3. 碰撞与边界检测 checkCollision(); // 4. 请求重绘,触发 paintComponent repaint(); } }); timer.start();逻辑说明:1000 / 60表示目标约 60 帧每秒,也就是每帧约 16 毫秒。参数说明:这个数字直接决定游戏手感,调大(比如1000 / 30)会明显卡顿,调小(比如1000 / 120)在低配机器上可能因为重绘跟不上而抖动。我一般会把它抽成一个常量FPS,方便统一调整。
2.2 更新与绘制分离带来的实际好处
把逻辑更新放在定时器里、把绘制放在paintComponent里,好处是绘制只负责「读当前状态并画出来」,不掺杂任何状态修改。这样你后面加暂停功能时,只需要停掉定时器,画面会自然停在最后一帧,不用去改绘制代码。
@Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 只读状态,不做任何修改 g.drawImage(background, 0, 0, null); for (Pipe pipe : pipes) { pipe.draw(g); } bird.draw(g); g.setColor(Color.WHITE); g.drawString("Score: " + score, 20, 30); }逻辑说明:super.paintComponent(g)必须先调用,否则上一帧的残影不会被清除,画面会拖尾。参数说明:drawString的坐标是文字左下角基准点,不是左上角,新手常在这里把分数画到屏幕外。
提示:如果你发现画面闪烁,先确认是不是在
paintComponent里直接改了状态,或者漏掉了super调用。
3. 小鸟物理与管道生成:几个必须调对的参数
3.1 重力、跳跃速度与下落手感
小鸟的运动本质是一维变速运动:每帧速度受重力影响增加,位置再按速度更新;点击时给速度一个向上的瞬时赋值。这套模型简单,但参数配比决定了游戏是「能玩」还是「劝退」。
// 小鸟物理参数 private static final double GRAVITY = 0.5; // 每帧重力加速度 private static final double JUMP_SPEED = -8.0; // 点击时的向上初速度 private double velocityY = 0; // 当前纵向速度 public void update() { velocityY += GRAVITY; // 速度持续受重力影响 y += velocityY; // 位置按速度更新 } public void jump() { velocityY = JUMP_SPEED; // 瞬时重置为向上速度 }逻辑说明:GRAVITY和JUMP_SPEED是一对必须一起调的参数。参数说明:重力偏大、跳跃偏小,小鸟会像石头一样往下砸;重力偏小、跳跃偏大,小鸟会飘得像在月球。常见做法是让跳跃初速度的绝对值约为重力的 15 到 20 倍,这样一次点击大约能上升 60 到 80 像素,手感比较接近经典版本。
3.2 管道间距与随机高度的边界控制
管道生成的难点不在「生成」,而在「生成得合理」。如果随机高度范围没控制好,会出现上下管道缝隙贴边、甚至缝隙为负导致必死的情况。
// 管道生成参数 private static final int PIPE_GAP = 150; // 上下管道之间的缝隙高度 private static final int MIN_TOP = 50; // 上管道最小高度 private static final int MAX_TOP = 350; // 上管道最大高度 private void spawnPipe() { Random random = new Random(); // 上管道高度在安全区间内随机 int topHeight = MIN_TOP + random.nextInt(MAX_TOP - MIN_TOP); pipes.add(new Pipe(screenWidth, topHeight, PIPE_GAP)); }逻辑说明:topHeight是上管道从顶部往下延伸的高度,下管道的位置由topHeight + PIPE_GAP推出。参数说明:MIN_TOP和MAX_TOP必须保证MAX_TOP + PIPE_GAP不超过屏幕高度,否则下管道会跑出画面。我一般会先算出屏幕高度,再反推这两个边界值,而不是拍脑袋写死。
注意:
Random每次new出来虽然能用,但更稳妥的做法是把它作为成员变量复用,避免高频生成时的额外开销。
4. 碰撞检测与分数判定:矩形相交的落地写法
4.1 用矩形相交做碰撞,够用且好调
这类游戏不需要像素级碰撞,用矩形包围盒相交就足够,而且性能好、易调试。小鸟和管道各自维护一个矩形区域,每帧判断是否相交即可。
// 碰撞检测:矩形相交 public boolean intersects(Rectangle other) { return this.x < other.x + other.width && this.x + this.width > other.x && this.y < other.y + other.height && this.y + this.height > other.y; } private void checkCollision() { Rectangle birdRect = bird.getBounds(); for (Pipe pipe : pipes) { if (birdRect.intersects(pipe.getTopBounds()) || birdRect.intersects(pipe.getBottomBounds())) { gameOver(); return; } } // 越界检测 if (bird.getY() < 0 || bird.getY() > screenHeight) { gameOver(); } }逻辑说明:四个条件分别判断两个矩形在四个方向上是否分离,全部不分离才算相交。参数说明:getBounds()返回的矩形宽高建议比图片实际尺寸略小一圈,这样视觉上「擦边」时不会误判死亡,手感更宽容。
4.2 分数只在通过管道时加一次
分数判定最常见的翻车是「一根管道加了好几次分」。解决办法是给每根管道加一个标记位,通过后置为已计分。
private void updateScore() { for (Pipe pipe : pipes) { // 小鸟完全越过管道右边缘,且该管道未计分 if (!pipe.isScored() && bird.getX() > pipe.getX() + pipe.getWidth()) { score++; pipe.setScored(true); } } }逻辑说明:isScored保证每根管道只贡献一次分数。参数说明:判断条件用小鸟左边缘越过管道右边缘,而不是中心点,这样计分时机更符合视觉预期。
5. 避坑排查:这份源码最容易翻车的五个地方
5.1 画面闪烁或拖影
现象:运行后小鸟和管道有残影,移动时像拖了一条尾巴。原因:paintComponent里没有先调用super.paintComponent(g),或者用了双缓冲但没开启。解决:确保第一行调用super.paintComponent(g);Swing 默认已支持双缓冲,不要手动关掉。
5.2 点击没反应或反应延迟
现象:鼠标点了但小鸟不跳,或者要连点好几下才跳一次。原因:键盘/鼠标监听器注册在了错误的组件上,或者焦点被其他组件抢走。解决:把监听器注册到游戏面板本身,并在面板获得焦点后再开始游戏;用鼠标监听时优先用mousePressed而不是mouseClicked,响应更快。
5.3 管道生成后缝隙贴边甚至为负
现象:偶尔出现上下管道几乎贴在一起、根本过不去的情况。原因:随机高度范围没有和缝隙高度、屏幕高度做联合约束。解决:按第 3.2 节的公式,先确定屏幕高度,再反推MIN_TOP和MAX_TOP,保证MAX_TOP + PIPE_GAP留出足够的下管道空间。
5.4 游戏结束后无法重开
现象:撞管道后画面卡死,点重开没反应。原因:结束时只停了定时器,但没有重置小鸟位置、速度、管道列表和分数。解决:写一个统一的reset()方法,把所有可变状态恢复初始值,再重新start()定时器。
5.5 分数显示错位或乱码
现象:分数画到了屏幕外,或者中文显示成方块。原因:drawString坐标用了左上角思维,或者字体不支持中文。解决:坐标按左下角基准点算;如果要在画面里显示中文,显式设置支持中文的字体,否则就用英文或数字。
提示:这五条里,前三条几乎每个新手都会踩一遍,建议跑通后先照着排查一轮,再动手改功能。
6. 进阶改造:把这份骨架变成你自己的项目
跑通原版只是起点,真正让这份源码值回票价的是它足够干净、方便你往上加东西。我一般会按下面的顺序做二次开发,每一步都能独立验证,不会一次改太多导致排查困难。
第一步是加状态机。原版只有「进行中」和「结束」两个状态,你可以扩成「准备、进行、暂停、结束」四态,用一个枚举变量控制,定时器回调里先判断状态再决定是否更新逻辑。这样加暂停菜单、加开始界面都会顺很多。
第二步是抽离配置。把重力、跳跃速度、管道缝隙、生成间隔、帧率这些全部集中到一个配置类或配置文件里,改手感时不用满工程找数字。下面是一个简单的配置类写法:
public class GameConfig { public static final int FPS = 60; public static final double GRAVITY = 0.5; public static final double JUMP_SPEED = -8.0; public static final int PIPE_GAP = 150; public static final int PIPE_INTERVAL = 90; // 每多少帧生成一根管道 }逻辑说明:所有可调参数集中管理,改一处即可全局生效。参数说明:PIPE_INTERVAL控制难度节奏,数值越小管道越密,配合PIPE_GAP一起调就能做出从易到难的曲线。
第三步是加难度递增。常见做法是随着分数提高,逐步缩小PIPE_GAP或缩短PIPE_INTERVAL,但要注意设下限,否则后期会变成不可能完成的任务。我一般会让缝隙最小不低于小鸟高度的 2.5 倍。
第四步是加音效和动画。音效用javax.sound.sampled加载短音频,在跳跃、得分、碰撞三个时机触发;动画则可以把小鸟的绘制换成多帧切换,用帧计数取模来决定画哪一帧。
验证改造是否成功,我习惯用三个检查点:一是连续玩三分钟不崩、不卡;二是每加一个功能,先把原版行为回归一遍,确认没改坏;三是把配置参数拉到极端值各跑一次,看边界处理是否还稳。
从那以后我每次拿到一份能跑的完整源码,都强制先跑通原版、再按配置类收口参数、最后才动功能,绝不一上来就大改。这套顺序帮我省下了大量「改完不知道哪里坏了」的时间。希望帮到你。
本文还有配套的精品资源,点击获取