ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java Swing开发捕鱼达人:从游戏逻辑到碰撞检测实战

Java Swing开发捕鱼达人:从游戏逻辑到碰撞检测实战 简介这是一份面向Java学习者与游戏开发初学者的完整捕鱼达人桌面游戏源码项目把一个典型的Java游戏开发流程完整呈现出来玩家控制大炮发射子弹捕捉游鱼捕获不同鱼种获得对应分数背后需要综合运用对象建模、界面绘制、多线程调度、事件监听、动画与帧率控制、碰撞检测等核心技能。压缩包体积约12.8MB一共包含233个文件其中既有63个Java源文件和95个class编译产物也有png图片素材、ogg与mp3音效文件、plist配置说明以及jar依赖库方便对照代码理解图片、音频、配置等资源在游戏中的加载方式。同时还提供了bat启动脚本和项目配置文件便于直接运行和二次开发适合课程设计或毕业设计场景。已有800人学习/下载读者可以从这个项目中掌握游戏循环、资源管理、对象组织与常见排错思路是一份兼具完整性与实用性的入门练手项目。 看到“Java实现捕鱼达人游戏源码”这个标题我的第一反应是又是一个经典到不能再经典的Java练手项目。捕鱼达人这游戏从街机厅火到手机端玩过的人不少但真正自己动手写过的恐怕没那么多了。其实拿Java来复刻一个捕鱼达人是一个性价比极高的学习路径——它能一次性覆盖Swing图形界面、事件监听、多线程刷新、碰撞检测、游戏状态管理等一堆Java核心知识点而且做完之后成就感是实打实的。这篇文章我就把这个项目的完整思路、核心代码逻辑和我在实际开发中踩过的坑完整拆一遍给你一份可以直接照着写的“作业参考”。1. 项目整体设计与玩法拆解动手写代码之前我通常先不做任何技术选型而是先把“游戏应该是什么样”定义清楚。捕鱼达人这个项目如果剥掉华丽的外皮核心其实就四件事鱼从哪来、鱼往哪走、子弹怎么打、命中了怎么算。1.1 捕鱼达人的核心玩法逻辑捕鱼达人最基础的玩法是玩家控制炮台点击屏幕发射炮弹炮弹在水中飞行碰到鱼时触发捕获判定。不同种类的鱼有不同的分值捕获难度也不同。小丑鱼、灯笼鱼这类容易打、分低金鲨、蓝鲸这类难打但打中一条就能让分数大幅上涨。在落地成Java代码的时候这五个环节可以拆成五个独立的模块来设计鱼群生成模块负责在屏幕边缘随机生成鱼控制生成频率和鱼的类型。鱼群移动模块负责让鱼按指定路径游动游出屏幕后回收。炮台与子弹模块负责炮台角度跟随鼠标、子弹发射和飞行。碰撞检测模块负责检测子弹与鱼是否相交并计算捕获结果。计分与游戏状态模块负责金币、炮台等级、游戏结束条件等UI和状态更新。这个拆法不仅让代码结构清晰也方便你后期扩展——比如将来想加一个新鱼种只需要新增一个鱼类型然后把它挂到生成器里就行完全不需要改动其他模块。1.2 项目包结构与类设计规划很多初学者拿到这个项目就直接开写最后写出来的代码全挤在几个类里改一个功能能引发三个bug。我建议在一开始就把类结构规划好。当时我的项目包结构长这样com.fish.game ├── main/GameFrame.java // 游戏主窗口 ├── panel/GamePanel.java // 游戏画布、核心绘制与循环 ├── model/Fish.java // 鱼类 ├── model/FishType.java // 鱼类型枚举 ├── model/Bullet.java // 子弹类 ├── model/Turret.java // 炮台类 ├── manager/FishManager.java // 鱼群生成与管理 ├── manager/CollisionManager.java // 碰撞检测 └── util/ImageUtil.java // 图片资源加载核心思路是“模型-视图-控制器”的简化版变体Fish、Bullet这些是模型只管数据和状态GamePanel是视图兼控制器负责绘制和接收用户输入Manager类负责游戏逻辑的调度。提示没必要一上来就套太重的框架或设计模式。捕鱼达人这个体量的项目做到“各司其职”就够了过度设计反而是负担。1.3 为什么选Swing而不是JavaFX或其他框架这是不少人问过我的问题。Java做游戏最常被提到的选项有三个纯Swing/AWT、JavaFX、还有LibGDX这种外部库。我的建议是练手项目优先选Swing。原因很简单Swing是JDK自带的不需要额外配置依赖你只要有JDK就能跑JavaFX虽然是Java官方的GUI框架但配置起来相对繁琐而且它的线程模型和Swing不太一样对新手来说反而多一层学习成本LibGDX确实强大但它面向的是专业游戏开发引入之后你得先了解它的生命周期、资源加载机制、渲染管线已经偏离了“用Java实现一个游戏”的学习目标。Swing虽然性能不如专业游戏引擎但捕鱼达人这种2D游戏绘制速度完全够用。核心做法是在JPanel上重写paintComponent方法用双缓冲绘制后面我会详细说。2. 核心技术选型与关键实现原理这个项目看着像是个玩具但里面涉及的技术点其实相当硬核。每个核心机制背后都有值得深挖的原理尤其是线程模型和碰撞检测这两块容易写错也最容易在面试时被追问。2.1 Swing绘制机制与双缓冲原理Swing的绘制原理表面上很简单你继承JPanel重写paintComponent(Graphics g)然后在这个方法里画东西。但实际运行时会涉及一个关键机制——重绘调度。当你调用repaint()方法时其实并不是立刻就会去调用paintComponent()而是由Swing的事件分发线程EDTEvent Dispatch Thread在未来的某个时间点统一处理。这种机制的好处是避免了多线程并发绘制同一个组件导致的界面崩溃问题。但随之而来的问题是如果你的绘制逻辑比较复杂画面会闪烁。这是因为系统在擦除背景和重新绘制之间有一个短暂的时间差人眼能感知到这个闪烁。解决方案就是双缓冲——先在内存里的“虚拟画布”上完成所有绘制再把整个画布一次性复制到屏幕上。在Swing里JPanel默认就已经双缓冲了只要你没有手动关闭它。所以你在写代码的时候只需要放心地在paintComponent里绘制你的鱼和子弹不需要关心底层双缓冲细节。这也是我推荐Swing的一个重要原因底层机制帮你处理好了很多头疼的事。2.2 游戏主循环与多线程刷新设计游戏和普通GUI程序的本质区别在于游戏需要持续不断地刷新界面形成动画效果。捕鱼达人里的鱼在游动子弹在飞行炮台在转动这些都在每一帧变化。实现方式有两种使用javax.swing.Timer定时器每隔固定毫秒数触发一个事件在事件里更新游戏状态并重绘。使用独立线程循环在run方法里执行“更新逻辑”然后调用repaint()。我的建议是用Swing Timer。用独立线程容易踩线程安全问题的坑——Swing组件只能在EDT线程中进行操作你在另一个线程里直接调用repaint()虽然通常不会报错但如果你在非EDT线程里修改了组件状态可能会引发偶发性的界面错乱或异常。Timer则是天然跑在EDT线程上的每次触发就相当于在Swing的事件流里执行一次更新和重绘不会产生线程安全问题。当时我的主循环设置的是每秒60帧也就是每隔16毫秒刷新一次Timer timer new Timer(16, e - { updateGameState(); // 更新鱼位置、子弹位置 repaint(); // 请求重绘 }); timer.start();这个帧率对于捕鱼达人来说已经足够流畅了。设置太高没有意义因为人眼能感知到的流畅度上限大概就是60帧设置太低画面会卡顿尤其当鱼多子弹多的时候。2.3 碰撞检测的核心算法选择碰撞检测是整个游戏里最核心的逻辑之一也是最容易写出bug的地方。捕鱼达人里的碰撞检测有两种主流方案矩形碰撞检测把鱼和子弹都看作一个矩形包围盒检测两个矩形是否相交。圆形碰撞检测把鱼和子弹都看作一个圆计算圆心距离是否小于半径之和。矩形检测的实现最简单Java的Rectangle类自带intersects(Rectangle r)方法直接用就行boolean hit fish.getBounds().intersects(bullet.getBounds());圆形检测稍微复杂一点但更贴合实际形状代码也很简单double dx fishCenterX - bulletCenterX; double dy fishCenterY - bulletCenterY; double distance Math.sqrt(dx * dx dy * dy); boolean hit distance fishRadius bulletRadius;我当时是混用两种方案的。子弹和鱼都用矩形包围盒做一次初步检测如果矩形不相交那么肯定没碰撞直接跳过。如果矩形相交了再判断是否真的命中。这个思路本质上就是“先粗检测再精检测”效率高且不易漏判。注意鱼的形状通常是长条形的用矩形做包围盒会比实际鱼身大一圈如果直接按矩形判定会出现子弹打中空气但鱼被捕获的情况。所以精检测阶段建议用圆形或更小的内嵌矩形来缩小判定范围。2.4 捕获概率设计让游戏真正“好玩”的关键我说个很多人会忽略的细节捕鱼达人真正的游戏性不在碰撞检测而在捕获概率的设计上。如果子弹碰到鱼就100%捕获那这个游戏玩起来就很乏味因为只要你瞄准了什么鱼都能打下来。常见的设计思路是子弹命中鱼后系统根据鱼的等级生成一个捕获概率。小鱼概率高大鱼概率低同时配合炮弹等级来影响概率。具体公式可以简单设计成捕获概率 基础捕获率 * (炮弹等级系数 / 鱼的等级)比如小丑鱼的基础捕获率是90%金鲨的基础捕获率是20%你的炮弹等级如果提升一级捕获率就乘以1.1。这个概率判断放在碰撞检测命中之后if (CollisionManager.checkHit(fish, bullet)) { double probability bullet.getLevel() * FishType.getBaseRate(fish.getType()); if (Math.random() probability) { catchFish(fish, bullet); } }从实现角度来说这只是一行随机数判断但从游戏设计角度来说这决定了你整个游戏的平衡性和耐玩度。我建议参数调优时不要只靠感觉把概率做成可以动态调整的配置多测几轮再固化。3. 核心玩法模块的源码实现理论讲得差不多了接下来咱们看真正能跑的代码。这一部分我会把核心模块的完整实现思路和关键代码写出来你先别复制粘贴跟着思路走一遍理解了再自己写。3.1 鱼类设计属性、移动与回收机制Fish这个类承载了所有鱼相关的数据和逻辑。属性包括鱼的类型、当前位置坐标、移动速度和移动方向、图片、宽度高度等。为了让鱼看起来自然不同鱼种的速度应该不同小鱼游得快大鱼游得慢。我定义了FishType这个枚举来统一管理鱼的类型参数public enum FishType { MINNOW(0, 30, 1, 1.0), // 小丑鱼10分速度快 YELLOW_FISH(1, 50, 2, 0.8), // 小黄鱼20分 SHARK(2, 100, 5, 0.4), // 蓝鲨100分速度慢 GOLDFISH(3, 200, 8, 0.5), // 金鱼200分 WHALE(4, 300, 15, 0.3); // 大鲸鱼300分极慢 private final int typeId; private final int score; private final int probabilityWeight; private final double speedFactor; // 构造方法和getter省略 }枚举里每个鱼种都配置了分数、出现权重和速度系数。出现权重决定了鱼在生成时被选中的概率权重高的出现更频繁这样游戏难度曲线是平滑的——你大部分时间打的都是小鱼偶尔碰到大鱼会更有惊喜感。鱼移动的逻辑就写在update方法里public void update() { x speed * directionX; y speed * directionY; }方向向量在生成鱼时随机赋予。为了让鱼游动得自然一点我加了轻微的y轴波动让鱼在游动时不是一条直线而是有上下起伏的效果。当鱼完全游出屏幕后就把它的状态标记为“已移除”由FishManager统一清理避免内存中积累一堆看不见的鱼对象。3.2 子弹系统鼠标瞄准、发射与生命周期捕鱼达人里炮台的瞄准方式很简单——鼠标指向哪里炮口就朝向哪里点击时发射一枚子弹。鼠标事件的监听直接写在GamePanel里addMouseListener(new MouseAdapter() { Override public void mouseClicked(MouseEvent e) { if (gameState GameState.RUNNING) { Turret.fire(e.getX(), e.getY()); } } }); addMouseMotionListener(new MouseMotionAdapter() { Override public void mouseMoved(MouseEvent e) { turret.setAngleByTarget(e.getX(), e.getY()); } });炮台角度计算用Math.atan2方法这个方法能根据鼠标坐标和炮塔坐标的差值算出角度非常方便public void setAngleByTarget(int targetX, int targetY) { double dy targetY - turretCenterY; double dx targetX - turretCenterX; angle Math.atan2(dy, dx); }子弹发射后会沿着炮台指向的角度匀速飞行。每一帧更新子弹的位置public void update() { x Math.cos(angle) * speed; y Math.sin(angle) * speed; }子弹也有生命周期。如果子弹飞行了足够长的距离还没有碰撞到任何鱼就应该让它消失。否则子弹会无限飞浪费内存和性能。这个逻辑通过检查子弹X、Y坐标是否超出屏幕范围实现。3.3 碰撞检测与捕获判定逻辑碰撞检测的思路前面已经说过了关键是把检测逻辑封装在独立的CollisionManager里而不是散落在各个类中这样方便统一维护。我当时的实现逻辑分三步第一步遍历当前所有存活子弹判断子弹是否与某条鱼相交。第二步先用矩形包围盒粗检测再用圆形精检测。因为鱼比较多的时候直接对所有鱼做圆形距离计算会有一定成本先用Rectangle的intersects方法过滤掉大部分不相交的样本能省很多计算。第三步命中后按概率决定是否捕获。捕获成功则给玩家加分移除鱼和子弹捕获失败只移除子弹鱼继续游走。public static boolean checkHit(Fish fish, Bullet bullet) { Rectangle fishRect fish.getBounds(); Rectangle bulletRect bullet.getBounds(); if (!fishRect.intersects(bulletRect)) { return false; } // 精检测以中心和半径做圆碰撞 double dx fish.getCenterX() - bullet.getCenterX(); double dy fish.getCenterY() - bullet.getCenterY(); double distance Math.sqrt(dx * dx dy * dy); return distance fish.getRadius() bullet.getRadius(); }这套逻辑看起来简单但在实际运行时会发现一个新问题子弹飞行速度很快可能某一帧子弹还在鱼的左侧下一帧就直接穿到了鱼的右侧但两帧之间鱼和子弹的位置差让它们永远没有“相交”的那一刻。这就是经典的“子弹穿透问题”。解决方法是做“连续碰撞检测”把子弹在本帧的移动路径当作一条线段检测这条线段是否与鱼的包围盒相交。在Swing这种简单场景里可以在子弹位置更新之前记录旧位置然后判断“旧位置到新位置”这条线段与鱼的矩形区域是否有交点。如果不需要这么精细另一个简单粗暴的办法是把子弹的速度控制在一个合理范围子弹每帧移动的距离小于鱼的包围盒尺寸基本也能避免穿透。3.4 计分系统与游戏状态管理计分这块用Swing并不复杂。在GamePanel顶部用JLabel展示当前金币数和炮弹等级每次捕获鱼后更新文本内容。但游戏状态管理这部分值得多说一句。捕鱼达人不是一个“单局制”的游戏但为了代码完整我还是给它加了状态机READY准备中、RUNNING运行中、PAUSED暂停、GAMEOVER结束。我用一个枚举来管理游戏状态public enum GameState { READY, RUNNING, PAUSED, GAMEOVER }主循环每次更新和绘制之前都先检查当前游戏状态不同状态走不同的逻辑分支。这样整个游戏的生命周期是可控的不会出现“暂停了鱼还在游”这种问题。金币扣除的逻辑是每发射一颗子弹就从玩家金币里扣除炮弹等级对应的金币数。如果金币不够不允许发射。捕获成功的鱼的分值由鱼类型和炮弹等级共同决定炮弹等级越高分数倍率越高——这也和街机厅的原版设计保持一致。4. 常见问题与排查技巧实录这部分可能是最有价值的内容了。我在实际写这个项目的过程中踩了不少坑这些问题如果你提前知道解决方案能省下大量调试时间。4.1 画面闪烁鱼在屏幕上“闪来闪去”这是我遇到的第一个问题。一开始我用的是线程循环加repaint()的方式刷新界面画面抖动得非常厉害。后来排查发现问题出在绘制逻辑上——在paintComponent里我用的是直接绘制背景清理和图像绘制之间有延迟人眼能感知到。解决方法简单粗暴要么确保JPanel的双缓冲生效要么在绘制之前调用super.paintComponent(g)来清屏。实际上Swing默认双缓冲已经开着但我人为覆盖paintComponent时忘了调用super的方法导致背景没有正确清除画面当然会花。经验教训重写paintComponent时第一行代码务必调用super.paintComponent(g)这不仅仅是惯例它会触发布局管理器更新和清屏操作。4.2 鱼和子弹都动得“一卡一卡”的卡顿的原因通常是主循环里做了太重的计算。比如每次生成新鱼时都重新加载一次图片对象磁盘I/O累积多了就会拖慢整个刷新频率。优化思路是做一个图片缓存工具类public class ImageUtil { private static final MapString, Image IMAGE_CACHE new HashMap(); public static Image load(String path) { return IMAGE_CACHE.computeIfAbsent(path, p - { try { return ImageIO.read(ImageUtil.class.getResource(p)); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(图片加载失败: p); } }); } }用这个工具类统一加载和管理图片资源所有鱼和子弹的图片都从缓存中获取避免每次new ImageIcon时重复加载文件。4.3 图片路径找不到运行时报文件不存在这个问题很典型尤其当你用IDE直接运行和打包成Jar之后行为还不一样。很多人直接用相对路径“images/fish1.png”本地能跑但打成Jar包之后路径就失效了。标准做法是使用类加载器来获取资源路径URL imageUrl getClass().getClassLoader().getResource(images/fish1.png); Image image ImageIO.read(imageUrl);关键是确保图片文件放在了src/main/resources目录下。如果你用的是普通Java项目要确保构建路径中包含了图片目录。4.4 暂停之后重新开始鱼和子弹都“穿越”了这个问题出在线程逻辑没有真正暂停只是停止了更新和重绘。我一开始用线程循环暂停时只让线程sleep一段时间没有真正暂停状态更新导致恢复后鱼一下子跳到了很远的位置。用了Swing Timer之后这个问题迎刃而解。因为定时器本身就可以被stop和start控制暂停时调用timer.stop()恢复时调用timer.start()鱼和子弹的位置就从暂停时的状态继续推进不会有穿越问题。4.5 内存占用持续增长游戏跑久了变卡这个问题的元凶是鱼生成后没有及时回收。当时我用一个ArrayList存放所有鱼对象鱼游出屏幕后我只是把它的可见性设为false但没从list里移除。时间一长list里积累了成千上万条“消失但没移除”的鱼遍历所有鱼做碰撞检测时性能自然就崩了。解决方法是维护一个“待移除队列”把已经游出屏幕或者被捕获的鱼统一标记然后在主循环的更新阶段统一移除。其实更高效的做法是用对象池来复用鱼对象但对于这个体量的项目ArrayList 及时移除已经足够。总结一下我这个项目的最终实现效果我最后完成的项目支持炮台旋转瞄准、四种子弹等级、十种鱼、多线程鱼群生成、概率捕获判定、计分与暂停/继续功能。主循环稳定运行在60帧运行一小时内存占用没有持续增长。我个人的体会是这个项目的核心难点不在于某个单一的技术点而在于如何把它们串起来让整个游戏处于“均衡运行”的状态。鱼生成频率多少合适子弹速度多快合适捕获概率怎么调才让人既觉得爽又不会腻——这些都是用调试和时间慢慢磨出来的。如果你是Java初学者可以只实现最基础的功能先跑通流程如果你已经有一定基础不妨在这个基础上做个局域网联机版或者加一些特殊道具这些扩展方向我在实际开发过程中也都会顺手实验一下每一个小改进都会让游戏变得更有趣。本文还有配套的精品资源点击获取
返回列表