ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java AWT/Swing 实现捕鱼达人游戏核心逻辑

Java AWT/Swing 实现捕鱼达人游戏核心逻辑 简介这是一份基于Java开发的《捕鱼达人》游戏完整源码实现面向Java初学者与游戏开发入门者帮助理解面向对象设计、Swing图形界面、多线程动画及碰撞检测等核心编程实践。资源包含333个文件主体为284张PNG鱼帧图支撑逐帧动画辅以5个核心Java类如Fish、Pool、Net、6个编译后class文件、7个XML配置及若干工程元数据文件整体压缩包仅4.15MB轻量易解压学习。已有2980人学习下载体现了较强的实践参考价值。读者可直接运行项目观察鱼群并发移动、渔网捕获判定、视觉暂留式摆尾动画等关键机制代码结构清晰含详细注释与模块化设计如鱼池继承JPanel、鱼对象独立线程便于拆解学习类关系建模、循环队列索引控制Index取余及矩形重叠碰撞算法是掌握Java GUI游戏开发全流程的优质教学范例。1. 这不是玩具代码用纯 Java AWT/Swing 实现《捕鱼达人》核心逻辑不依赖任何游戏引擎适合 Java 初学者练手、面试项目复现与 OOP 教学拆解你可能见过太多“Java 实现贪吃蛇”“Java 扫雷”的教程——它们结构简单但离真实游戏逻辑太远。而这份《捕鱼达人》源码是少有的、能在 JDK 8 环境下零依赖直接运行的完整桌面游戏工程它没有用 LibGDX、LWJGL 或 JavaFX而是扎扎实实用 AWT Swing 多线程 双缓冲绘图实现了鱼群动态生成、渔网碰撞检测、帧动画播放、多线程异步移动、得分实时渲染等关键模块。它不是 Demo而是可交互、可调试、可扩展的“最小可行游戏系统”——我去年带实习生做课程设计时就用它讲透了“如何把‘鱼在游’这个需求一步步翻译成类关系、线程调度和像素级重绘”。如果你正卡在 Java 面向对象建模、Swing 事件循环机制、或“多线程更新 UI 为什么总报错”上这份源码就是你的实体教具。它不炫技但每行都在回答一个经典问题当业务语言“鱼要摆尾”“网要罩住鱼”遇上编程语言repaint()、Thread.sleep()、Rectangle.intersects()中间那座桥是怎么搭起来的。2. 从“找对象”到“画鱼”业务建模 → 类设计 → 帧动画实现的三阶落地路径2.1 业务对象建模为什么 Fish 和 Net 必须是独立类而 Pool 是容器而非父类很多初学者一上来就想“继承”结果把Fish和Net都塞进GamePanel里硬编码。但这份源码的建模逻辑非常清晰Fish代表单条鱼的生命周期位置、速度、图片序列、当前帧索引、是否被捕获Net代表玩家操作的渔网坐标、半径、激活状态、捕获判定范围Pool不是“鱼的父类”而是承载所有鱼实例 渔网 游戏主循环的 JPanel 容器它持有ListFish和Net引用并负责调用paintComponent()绘制全部元素。这种设计规避了“上帝类”陷阱。比如当你要加“Boss 鱼”时只需新建FishLord extends Fish重写move()和getImages()而Pool完全不用改——因为它的for (Fish f : fishes)循环天然兼容子类。这也是为什么FishlordFrame.class和FishlordGameLauncher.class能作为独立入口存在它们只是换了一套图片资源和初始参数核心逻辑复用率 100%。提示config目录下description文件虽小但定义了每种鱼的step移动步长、imageCount帧数、score分值这是解耦配置与代码的关键证据——别把它当成注释它是运行时加载的规则表。2.2 帧动画实现用Image[]index % length模拟视觉暂留比 Timer 更可控动画效果不靠javax.swing.Timer而是由每个Fish自己控制帧切换节奏// Fish.java 关键片段 private int index 0; // 当前帧索引 private Image[] images; // 预加载的帧图数组如 fish_0.png, fish_1.png... private final int frameDelay 150; // 每帧停留毫秒数可按鱼种类差异化 public void updateFrame() { index (index 1) % images.length; // 循环队列式取余防越界 } public Image getCurrentImage() { return images[index]; }Pool.paintComponent(Graphics g)中对每条鱼调用fish.getCurrentImage()获取当前帧再g.drawImage(...)绘制。这种“数据驱动动画”模式让不同鱼能拥有不同帧率金枪鱼快、乌龟慢且无需全局 Timer 干扰线程调度——因为Fish的run()方法里已内置Thread.sleep(frameDelay)。2.3 碰撞检测用Rectangle.intersects()替代像素级检测兼顾性能与精度捕鱼判定不是“渔网中心点是否在鱼体内”而是矩形包围盒重叠检测// Net.java 中的判定逻辑 public boolean isCapturing(Fish fish) { Rectangle netRect new Rectangle(x - radius, y - radius, radius * 2, radius * 2); Rectangle fishRect new Rectangle(fish.getX(), fish.getY(), fish.getWidth(), fish.getHeight()); return netRect.intersects(fishRect); }注意fish.getWidth()/getHeight()不是图片原始尺寸而是fish.getImage().getWidth(null)动态获取——因为不同鱼图片大小不同。这个细节常被忽略导致小鱼永远捕不到矩形太小或大鱼误捕矩形太大。源码中Fish构造时会缓存width/height避免每次paintComponent都重复调用getImage().getWidth(null)这是 Swing 性能优化的血泪经验。3. 多线程协同Fish 独立线程移动 vs Pool 主线程重绘的锁与同步策略3.1 为什么每条鱼都继承 Thread—— 解耦运动逻辑与 UI 刷新Fish extends Thread是本项目最易被误解的设计。有人觉得“用 Runnable 更规范”但这里Thread是刻意为之每条鱼有自己的sleep()周期step决定移动频率frameDelay决定动画节奏Fish.run()中包含完整的move() → updateFrame() → checkBoundary()闭环不依赖外部调度Pool只需在startGame()中遍历fishes并调用fish.start()后续完全自治。这模拟了真实游戏中的“实体独立行为体”思想。若改用ExecutorService统一管理反而要为每条鱼定制ScheduledFuture增加复杂度。3.2 双缓冲绘图避免 Swing 重绘闪烁的核心实践Pool重写paintComponent()时必须启用双缓冲// Pool.java Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 创建双缓冲图像 if (offscreenImage null || offscreenImage.getWidth(null) ! getWidth() || offscreenImage.getHeight(null) ! getHeight()) { offscreenImage createImage(getWidth(), getHeight()); } Graphics2D g2d (Graphics2D) offscreenImage.getGraphics(); // 在 offscreenImage 上绘制所有元素 drawBackground(g2d); for (Fish fish : fishes) { g2d.drawImage(fish.getCurrentImage(), fish.getX(), fish.getY(), null); } g2d.drawImage(net.getImage(), net.getX(), net.getY(), null); drawScore(g2d); // 一次性刷到屏幕 g.drawImage(offscreenImage, 0, 0, null); g2d.dispose(); }关键点offscreenImage是Pool的成员变量不是每次paintComponent都新建g2d.dispose()必须调用否则内存泄漏super.paintComponent(g)不能省它负责清除脏区域。3.3 线程安全边界哪些变量需要 synchronized哪些根本不用必须加锁fishes列表Pool.addFish()和Fish.run()中的remove()可能并发无需加锁Fish.x/y每条鱼只被自己线程修改Pool读取时是“最终一致性”——即使读到旧值下一帧就更新了特殊处理Net的x/y由鼠标监听器修改Pool的mouseDragged()中直接赋值不加锁因为 Swing 事件处理在 EDT 线程而paintComponent也在 EDT不存在并发写。注意exclude .gitignore文件存在说明作者已预判多人协作场景——.class文件和config/description不应纳入版本控制但src/下的.java必须全量提交。这是 Java 工程规范的隐性提示。4. 避坑指南6 个真实踩过的坑覆盖编译、运行、动画、碰撞四大高频翻车点4.1 编译报错 “cannot find symbol class FishlordFrame” —— 源码包结构未还原现象javac *.java报错找不到FishlordFrame但文件明明存在原因FishlordFrame.java位于com/example/fishgame/包下而你直接在源码根目录执行javacJVM 无法解析包路径解决# 正确做法进入 src 目录假设源码结构为 src/com/example/fishgame/*.java cd src javac com/example/fishgame/*.java java com.example.fishgame.FishlordGameLauncher4.2 游戏启动后黑屏控制台无报错 —— 图片资源路径错误或未打包现象窗口弹出但背景全黑System.out.println(Loading image: ...)显示图片加载失败原因ImageIO.read(getClass().getResource(/images/fish_0.png))中/images/是相对于 classpath 的路径但你把图片放在./images/当前目录而非src/images/解决将images文件夹复制到src目录下确保编译后images/出现在classes/根目录或改用绝对路径new File(images/fish_0.png)仅限本地调试。4.3 鱼群静止不动CPU 占用 100% ——Thread.sleep()被异常吞没现象鱼不移动top显示 Java 进程 CPU 满载原因Fish.run()中try { Thread.sleep(...) } catch (InterruptedException e) { }空 catch线程被中断后立即进入死循环解决在catch块中添加Thread.currentThread().interrupt();并break退出循环或return结束线程。4.4 渔网拖拽卡顿鼠标轨迹断续 ——mouseDragged()中执行耗时操作现象鼠标快速拖动时渔网位置跳跃不跟随原因mouseDragged()里调用了repaint()而paintComponent()中含大量drawImage()导致 EDT 被阻塞解决mouseDragged()只更新net.x/net.yrepaint()放在mouseReleased()后批量触发或改用SwingWorker异步加载图片本项目未用但可升级。4.5 捕鱼成功但分数不增加 ——Net.isCapturing()返回 true 后未及时移除鱼现象渔网罩住鱼鱼消失但score变量未变原因Pool.checkCapture()中调用net.isCapturing(fish)后只执行fish.setCaptured(true)但paintComponent()仍绘制该鱼且score fish.getScore()未执行解决在checkCapture()中if (net.isCapturing(fish)) { score fish.getScore(); fishes.remove(fish); }并确保fishes是CopyOnWriteArrayList或加synchronized块。4.6 多次启动后内存溢出 ——Fish线程未正确终止现象反复点击“开始游戏”程序越来越慢最终OutOfMemoryError原因Fish.run()是无限循环while (alive) { ... }但alive字段未被Pool.stopGame()设为false旧线程持续运行解决Fish类添加volatile boolean alive true;Pool.stopGame()中遍历fishes并调用fish.alive false;Fish.run()循环条件改为while (alive)。5. 参数化改造从硬编码到可配置用 properties 文件驱动鱼行为与游戏难度5.1 将Fish的step、frameDelay、score提取为外部配置源码中config/description是文本文件但实际应改为fish.properties# fish.properties goldfish.step3 goldfish.frameDelay200 goldfish.score10 goldfish.imagesfish_gold_0.png,fish_gold_1.png,fish_gold_2.png shark.step8 shark.frameDelay100 shark.score50 shark.imagesfish_shark_0.png,fish_shark_1.png加载逻辑封装为工具类// FishConfigLoader.java public static Fish createFish(String type) { Properties props new Properties(); try (InputStream is FishConfigLoader.class.getResourceAsStream(/config/fish.properties)) { props.load(is); } catch (IOException e) { throw new RuntimeException(Failed to load fish config, e); } String stepStr props.getProperty(type .step, 5); String frameStr props.getProperty(type .frameDelay, 150); String scoreStr props.getProperty(type .score, 1); String imagesStr props.getProperty(type .images, ); Fish fish new Fish(); fish.setStep(Integer.parseInt(stepStr)); fish.setFrameDelay(Integer.parseInt(frameStr)); fish.setScore(Integer.parseInt(scoreStr)); fish.setImages(loadImages(imagesStr.split(,))); return fish; }5.2 游戏难度动态调节通过config/game.properties控制鱼群密度与刷新间隔# game.properties initialFishCount12 maxFishCount30 spawnIntervalMs3000 backgroundMusicEnabledtruePool初始化时读取private void loadGameConfig() { Properties gameProps new Properties(); try (InputStream is getClass().getResourceAsStream(/config/game.properties)) { gameProps.load(is); this.initialFishCount Integer.parseInt(gameProps.getProperty(initialFishCount, 10)); this.spawnIntervalMs Long.parseLong(gameProps.getProperty(spawnIntervalMs, 5000)); } catch (Exception e) { System.err.println(Using default game config); } }这样无需改 Java 代码仅调整 properties 文件就能测试不同难度档位——这对课程设计答辩或面试演示极其重要你能当场展示“调高 spawnIntervalMs 让鱼变少降低难度”。5.3 图片资源自动加载用ClassLoader.getResources()批量扫描 images 目录避免手动维护images数组private Image[] loadImages(String prefix) { ListImage list new ArrayList(); try { URL url getClass().getResource(/images/); File dir new File(url.toURI()); for (File imgFile : dir.listFiles((d, name) - name.startsWith(prefix) name.endsWith(.png))) { list.add(ImageIO.read(imgFile)); } } catch (Exception e) { e.printStackTrace(); } return list.toArray(new Image[0]); }此方法让新增鱼种只需放图、改配置彻底解耦资源与代码。6. 验证与调试技巧用 JVisualVM 实时监控线程、内存与 Swing 绘制性能6.1 用 JVisualVM 定位“鱼不动”问题线程状态与堆栈分析启动游戏后打开jvisualvmJDK 自带连接进程切换到Threads标签页查看Fish-1、Fish-2等线程状态若为TIMED_WAITING说明Thread.sleep()正常若为RUNNABLE且 CPU 占用高说明move()里有死循环点击线程名查看Stack Trace确认是否卡在ImageIO.read()图片路径错、Rectangle.intersects()坐标计算溢出或paintComponent()双缓冲未生效。6.2 用 JConsole 检查内存泄漏重点关注java.awt.image.BufferedImage实例数在MBeans → java.lang → Memory → HeapMemoryUsage中观察若used持续上涨且 GC 后不回落说明offscreenImage或Fish.images未被回收切换到Classes标签搜索BufferedImage若实例数随游戏时间线性增长证明Fish构造时重复ImageIO.read()未缓存或offscreenImage未复用。6.3 Swing 绘制性能压测用System.nanoTime()测量paintComponent耗时在Pool.paintComponent()开头和结尾添加long start System.nanoTime(); // ... 原有绘制逻辑 ... long end System.nanoTime(); System.out.printf(Paint took %.2f ms%n, (end - start) / 1_000_000.0);健康值应 16ms60FPS。若 30ms检查是否offscreenImage频繁重建尺寸变化是否drawImage()调用次数过多每条鱼一次100 条鱼就是 100 次是否启用了抗锯齿g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)会显著降速本项目未启用正确。6.4 碰撞检测精度验证用Rectangle边界框可视化调试临时在paintComponent()中添加// 调试用绘制鱼和网的碰撞框 g2d.setColor(Color.RED); g2d.draw(new Rectangle(fish.getX(), fish.getY(), fish.getWidth(), fish.getHeight())); g2d.setColor(Color.BLUE); g2d.draw(new Rectangle(net.getX() - net.getRadius(), net.getY() - net.getRadius(), net.getRadius() * 2, net.getRadius() * 2));运行后红色框应紧贴鱼身蓝色框应以网为中心——若框偏移说明fish.getX()/getY()或net.getRadius()计算错误。从那以后我每次重构 Swing 游戏都会先开 JVisualVM 看线程状态再打一行System.out.println(paint: System.currentTimeMillis())确认重绘频率最后用drawRect把所有逻辑框画出来。这三步走完90% 的“玄学 bug”当场现形。希望帮到你。本文还有配套的精品资源点击获取
返回列表