尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

飞翔的小鸟 Java 实现:Swing 游戏循环与碰撞检测详解

飞翔的小鸟 Java 实现:Swing 游戏循环与碰撞检测详解 简介飞翔的小鸟游戏Java实现源码与配套教程专为期末大作业、课程设计及毕业设计场景打造适合有一定Java基础、希望快速上手游戏开发的学生参考与复用。资源包内共45个文件主要包括Java源码、XML配置、PNG图像素材、GIF动效以及Markdown开发文档压缩包大小仅4.33MB目录结构精简便于阅读和二次修改。源码附有详细注释并配套开发教程与README说明有助于理解游戏循环、碰撞检测、界面绘制等核心逻辑简单部署即可运行。项目整体经过严格调试界面美观、操作流畅系统功能完善具备较高的完整度和实际应用价值。目前已有84人学习下载是Java游戏方向期末项目、课程设计高分的有力参考。1. 飞翔的小鸟 Java 实现一个能当高分项目交出去的游戏源码方向临近课程设计验收很多人选游戏方向而飞翔的小鸟总是被点名的那个玩法一眼能看懂、需求说得清、演示效果好。可真正动手才发现这个看似简单的游戏里全是细节——重力加速度设多少手感才顺管道间距怎么定才不会让人想砸键盘碰撞检测为什么总是「看起来没碰到却死了」把这些问题逐个解决就是一份能讲清楚、能演示、能拿高分的小游戏源码项目。这篇笔记从零带你把飞翔的小鸟用纯 Java 写出来核心用 Swing 做窗口和绘图用定时器驱动游戏循环代码结构按「窗口、实体、逻辑、渲染」拆开最后附上分步教程和一组可复现的参数。新手可以按步骤敲出来老手可以直接跳到参数调优和避坑部分核对你的实现细节。2. 核心设计拆解Swing 绘图、游戏循环与碰撞检测2.1 为什么用 Swing Timer而不是 JavaFX 或游戏引擎飞翔的小鸟本质上是一个 2D 平面游戏需要的东西就三样一块能画图的画布、一个每帧回调的循环、一组键盘事件监听。Java 自带的 Swing 组件刚好全部覆盖而且课程设计环境里大概率已经装好了 JDK不需要额外引入依赖。相比之下JavaFX 虽然动画能力更顺但不同版本的 JDK 对 JavaFX 的支持不一致反而容易在环境配置上翻车引入第三方游戏引擎更不现实交源码的时候老师要跑你的工程光是依赖下载就能卡住半天。常见做法是用JPanel作为游戏画布重写它的paintComponent(Graphics g)来绘制小鸟、管道和分数然后用javax.swing.Timer驱动刷新每 16 毫秒左右触发一次。Timer的好处是它在 EDT事件分发线程上执行不会像Thread.sleep那样出现界面卡死的问题也不用手动去管线程同步。2.2 重力和速度模型先定公式再写代码飞翔的小鸟的物理模型不需要真实物理引擎三个变量就能表达完整小鸟的垂直速度velocity、重力加速度gravity、以及点击屏幕时给的一个向上的冲量jumpSpeed。每一帧的逻辑非常简单velocity gravity; // 重力让下落速度越来越快 birdY velocity; // 用当前速度更新位置这三行就是整个游戏的核心运动逻辑。gravity不是「每秒下落多少像素」而是「每帧速度增加多少像素」理解这个区别很重要。比如gravity 0.4f表示每帧速度增加 0.4 像素jumpSpeed -7.0f表示点击一次瞬间获得向上 7 像素/帧的速度。这个数值组合直接决定了游戏难度和操作手感。调参数的时候注意一个关系如果gravity太大小鸟会像自由落体一样迅速砸地如果jumpSpeed的绝对值太小玩家连点鼠标都救不回来。经典手感的区间是gravity在0.3f ~ 0.5fjumpSpeed在-8.0f ~ -6.0f后面第 4 章专门讲怎么调。2.3 碰撞检测的三种范围像素、矩形、圆形飞翔的小鸟有碰撞就有三种判定方式。第一种是矩形碰撞拿小鸟图片的矩形范围和管道矩形的范围做intersects判断最简单也最快缺点是管道是空心矩形图片四个角有空白像素会「假死」。第二种是圆形碰撞把小鸟近似成一个圆和矩形管道逐边判断距离比矩形精确一点。第三种是像素级碰撞逐像素比对两个图片的透明度最准确但性能消耗高在 Swing 里逐帧做不划算。实际项目里用「缩小后的矩形」最合适——不是拿整张图片算而是向内收缩 6~10 个像素再参与碰撞判断。这样既避免了透明像素造成的误判性能也完全够用。判定代码放在每帧管道移动完之后执行碰撞一旦成立就切换到游戏结束状态。3. 完整源码实现从窗口到计分的分步代码3.1 主窗口与画布初始化先把工程骨架立起来。一个JFrame作为主窗口一个GamePanel作为画布。窗口固定大小建议 420x640这是经典手机竖屏的比例放到电脑屏幕上显示也协调。public class BirdGame { public static void main(String[] args) { JFrame frame new JFrame(飞翔的小鸟 - Java实现); GamePanel panel new GamePanel(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setResizable(false); frame.setSize(420, 640); frame.add(panel); frame.setLocationRelativeTo(null); frame.setVisible(true); panel.requestFocus(); // 让面板获得键盘焦点 } }有个细节容易踩frame.setSize(420, 640)设的是窗口外框尺寸实际画布会比这个小因为标题栏占了高度。要么用frame.setBounds加大高度要么在GamePanel里重写getPreferredSize()并配合frame.pack()。我一般直接在面板里定义一个常量PANEL_WIDTH和PANEL_HEIGHT所有逻辑都基于这两个常量算窗口尺寸设得比面板内部大一圈就行。requestFocus()这句不能省否则键盘监听收不到按键事件点击窗口还要先点一下才响应演示的时候很尴尬。3.2 小鸟的物理模型与按键处理GamePanel里维护小鸟的位置birdY和速度velocity点击空格或鼠标左键触发跳跃。这里有一个新手经常写错的点跳跃不是直接把birdY往上移一段而是给velocity赋一个负值。public class GamePanel extends JPanel implements ActionListener, KeyListener { private static final int PANEL_WIDTH 420; private static final int PANEL_HEIGHT 640; private static final float GRAVITY 0.4f; private static final float JUMP_SPEED -7.0f; private static final int BIRD_X 120; // 小鸟固定横坐标 private float birdY PANEL_HEIGHT / 2f; private float velocity 0f; private Timer timer; public GamePanel() { setBackground(new Color(0x4EC0CA)); timer new Timer(16, this); // 约60帧/秒 timer.start(); addKeyListener(this); setFocusable(true); } private void jump() { velocity JUMP_SPEED; // 速度直接重置为向上的冲量 } Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() KeyEvent.VK_SPACE) { jump(); } } Override public void actionPerformed(ActionEvent e) { velocity GRAVITY; // 每帧重力加速 birdY velocity; // 按速度移动 repaint(); // 触发重绘 } }Timer(16, this)是核心所在第一个参数是毫秒间隔16 毫秒约等于 60 FPS第二个参数是监听器每次定时器触发都会调用actionPerformed。用float而不是int存位置和速度非常重要因为0.4f的重力累加到整数上会被截断表现就是小鸟的位移一顿一顿的。这里还埋了一个「手感玄学」window 系统下Timer的触发间隔不是严格准确的16 毫秒可能变成 15 或 20 毫秒。因为每帧位移量是固定的不依赖真实时间差所以不同性能的机器上游戏速度会略有差异。课程设计不用管这个但如果你想做成发布级的游戏要用System.nanoTime()计算真实帧间隔来做增量更新。3.3 管道生成与移动逻辑管道是游戏的第二个关键对象。一套管道包含上管道和下管道中间的间隙就是小鸟要穿过的通道。管道在右边缘生成以恒定速度向左移动移出左边界后销毁。public class Pipe { int x; int gapY; // 间隙中心的Y坐标 int width 70; int gapHeight 170; // 上下管道之间的空隙高度 }管道列表放在GamePanel里用一个ListPipe存储。生成节奏由距离控制每当最右侧管道的横坐标小于某个阈值比如PANEL_WIDTH - 220时就生成一个新管道。这样无论游戏跑多快管道之间的水平间距都能保持稳定。private ListPipe pipes new ArrayList(); private static final int PIPE_SPACING 220; // 两根管道之间的水平距离 private void updatePipes() { if (pipes.isEmpty() || pipes.get(pipes.size() - 1).x PANEL_WIDTH - PIPE_SPACING) { Pipe p new Pipe(); p.gapY 150 new Random().nextInt(280); // 间隙中心在150~430之间随机 p.x PANEL_WIDTH; pipes.add(p); } for (Pipe pipe : pipes) { pipe.x - 3; // 管道左移速度每帧3像素 } pipes.removeIf(pipe - pipe.x pipe.width 0); }gapY的随机范围决定了游戏难度。150~430 意味着管道间隙可以在 150 像素高到 430 像素高的范围内变动配合 170 像素的间隙高度小鸟穿越的角度会比较丰富。如果范围设太小比如 250~350所有管道的高度几乎一样游戏变得单调如果范围设太大会出现间隙贴到屏幕边缘导致玩家根本没法反应的情况。管道左移速度pipe.x - 3和Timer的 16 毫秒间隔组合大约是 187 像素/秒的移动速度。这个速度要跟小鸟的JUMP_SPEED配合太快了玩家来不及反应太慢了游戏节奏拖沓。3.4 碰撞检测与游戏状态切换游戏状态分为「准备中」「飞行中」「已结束」三种。准备中显示提示文字按空格后进入飞行飞行中运行完整逻辑已结束显示分数并等待重启。private enum GameState { READY, FLYING, GAMEOVER } private boolean checkCollision() { if (birdY 0 || birdY PANEL_HEIGHT - 40) { return true; // 撞到上下边界 } Rectangle birdRect new Rectangle(BIRD_X 8, (int) birdY 8, 40 - 16, 30 - 16); for (Pipe pipe : pipes) { Rectangle upRect new Rectangle(pipe.x, 0, pipe.width, pipe.gapY - pipe.gapHeight / 2); Rectangle downRect new Rectangle(pipe.x, pipe.gapY pipe.gapHeight / 2, pipe.width, PANEL_HEIGHT); if (birdRect.intersects(upRect) || birdRect.intersects(downRect)) { return true; } } return false; }注意birdRect的构造原始图片假设是40x30我在四边各收缩了 8 像素这样小鸟头尾的透明角落就不会触发误判。上下边界判定里PANEL_HEIGHT - 40而不是PANEL_HEIGHT是给底部留了一点缓冲实际上如果小鸟贴到最底部才死玩家的挫败感会小很多。游戏中还有一个经常被忽略的细节——计分。计分时机不是「碰到管道得分」而是「小鸟越过了管道的右边缘」。判断方法很简单管道右边缘横坐标小于小鸟横坐标且该管道没被计过分。每根管道记录一个boolean scored字段越过之后置为true避免一帧内重复加分。3.5 绘制与图形刷新所有绘制集中在paintComponent一帧画完调用repaint()触发下一帧。绘制顺序有讲究先背景、再管道、再小鸟、最后分数和状态文字。Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 (Graphics2D) g; // 绘制管道 g2.setColor(new Color(0x5B8C3E)); for (Pipe pipe : pipes) { int gapTop pipe.gapY - pipe.gapHeight / 2; int gapBottom pipe.gapY pipe.gapHeight / 2; // 上管道从0画到间隙顶部 g2.fillRect(pipe.x, 0, pipe.width, gapTop); // 下管道从间隙底部画到底 g2.fillRect(pipe.x, gapBottom, pipe.width, PANEL_HEIGHT - gapBottom); } // 绘制小鸟用圆形代替实际项目可换成图片 g2.setColor(new Color(0xF8D347)); g2.fillOval(BIRD_X, (int) birdY, 40, 30); // 绘制分数 g2.setColor(Color.WHITE); g2.setFont(new Font(SansSerif, Font.BOLD, 28)); g2.drawString(String.valueOf(score), PANEL_WIDTH / 2 - 10, 80); // 状态提示 if (state GameState.READY) { g2.drawString(按空格开始, PANEL_WIDTH / 2 - 50, PANEL_HEIGHT / 2); } else if (state GameState.GAMEOVER) { g2.drawString(得分: score, PANEL_WIDTH / 2 - 50, PANEL_HEIGHT / 2); g2.drawString(按空格重新开始, PANEL_WIDTH / 2 - 70, PANEL_HEIGHT / 2 40); } }super.paintComponent(g)必须保留它负责清空上一帧的画面否则会出现「拖影」——上一帧的小鸟和管道位置还留在画布上整个画面糊成一团。这也是 Swing 绘图最容易翻车的地方。上面代码先用圆形代替了小鸟图片跑通逻辑后再换图。换图时用Image或ImageIcon加载资源文件在paintComponent里用g2.drawImage()替代fillOval()即可。注意图片要用getResource加载不要用绝对路径否则换台机器就找不到图片了。4. 教程式参数调优跳一次多高才手感好4.1 必调参数清单与作用范围飞翔的小鸟的源码本身不难难的是让游戏「好玩」——太简单玩家不会死太难了连作者自己都过不去第一根管道。好在所有手感都集中在一组参数上调参不需要改逻辑代码。参数建议范围影响GRAVITY0.3f ~ 0.5f下落加速度越大游戏速度感越强JUMP_SPEED-8.0f ~ -6.0f跳跃力度绝对值越大跳得越高PIPE_SPACING180 ~ 260管道水平间距越小反应时间越短pipe.x移动速度2 ~ 4游戏整体节奏gapHeight140 ~ 200缝隙大小越小越难gapY随机范围120 ~ 480管道高度变化幅度一组经过大量试玩后比较稳的入门组合是GRAVITY 0.35fJUMP_SPEED -6.5fPIPE_SPACING 200管道左移速度3gapHeight 160。这个组合下小鸟一次跳跃大约上升 90~100 像素玩家点击频率约每秒 2~3 次节奏接近手机版原作的体验。4.2 手感调试的「秒杀法」和判断标准调参最怕「凭感觉猜」我给你一个可以在十分钟内完成的调试流程。第一步把GRAVITY调成极端的0.8fJUMP_SPEED调成-3f玩一局——你会发现小鸟根本控不住像一块砖头往下掉这会让你清楚地看到「太难」是什么状态。第二步反向调到GRAVITY 0.1fJUMP_SPEED -12f小鸟会飘在屏幕顶部不下来管道走过去都碰不到它这是「太简单」。第三步从极端值往中间收敛每次只动一个参数直到一局能稳定穿越 10 根以上管道为止。还有一个数字指标可以参考小鸟从最高点回落到最低点撞地的时长应该控制在 2.5~3.5 秒之间。计算方法是用velocity的初末速度差除以GRAVITY。比如JUMP_SPEED -6.5fGRAVITY 0.35f那么从跳到最高点到回到原位大约需要6.5 / 0.35 * 2 ≈ 37帧也就是约 0.6 秒。如果这个时长低于 0.4 秒玩家会觉得手指追不上高于 1 秒游戏会变得无聊。注意每次改完参数必须重启程序因为GRAVITY和JUMP_SPEED是static final常量改完直接刷新没用。等你把参数调满意了再决定要不要把这些常量提取到配置文件里。5. 飞翔的小鸟 Java 实现避坑指南5.1 画面闪烁严重现象游戏运行后画面上下跳或者有严重的残影尤其移动管道的时候能明显看到之前位置的影子。原因Swing 的repaint()默认不清除旧内容或者清除了但重绘速度跟不上Timer的频率。另外super.paintComponent(g)被遗忘也是一个原因上一帧画面完全没被覆盖。解决第一行必须是super.paintComponent(g)。如果仍然闪烁启用双缓冲——JPanel默认已开启双缓冲但如果重写了paint而不是paintComponent就会绕开它。用paintComponent不要用paint这是 Swing 绘制机制的面试级考点。5.2 碰撞判定「看着没碰到却死了」现象小鸟的嘴或尾巴明明还有一段距离才碰到管道游戏却判定碰撞结束。原因典型的矩形碰撞拿原图尺寸计算而 PNG 图片四周有透明像素。透明区域也算进了矩形范围导致碰撞箱比小鸟实际轮廓大了一圈。解决给碰撞矩形向内收缩边距代码里写new Rectangle(BIRD_X 8, birdY 8, 32, 22)代替原尺寸 40x30。收缩的像素数通常取图片宽高的 20% 左右具体值需要根据实际素材微调。如果你用的是带透明通道的 PNGA 素材可以在代码里用image.getWidth()动态获取尺寸后做比例收缩。5.3 按空格没反应或者窗口点了没反应现象窗口能显示但按键没有任何效果鼠标点击窗口一次后按键才响应。原因面板没有获得焦点。JFrame的焦点默认在窗口本身KeyListener注册在JPanel上收不到事件。点击窗口相当于把焦点给了面板之后才生效。解决初始化面板时调用panel.setFocusable(true)然后在main方法里窗口显示后调用panel.requestFocus()。另外更靠谱的方案是改用InputMap和ActionMap来实现按键绑定这样不依赖焦点代码也更规范InputMap inputMap panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); ActionMap actionMap panel.getActionMap(); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), jump); actionMap.put(jump, new AbstractAction() { Override public void actionPerformed(ActionEvent e) { jump(); } });WHEN_IN_FOCUSED_WINDOW表示只要窗口在前台就能响应不会出现点击后才能操作的「玄学问题」。5.4 管道生成越来越快或者越来越慢现象刚启动时难度正常玩了半分钟后管道密集程度明显变化或者游戏运行一段时间后 FPS 下降。原因管道生成逻辑写在了「按帧数累积」上比如每 100 帧生成一次导致生成节奏和实际游戏速度绑定或者管道列表只增不减移出屏幕的Pipe对象没有被移除渲染和碰撞检测每帧都要遍历越来越多的对象。解决生成节奏用「最右侧管道的横坐标 间距」来判断代码参考 3.3 节。清理逻辑用pipes.removeIf(...)放在每次移动之后。如果你用每帧计数的写法应该改成基于像素距离这样才能保证不同 FPS 下节奏一致。5.5 Timer 与 Thread.sleep 混用导致卡死现象游戏运行中偶尔卡顿一下或者按空格后窗口完全无响应。原因部分教程会建议用while(true) { update(); repaint(); Thread.sleep(16); }实现游戏循环但同时又在main里启动了Swing Timer两个循环同时调repaint()而Thread.sleep会阻塞 EDT 导致窗口无法处理键盘事件。解决只用一种驱动方式。课程设计用javax.swing.Timer就够了如果你确实要自定义循环确保循环代码跑在独立线程里用SwingUtilities.invokeLater包裹 UI 更新操作并且不要在 EDT 上调用Thread.sleep。6. 进阶优化把项目从「能跑」做到「高分」6.1 逻辑循环与渲染循环分离当前实现里actionPerformed方法同时承担了物理更新和repaint()调用这在帧率要求不高的课程项目里够用。但如果你想让项目在评审时被问到「能不能做一个独立的游戏循环」可以重构为固定步长更新用System.nanoTime()记录上一帧时间每帧计算真实时间差delta用delta去修正位移量。long lastTime System.nanoTime(); double delta 0.0; public void gameLoop() { while (running) { long now System.nanoTime(); delta (now - lastTime) / 1_000_000_000.0; // 单位秒 lastTime now; update(delta); // 位移量乘以delta物理与帧率解耦 render(); } }这样改之后即使一台电脑跑 120 FPS、另一台跑 30 FPS小鸟的移动速度在真实时间里完全一致。这对老手来说属于必改项对新手来说可以作为答辩加分点来准备。6.2 绘制小鸟的旋转角度原版飞翔的小鸟里小鸟不是水平直飞的——上跳时头朝上下落时头朝下姿态随速度变化。这个细节非常小但对观感提升巨大。用Graphics2D的旋转功能在绘制前根据velocity计算一个角度。float angle Math.min(Math.max(velocity * 0.06f, -0.5f), 0.5f); Graphics2D g2 (Graphics2D) g; g2.translate(BIRD_X 20, birdY 15); g2.rotate(angle); g2.drawImage(birdImage, -20, -15, 40, 30, null); g2.rotate(-angle); g2.translate(-(BIRD_X 20), -(birdY 15));velocity * 0.06f把速度映射到弧度范围并限制在±0.5弧度之间避免大头朝下。这段代码写完以后每次跳跃小鸟会先仰头、再俯冲游戏质感立刻上一个台阶。6.3 资源加载与状态扩展把图片、音频统一放到resources目录用类加载器读取而不是写C:/Users/...这种绝对路径。算分逻辑可以扩展出「最高分」用Preferences类做本地存储下次启动时自动读取历史最高分显示在开始界面上。这部分代码不超过 30 行但能让项目看起来完整很多。Preferences prefs Preferences.userNodeForPackage(GamePanel.class); int highScore prefs.getInt(highScore, 0); // 游戏结束时 if (score highScore) { prefs.putInt(highScore, score); }评审老师问「数据怎么保存」时这比随口说「每次都重新开始」要体面得多。最后说一个我做这个项目的教训不要把参数调好的时间排在交付前一天。按下空格后小鸟飞得快还是慢这种事文字写不清楚只有亲手玩几十局才能找到那组「玩家想继续玩下去」的参数组合。我第一次调完直接交了上去结果演示时一上来就连撞两次场面相当尴尬。先让同事玩一玩、也让你负责演示的朋友玩一玩把手指节奏对应起来再定稿。这一套源码加调试流程跑下来你对 Swing 的认识已经能盖过大多数用模板交作业的同学了希望帮到你。本文还有配套的精品资源点击获取
返回列表