Java捕鱼达人游戏源码解析:管理器模式与Swing线程实战
发布时间:2026/10/9 2:10:27 锦皓数字建站

简介这是一份面向Java初学者与游戏开发爱好者的捕鱼达人小游戏完整实现工程适合用于课程设计、毕业设计参考或Java图形界面与多线程编程的练手项目。资源以zip压缩包形式提供共223个文件、约10.49MB其中包含60个java源码与93个class编译文件另有png、jpg图片素材、plist与xml配置、ogg与mp3音效以及ttf字体等覆盖游戏运行所需的代码、美术与音频资源。从内容结构看工程按模块划分了鱼类管理、炮台管理、鱼群生成、计分与布局等核心类并配有独立的运行线程便于读者理解捕鱼达人的整体架构与对象协作方式。目前已有177人学习下载。通过研读源码读者可掌握鱼类生成与移动、武器切换、道具系统、关卡难度递增等常见休闲游戏机制的实现思路并在此基础上自行扩展玩法或替换素材是一份结构完整、可直接导入IDE运行的实战参考工程。1. 从一份 class 文件清单说起这套 Java 捕鱼达人到底能不能跑翻到这份资源的时候我第一反应不是看它有多少行代码而是盯着那串文件名看了半天CannonManager.class、FishManager.class、ShoalManager.class、FishRunThread.class、MainSurface.class、LayoutManager.class外加ImageManager和ScoreManager。这是一套典型的 Java SE 桌面小游戏结构用 Swing/AWT 做渲染用线程驱动鱼群游动用管理器类拆分炮台、鱼群、分数、布局。它解决的不是教你写游戏引擎这种大问题而是给了一个能直接编译、能改参数、能当课程设计交差的完整小游戏骨架。适合谁Java 基础刚过、想做点看得见摸得着的东西练手的人也适合带课的老师拿它当面向对象拆分的案例。下面我按结构怎么读 → 怎么跑起来 → 参数怎么调 → 哪里会翻车的顺序把这份资源拆开讲。2. 管理器模式拆解八个类各自管什么2.1 从类名反推职责边界拿到一份只有 class 文件或者半成品源码的 Java 项目最忌讳上来就找 main 方法硬跑。我一般先按类名做职责归类这套资源的命名其实相当规整基本遵循了一个 Manager 管一类实体的思路类名推测职责依赖关系MainSurface游戏主窗口/主面板继承 JPanel 或 JFrame持有各 ManagerLayoutManager界面布局、按钮与菜单摆放被 MainSurface 调用ImageManager图片资源加载与缓存被 Fish/Cannon 调用FishManager单条鱼的状态与绘制被 ShoalManager 管理ShoalManager鱼群生成、批量移动、碰撞检测依赖 FishManagerCannonManager炮台角度、子弹发射、武器切换依赖 ImageManagerScoreManager分数累计、关卡目标判定被 MainSurface 读取FishRunThread鱼群移动的独立线程驱动 ShoalManager这张表不是让你背而是让你在改代码时知道该动哪个文件。比如鱼游得太快你该去FishRunThread里找Thread.sleep()的间隔而不是去FishManager改坐标增量。常见做法是线程里控制刷新频率实体类里控制单帧位移两者分开调互不干扰。2.2 线程与渲染的分离逻辑FishRunThread这个类的存在说明作者没有把鱼的移动塞进paint()里这是对的。Swing 的paint()会被系统频繁调用如果在里面做坐标计算和碰撞检测帧率一高就卡。正确姿势是线程负责改数据鱼的 x/y 坐标paint()只负责把当前数据画出来。// FishRunThread 的典型骨架我按常见写法补全 public class FishRunThread extends Thread { private ShoalManager shoalManager; private boolean running true; public FishRunThread(ShoalManager shoalManager) { this.shoalManager shoalManager; } Override public void run() { while (running) { // 1. 更新每条鱼的坐标 shoalManager.updateFishPositions(); // 2. 检测是否游出边界出界则回收 shoalManager.removeOutOfBounds(); // 3. 控制刷新节奏30ms 约等于 33 帧 try { Thread.sleep(30); } catch (InterruptedException e) { running false; } } } public void stopRunning() { this.running false; } }逻辑说明updateFishPositions()里遍历鱼群按每条鱼自己的速度向量累加坐标removeOutOfBounds()把游出屏幕的鱼从集合里删掉避免内存越堆越大。参数说明Thread.sleep(30)这个数字直接决定游戏手感调到 15 会明显更流畅但 CPU 占用上升调到 50 会有肉眼可见的拖影。我一般先在 30 附近试再根据机器性能微调。注意线程退出要用标志位而不是stop()后者在 Java 里已经废弃强杀线程会导致资源没释放。3. 把项目跑起来环境、编译与资源路径3.1 JDK 版本与编译命令这套代码用的是 Swing/AWT属于 Java SE 的基础库不需要额外依赖。JDK 8 到 JDK 17 都能编但如果你机器上装了多个 JDKjavac和java版本不一致会报UnsupportedClassVersionError。先确认# 查看当前 javac 和 java 版本是否一致 javac -version java -version # 如果项目有 src 目录编译到 out 目录 javac -encoding UTF-8 -d out src/*.java # 运行注意 -cp 指向 class 输出目录 java -cp out MainSurface参数说明-encoding UTF-8很关键Windows 默认 GBK源码里有中文注释或字符串就会乱码甚至编译失败。-d out把 class 文件统一输出避免和源码混在一起。运行时的-cp是 classpath指向编译产物目录不是源码目录这点新手经常搞反。3.2 图片资源的加载路径ImageManager大概率用ImageIO.read()或Toolkit.getImage()加载图片。这里有个血泪经验用相对路径new File(images/fish.png)时工作目录取决于你在哪敲的java命令换个目录就找不到图。稳妥做法是把图片放进resources目录用类加载器读// ImageManager 里推荐的资源加载方式 public class ImageManager { public static Image load(String name) { // 从 classpath 根目录下的 images 文件夹读取 URL url ImageManager.class.getClassLoader() .getResource(images/ name); if (url null) { throw new RuntimeException(图片缺失: name); } return new ImageIcon(url).getImage(); } }逻辑说明getClassLoader().getResource()从 classpath 根开始找只要编译时把images目录一起放进out运行时就不会因为工作目录变化而丢图。参数说明name传的是相对images/的文件名比如fish01.png。如果返回 null先检查out/images/下有没有这个文件再看文件名大小写——Linux 区分大小写Windows 不区分跨平台时容易翻车。4. 参数调优鱼群速度、炮台角度与分数曲线4.1 鱼群速度与生成频率游戏好不好玩八成看参数。ShoalManager里通常有两个核心参数生成间隔和鱼的基础速度。生成太快屏幕全是鱼碰撞检测压力大生成太慢玩家干等体验差。// ShoalManager 中的参数区按常见实现补全 public class ShoalManager { // 每隔多少毫秒生成一批鱼 private int spawnInterval 1200; // 鱼的基础速度实际速度 baseSpeed * 鱼的speedFactor private int baseSpeed 2; // 屏幕上同时存在的鱼上限 private int maxFishCount 25; public void spawnFish() { if (fishList.size() maxFishCount) { return; // 达到上限就不再生成防止卡顿 } Fish fish new Fish(); fish.setSpeed(baseSpeed * fish.getSpeedFactor()); fishList.add(fish); } }逻辑说明maxFishCount是性能保护阀没有它的话玩家挂机十分钟屏幕能堆几百条鱼paint()每帧遍历一次直接卡死。参数说明spawnInterval建议在 800 到 1500 之间调baseSpeed在 1 到 4 之间调。想让游戏越玩越难就在ScoreManager里根据分数动态降低spawnInterval比如每涨 100 分减 50 毫秒但别低于 400否则鱼会叠在一起。4.2 炮台角度与子弹轨迹CannonManager管的是炮台旋转和子弹发射。炮台角度一般用鼠标位置反算子弹沿角度方向做匀速直线运动。// CannonManager 中根据鼠标计算角度并发射 public void aimAt(int mouseX, int mouseY) { // 炮台固定在底部中央 int cannonX 400, cannonY 550; // atan2 返回弧度转成角度 double angle Math.atan2(mouseY - cannonY, mouseX - cannonX); this.angle Math.toDegrees(angle); } public Bullet fire() { double rad Math.toRadians(angle); // 子弹速度分量 int vx (int) (bulletSpeed * Math.cos(rad)); int vy (int) (bulletSpeed * Math.sin(rad)); return new Bullet(cannonX, cannonY, vx, vy); }逻辑说明Math.atan2(dy, dx)比atan(dy/dx)好在能正确处理四个象限不会在左右方向出现角度跳变。参数说明bulletSpeed一般设 8 到 15太快看不清轨迹太慢打不中移动的鱼。注意cannonX/cannonY要和LayoutManager里炮台的实际绘制位置一致否则会出现子弹从炮口旁边飞出去的玄学现象——我见过有人调了半天以为是碰撞算法错了结果是坐标没对齐。5. 避坑与排查编译、线程、碰撞三类高频问题5.1 编译报错找不到符号现象javac报cannot find symbol指向某个 Manager 类。原因源码文件不在同一个包下或者编译时没把所有.java一起传进去。解决确认所有类在同一个包或正确 import用javac -d out src/*.java一次性编译别一个一个编。5.2 游戏窗口一闪而过现象运行后窗口出现又立刻消失。原因main方法里创建了窗口但没有保持事件分发线程存活或者setDefaultCloseOperation没设对。解决确保用SwingUtilities.invokeLater()启动界面并且JFrame设置了EXIT_ON_CLOSE。public static void main(String[] args) { SwingUtilities.invokeLater(() - { JFrame frame new JFrame(捕鱼达人); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.add(new MainSurface()); frame.setSize(800, 600); frame.setVisible(true); }); }5.3 鱼群移动卡顿或撕裂现象鱼移动一顿一顿或者画面闪烁。原因在paint()里直接改数据或者没开双缓冲。解决数据更新全部放线程里paint()只读不写Swing 的JPanel默认支持双缓冲如果自己重写了paint且用了getGraphics()改成用传入的Graphics对象。5.4 碰撞检测打不中鱼现象明明瞄准了却判定不中。原因碰撞用的是鱼的图片矩形但图片有透明边距实际鱼身比矩形小。解决给每条鱼维护一个比图片略小的碰撞框或者用圆形距离判定代替矩形判定。5.5 分数不更新或重复加分现象打中一条鱼加了好几次分。原因子弹和鱼的碰撞检测在每帧都跑同一条鱼被同一颗子弹连续命中。解决命中后立即把鱼标记为dead并从集合移除子弹也标记为失效下一帧不再参与检测。6. 进阶玩法用配置化参数把改代码变成改文件玩到这一步你会发现每次调鱼速、调分数都要重新编译很烦。我的习惯是把这些参数抽到一个game.properties里启动时读一次改完重启就生效不用碰 Java 代码。// ConfigLoader从 classpath 读取配置 public class ConfigLoader { private static Properties props new Properties(); static { try (InputStream in ConfigLoader.class .getClassLoader().getResourceAsStream(game.properties)) { props.load(in); } catch (Exception e) { e.printStackTrace(); } } public static int getInt(String key, int def) { return Integer.parseInt(props.getProperty(key, String.valueOf(def))); } }配套的game.properties长这样fish.baseSpeed2 fish.spawnInterval1200 fish.maxCount25 bullet.speed10 score.perFish10逻辑说明static块保证配置只加载一次getInt带默认值配置文件缺失也不会崩。参数说明score.perFish是每条鱼的基础分稀有鱼可以在Fish类里用scoreFactor乘上去。这样调平衡时只改文本文件策划和程序不用互相等。验证方法改完game.properties后重新运行打中一条普通鱼看分数是不是按新值加。如果没变先确认文件在out目录下编译时要把.properties一起复制过去再看 key 名有没有拼错——Properties读不到 key 不会报错只会返回默认值这个黑匣子特性坑过不少人。从那以后我每次拿到这种管理器模式的小游戏都先做一件事把所有硬编码的数字抽成配置再开始调手感。这样后面无论怎么改都不用担心改坏逻辑。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。