手写Java黄金矿工:Swing游戏开发、碰撞检测与性能调优

发布时间:2026/9/28 1:02:12
手写Java黄金矿工:Swing游戏开发、碰撞检测与性能调优 简介Java实现黄金矿工游戏开发资源面向已掌握Java语法、想通过完整项目巩固面向对象与GUI编程的开发者内容涵盖矿工角色建模、钩子投掷回收、金矿与岩石碰撞检测、计分与倒计时机制、动画刷新和存档处理等核心模块。压缩包共30个文件以java源码和class编译文件为主搭配png、gif、jpg等多格式游戏图像资源以及jar可执行程序、iml工程文件、META-INF清单和README说明整体仅240KB轻量且目录清晰。目前已有1187人学习下载。资源内项目结构完整适合在Eclipse或IntelliJ IDEA中导入后对照学习从Swing/Javafx界面构建、事件监听器注册到Thread与Runnable实现并行刷新再到File与ObjectOutputStream完成数据持久化均可在源码中定位对应代码同时异常处理、资源加载方式也能在具体场景中体会。对希望熟悉Java小游戏开发流程、提升代码阅读与实操能力的开发者而言是一份可直接上手的参考项目。1. 手写一个《黄金矿工》到底能练到什么先说结论《Java实现黄金矿工.zip》这个标题说白了就是一份用 Java 写出来的《黄金矿工》小游戏源码。别小看这个“小”字它几乎是 Java 课程设计里最经典的一个题目有窗口界面、有定时器、有物理运动、有碰撞检测规模卡在一个学期项目刚刚好的位置。很多招聘 JD 里写着“熟悉 Java 基础语法、了解 Swing、能独立完成模块开发”这份代码就是把这些话翻译成能跑的东西。如果你是正在准备 Java 面试、做课程设计或者刚开始走 Java 学习路线想找一个既不复杂到劝退、又能讲清楚“对象、线程、事件、碰撞”这些知识点的项目这份源码是真能帮上忙的。它解决的不是“我能不能写出来”的问题而是“我写完之后能不能跟别人讲明白每个类在干嘛、为什么这么设计”。今天这篇就把这个项目的拆解、实现、参数调优和踩坑点一次讲清楚让你拿到手不是只会跑个 jar而是能自己改、自己扩展、能在面试里说出个所以然来。2. 核心设计拆解这个游戏凭什么跑起来2.1 窗口、画布与游戏循环Swing 不是黑匣子整个黄金矿工在技术上并不依赖任何重型框架常见的做法就是用 Java Swing 把窗口和画布搭起来。主窗口一般继承 JFrame游戏区域用一个继承 JPanel 的自定义画布对象来承载然后在这个画布上完成所有绘制逻辑。这个结构本身非常朴素但它是理解整个项目的第一把钥匙。核心的游戏循环通常不直接写在 JFrame 里而是单独拆一个线程。常见做法是让游戏主体实现 Runnable 接口在 run() 方法里写一个 while 循环每一帧做三件事更新游戏状态矿工位置、钩子角度、金块坐标、抓取结果、重绘画布、控制帧率。帧率控制最土但最稳的方式是 Thread.sleep()按 16 毫秒一帧去睡基本能稳住 60 FPS。如果你看到源码里没有写 sleep而是让循环空转那这个项目大概率会有 CPU 占用率高的问题你自己改的时候一定要加。伪代码大概是这个意思public class GamePanel extends JPanel implements Runnable { private Thread gameThread; // 游戏线程 private boolean isRunning; // 循环开关 private int fps 60; public void startGame() { isRunning true; gameThread new Thread(this); gameThread.start(); } Override public void run() { while (isRunning) { updateGameLogic(); // 更新所有游戏对象的状态 repaint(); // 触发 paintComponent 重绘 try { Thread.sleep(1000 / fps); // 帧间隔 } catch (InterruptedException e) { e.printStackTrace(); } } } }这段代码的重点不在语法而在结构。updateGameLogic() 和 repaint() 的分离是关键前者只改数据金块坐标、钩子长度、分数后者只负责根据数据把画面画出来。这个习惯带进实际开发里能避免很多“画面乱跳、状态对不上”的诡异问题。参数方面fps 不要调到 100 以上Swing 的绘制能力有限而且人眼对超过 60 帧的游戏画面升级感知很弱白白浪费 CPU。2.2 钩子的运动模型角度、速度与“抓取判定”黄金矿工最核心的交互就是钩子按方向键左右调整角度按空格释放钩子钩子伸出去碰到金块或者岩石就抓回来。这个机制听起来简单但你去看市面上的代码十个里面有八个把钩子做成“直线移动”往一个方向直直地飞出去抓到东西再直直地飞回来。这样能用但是手感很死板。真正像原版黄金矿工的效果是钩子拥有一个摆动角度类似钟摆在释放之前会来回晃玩家要抓时机。技术实现上其实不复杂就是用一个角度变量 currentAngle 来记录钩子当前摆动的角度每帧在左右边界之间做增量变化。释放钩子后钩子沿当前角度的方向直线伸出同时记录伸出的长度。抓取到金块后钩子缩短金块跟着钩子回到原点。public void updateHookPosition() { if (!hook.isReleased()) { // 未释放做钟摆运动 hook.setAngle(hook.getAngle() swingSpeed * direction); if (hook.getAngle() maxAngle || hook.getAngle() minAngle) { direction -direction; // 边界反弹 } } else if (hook.isExtending()) { // 释放并伸出 hook.setLength(hook.getLength() extendSpeed); // 在这里做碰撞检测 checkHitTarget(); } else if (hook.isRetracting()) { // 回缩抓到了东西带着目标一起回来 hook.setLength(hook.getLength() - retractSpeed); updateAttachedTargetPosition(); if (hook.getLength() 0) { hook.setState(HookState.IDLE); settleScore(); // 结算得分 } } }参数上最需要调的是 swingSpeed 和 extendSpeed。swingSpeed 建议控制在 2~4 度/帧太快玩家看不清角度太慢等得着急extendSpeed 建议 8~12 像素/帧拉伸距离越长速度越快这个设计在源码里也很常见本质上是难度曲线的一种体现。碰撞检测不要做成整个金块外接矩形和钩子端点的矩形相交检测那样容易出现“看着碰到了但就是抓不到”的玄学手感。更合理的做法是把钩子端点简化成一个圆点金块简化成一个圆心加半径的圆用两点距离做判定。2.3 抓取判定到底怎么算从矩形碰撞到圆形距离你们要是在网上看过几个 Java 黄金矿工的源码会发现碰撞检测的写法千奇百怪但质量高下立判。最粗糙的版本是遍历所有金块每个金块用 getBounds() 拿一个矩形然后和钩子端点的矩形做 intersects() 判定。这个做法在游戏初始阶段没问题但一旦加入了不同大小的金块碰撞框和实际图形不一致的问题就暴露了——大金块周围一圈“空气墙”小金块反而不好抓。更贴近原版手感的做法是圆形判定钩子尖端是一个 2~3 像素半径的小圆金块根据图片的实际大小生成一个外接圆半径然后算两个圆心之间的欧几里得距离。public GoldBlock checkCollision(Hook hook, ListGoldBlock blocks) { double hookTipX hook.getTipX(); double hookTipY hook.getTipY(); for (GoldBlock block : blocks) { // 用圆心距离判断比矩形相交更贴合画面上看到的判定范围 double dx hookTipX - block.getCenterX(); double dy hookTipY - block.getCenterY(); double maxDistance block.getCollideRadius() hook.getTipRadius(); if (dx * dx dy * dy maxDistance * maxDistance) { return block; } } return null; }两点间距离公式的判断没有调用 Math.sqrt()而是直接比较平方和。这不是什么高深的优化纯粹是平方开根比较浮点开销更大做游戏循环时每一帧可能调用几十上百次碰撞检测省一点是一点。另外一个细节是碰撞判定只对“当前帧钩子尖端所在点”做检测如果伸出速度太快一帧移动了 20 像素可能直接跨过一个 10 像素半径的小金块出现“穿透”现象。遇到这种问题解决方案不是调慢速度而是做分段移动把一步拆成两步。2.4 对象模型与素材管理类拆得好不好一眼就能看出来能跑和能看是两回事。一个值得写到简历上的 Java 项目类设计至少要能回答“这个游戏有哪些实体”这个问题。最常见的划分是有GamePanel游戏面板控制循环、Hook钩子实体、GoldBlock金块父类、GoldNugget小金块、GoldBag大金块、Stone石头、GameConfig全局配置外加工具类负责图片加载和音效播放。很多初学者会犯一个错误就是所有逻辑塞进一个 GamePanel 类里金块用一个二维数组标记有没有被挖走钩子坐标直接存在面板的成员变量里面。我知道你们嫌类多了麻烦但这么写的问题是后期根本没有办法加新功能——你加一个道具就要在面板里加一堆 if-else加几个新金块类型又要改一堆硬编码坐标。合理的做法是抽象出一个 GameObject 基类把 x、y、width、height、image、collideRadius 这几个所有物体共有的字段放进去。public abstract class GameObject { protected int x; protected int y; protected int width; protected int height; protected Image image; protected int collideRadius; // 判定半径 }然后 GoldBlock 继承 GameObject内部用 int value 记录这块金子的价格。石头可以是 GoldBlock 的一个子类定义 value 为 0draw() 方法里画不同的图片。这套设计的好处在于后续做“钻石”这种高价值低判定半径的特殊物体时只需要新增一个类不需要改动抓取逻辑。给你们的建议是拿到这份源码之后第一件事不是跑起来而是画一张类图把每个类之间的关系列一遍能画清楚说明你真看懂了。3. 从零搭建项目写代码前的目录设计与最小可运行版本3.1 用 Maven 还是不用 Maven课程设计场景下的选择很多课程设计源码包打开以后是一堆 .java 文件加一个 .jbr 文件夹直接命令行 javac 编译也能跑但你要加第三方库比如 JSON 配置读取或者音效库就会很痛苦。我个人的建议是如果只是自己学习或做课程设计交作业不需要引入 Maven纯 Java 项目目录就够用了但如果你想把这个项目变成简历上的作品最好从第一天就用 Maven 结构这一步能体现你对工程化的理解。Maven 目录结构和纯手写的差别主要在于 src/main/java、src/main/resources 这两个标准路径的出现。resources 目录专门放图片、音频和配置文件打包的时候 Maven 会统一处理不会出现“图片找不到”这种在 IDE 里能跑、打包成 jar 就崩的问题。而且现在的主流 IDE 对 Maven 项目支持非常好导入即识别不用手动配 classpath。如果不想用 Maven纯 Java 项目建议按这个结构组织src/ ├── game/ // 游戏核心逻辑 │ ├── GamePanel.java │ ├── GameLauncher.java │ ├── GameObject.java │ ├── Hook.java │ └── GoldBlock.java ├── ui/ // 界面相关 │ └── MainFrame.java └── resources/ ├── images/ // 所有游戏图片 └── config/ └── game.properties我在实际带人做课设的时候发现一个特别能加分的小细节是配置文件单独拎出来。比如钩子的伸出速度、摆动速度、帧率、各类金块的价格全部写进 game.properties代码里只通过一个 Config 类读取。面试官问你“这个参数为什么设成这个值”你可以说是配置的、可以根据难度调整比说“写死的”听起来靠谱得多。3.2 配置类与图片加载解决“换个目录就跑不了”的祖传问题拿到任何一份 Java 游戏源码第一步要检查的就是图片加载方式。常见的翻车写法是用绝对路径类似ImageIcon(C:/Users/xxx/Desktop/gold.png)这种代码在你电脑上能跑换一台机器就崩。正确的姿势是通过类路径加载把图片放进 resources 或 src 目录下用 getResource 去拿。public class ImageLoader { // 通过类路径加载打包成 jar 后依然有效 public static Image loadImage(String path) { java.net.URL imgUrl ImageLoader.class.getResource(path); if (imgUrl null) { throw new RuntimeException(图片资源未找到: path); } return new ImageIcon(imgUrl).getImage(); } }这里有一个隐藏很深的坑如果用了 Maven 但图片放在了 src/main/java 目录下而不是 src/main/resourcesMaven 默认不会把这个目录下的非 .java 文件打进 final jar 包。所以你会遇到 IDE 里运行游戏图片显示正常双击 jar 包后整个窗口变成一片空白的问题。这不是代码逻辑错了是构建配置的问题。解决方式有两个要么把图片全部挪到 resources 目录要么在 pom.xml 的 build 节点里加 resource 声明把图片目录包含进去。我一般直接用第一种简单直接不会忘。3.3 最小可运行版本先让钩子动起来再谈美术和关卡学习这个项目最忌讳的就是一上来就追求完整还原原版。正确路线是做一个“最小可运行版本”只包含五个要素一个能显示背景图的窗口、一个重力作用下做钟摆运动的钩子、一个按下空格后直线伸出的钩子、一个碰到就抓回来的金块、一个计分面板。这五件事做完游戏的核心已经成立剩下的美术、音效、关卡、道具全部是锦上添花。我从操作步骤的层面给你们拆一下照着这个顺序做半天时间就能跑起来。第一步新建 MainFrame 继承 JFrame设置窗口大小和标题默认关闭操作第二步新建 GamePanel 继承 JPanel实现 Runnable 和 KeyListener第三步在 GamePanel 的 paintComponent 里画背景、画钩子第四步实现 run() 循环把帧率控制写上第五步添加键盘监听左右键改角度、空格释放钩子第六步把 GamePanel 添加进 MainFramesetVisible(true)。每一步做完都运行一次避免一口气写完然后疯狂 debug。这个小标题里所有代码的核心骨架在上面的章节里已经覆盖直接照着拼装就能得到一个可交互的钩子系统。拿到完整源码时候我的建议是用 diff 工具把这个最小版本和完整版对比看作者在哪些地方做了增量扩展这是学习代码演进最快的路径。4. 必调参数与避坑记录钩子穿透、键盘失灵和帧率陀螺4.1 钩子穿插金块不抓取分段移动才是后悔药现象钩子伸出去的速度一调快经常会从比较小的金块中间穿过去肉眼看着明显碰到了但代码判定没抓到。原因碰撞检测是“点”对“圆”的判定钩子每帧只计算一个端点位置。当 extendSpeed 超过 15 像素/帧时对小半径物体的判定就失效了——端点上一帧还在圆外下一帧已经跨到圆内并穿到另一侧。和人眼看到的画面相比逻辑上的采样间隔太大出现了“穿透”。解决不要调小 extendSpeed这会牺牲手感。正确做法是把一次长距离移动拆成多次小步长移动每移动一小步做一次碰撞检测。这个方案我一般叫“分段射线”实际实现上就是在原有路径的起点和终点之间插几个采样点。public boolean moveHookWithCollision(double targetLength) { int steps 5; // 把一帧的移动拆成 5 步减少穿透概率 double stepLen (targetLength - hook.getLength()) / steps; for (int i 0; i steps; i) { hook.setLength(hook.getLength() stepLen); if (checkCollision(hook, blocks) ! null) { return true; } } return false; }注意步数不是越多越好拆成 10 步以上碰撞检测的计算量翻倍但对于 Swing 这种低帧率场景来说5 到 8 步已经完全够用。判定追求的不是物理精确是“看起来合理”。4.2 键盘按下没反应焦点丢失与 KeyListener 的边界现象钩子一开始能动但随便点了一下鼠标之后按键就失灵了。这个问题在网上搜 Java swing 键盘监听几乎有一半人遇到过。原因Swing 的键盘事件是发送给当前拥有焦点Focus的组件。如果你点了窗口上的某个按钮或者面板之外的地方焦点从 GamePanel 上移走了KeyListener 自然收不到任何按键。这是 Swing 体系的一个经典黑匣子。解决在 GamePanel 构造器里加一行setFocusable(true);还不够还要在窗口里主动请求焦点。最稳的办法是用 Key Bindings 替代 KeyListener它不需要组件拥有焦点只要组件在窗口内可见且处于 enabled 状态按键事件就会触发。从项目实战的角度我推荐用 Key Bindings 而不是 KeyListener 写游戏能省掉很多和焦点斗智斗勇的时间。InputMap inputMap getInputMap(WHEN_IN_FOCUSED_WINDOW); ActionMap actionMap getActionMap(); // 向左调整角度 inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), swingLeft); actionMap.put(swingLeft, new AbstractAction() { Override public void actionPerformed(ActionEvent e) { hook.setAngle(hook.getAngle() - 2); } });WHEN_IN_FOCUSED_WINDOW 这个窗口级绑定范围很关键它意味着只要当前窗口是激活的按键就有效不再依赖某个具体组件是否拿到焦点。这是 Swing 里真正解决这个问题的最好方法。4.3 游戏帧率时高时低别再往 run() 里塞重逻辑现象帧率忽高忽低尤其是钩子抓到大金块回缩时画面会明显卡顿一下然后又恢复。原因一个最典型的写法错误是在游戏循环里直接做图片加载和缩放。new ImageIcon(path)没做缓存每次调用都去磁盘读文件一张大图读个几十毫秒是常事游戏画面就会突然卡住。另一个常见痛点是在主循环里调用Thread.sleep()试图限帧但循环里其他操作耗时大于 sleep 时间时帧率就完全失控了。解决图片全部在游戏启动时预加载到内存里用一个 Map 缓存后续绘制只从缓存取引用。不要做频繁的Graphics2D.drawImage缩放预先按目标尺寸缩放好存起来。帧率控制至少要写成“固定时间步”的模式而不是简单的 sleep。private double ticksPerSecond 60; private long lastTime System.nanoTime(); private final double nanoPerTick 1000000000.0 / ticksPerSecond; Override public void run() { double delta 0; while (isRunning) { long now System.nanoTime(); delta (now - lastTime) / nanoPerTick; lastTime now; while (delta 1) { updateGameLogic(); // 每秒固定调用 60 次 delta - 1; } repaint(); // 绘制可以稍微放开次数 } }固定时间步的写法在 Java 游戏开发里属于标准方案教程里经常看到但实际写的人反而不多。它对游戏手感的影响是决定性的不管运行设备的性能怎么波动逻辑更新频率是稳定的游戏速度不会一会儿快一会儿慢。4.4 双击 jar 包后白屏或报“找不到主类”现象在 IDE 里运行一切正常但导出成可执行的 jar 包之后双击运行要么弹窗报错要么窗口开出来是白屏什么也不画。原因两层原因。白屏是 4.2 提到过的图片路径问题资源没打包进 jar运行时拿不到图片。找不到主类的报错则是 MANIFEST.MF 里的 Main-Class 配置不对或者用了“导出 jar”功能但没把依赖一起打进去。解决如果你用的是 Maven直接在 pom.xml 里配置 maven-assembly-plugin 或 maven-shade-plugin让构建过程自动生成可执行 jar。Main-Class要写全限定类名比如com.goldminer.GameLauncher不要带 .class 后缀。打包完成后用java -jar xxx.jar在命令行里跑一次控制台会直接打印缺失资源的异常信息比双击黑匣子式的报错要容易定位十倍。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-assembly-plugin/artifactId configuration archive manifest mainClasscom.goldminer.GameLauncher/mainClass /manifest /archive descriptorRefs descriptorRefjar-with-dependencies/descriptorRef /descriptorRefs /configuration /plugin4.5 窗口关闭但进程没退出非守护线程在捣乱现象点窗口右上角的叉界面消失了但 Java 进程还在后台IDE 的控制台也不停。原因游戏线程是用Thread直接创建并start()的。JFrame 关闭时只是销毁了窗口对象这个线程没有收到任何停止信号还在 while 循环里跑。因为它在 JVM 里是非守护线程JVM 不会主动退出。解决把gameThread声明成daemon线程或者在窗口关闭事件里主动终止 isRunning 开关并把线程 join 回来。比较推荐的组合做法是两者都要线程设成 daemon 保底窗口关闭事件里设置 isRunning false让 run() 方法里的循环自然退出线程回到死亡状态JVM 干净地退出。5. 一把梭升级给黄金矿工加“钻石随机刷新”和存档玩到这一步基础版已经能跑、能抓、能计分但这篇文章到这里还不收咱们做一个真正能写进简历的升级功能钻石随机刷新。这个功能虽然只是“每隔一段时间多出一个高价值物体”但它会迫使你重新设计物体生成逻辑、碰撞判定和 UI 刷新比单纯调参数有意思得多。新增 Diamond 类继承 GoldBlock覆写构造器把 value 设为 500collideRadius 设小一点这样“小而珍贵”的差异化就做出来了。然后写一个 DiamondSpawner 管理类内部维护一个 Timer 和当前场上钻石列表。每隔 15 到 20 秒随机在画布上半区生成一个钻石同时限制场上最多同时存在 2 个超过就不再生防止画面资源太拥挤。public class DiamondSpawner { private Timer spawnTimer; private ListDiamond activeDiamonds new ArrayList(); private int maxDiamonds 2; public DiamondSpawner(GamePanel panel) { spawnTimer new Timer(15000, e - { if (activeDiamonds.size() maxDiamonds) { activeDiamonds.add(createDiamondAtRandomPos(panel)); } }); spawnTimer.start(); } private Diamond createDiamondAtRandomPos(GamePanel panel) { int x new Random().nextInt(panel.getWidth() - 60) 30; int y new Random().nextInt(panel.getHeight() / 2) 100; return new Diamond(x, y); } }生成坐标的规则我故意只让它在画布上半区生成因为下半区离钩子原点太近抓取没有难度。这个经验是在实际调游戏平衡性时一点点试出来的钻石必须放在“玩家需要调整角度、等待摆动时机”的位置不然就失去了奖励的意义。验证功能是否合格的方法很简单连续玩五局确认钻石出现间隔稳定不会一刷新就刷在钩子正下方抓取后分数正确加 500画面没有闪烁。这套验证逻辑比“我跑了一下没报错”有价值得多。如果你要往项目里加音效记得用 Clip 而不是 AudioClip前者是流式播放后者阻塞性明显会让游戏循环卡顿。写到这里从拿到标题到拆开源码、理解钩子运动模型、搭建最小版本、踩过四个最常见的坑、做了一次功能升级一条路径完整走下来了。这份源码值不值得花一个周末去啃我的看法是如果你连 for 循环和类继承都还没过一遍它的优先级不如做一道算法题但如果你学完 Java 基础正愁没有东西把零散知识点拧成一股绳那它就是最合适的那个绳。我个人带人做课设时最爱用这个项目试水平能独立写出钩子分段碰撞检测的人Java 基础基本是过关的写完还说得出“为什么不直接用矩形检测”的人基本可以直接看并发和 JVM 了。希望这篇文章能帮你在 Swing 游戏这条路上少走点弯路照着改、拆开玩比什么都强。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询