简介这是一份基于Java实现的黄金矿工小游戏完整源码包面向Java初学者、课程设计学生以及想通过经典小游戏练手的开发者帮助读者理解Swing图形界面、游戏循环、碰撞检测与资源加载等核心机制。压缩包共30个文件约141KB其中9个java源文件承载主要业务逻辑10个class为编译产物另有png、gif、jpg等图片素材用于角色与场景渲染并附带README说明文档与iml工程配置目录结构清晰导入IDE即可运行调试。目前已有217人学习下载。读者可从中获得可直接运行的完整游戏工程、可复用的游戏开发框架、清晰的类与模块划分思路以及碰撞判定、分数统计、关卡控制等实现细节适合作为课程设计参考或二次开发起点。1. 黄金矿工小游戏用 Java 重写从 Swing 定时器到抓钩物理的完整拆解很多人第一次接触「黄金矿工」是在 Flash 时代现在想用 Java 复刻一个能跑、能玩、能当课程设计交的版本核心难点其实不在画面而在抓钩的摆动、伸缩、碰撞判定和关卡节奏控制。这个标题指向的是一个典型的 Java SE 桌面小游戏项目用 Swing 做窗口和渲染用 Timer 驱动帧循环用面向对象拆出矿工、钩子、金块、石头、炸药桶等实体再靠一套状态机管理「摆动—发射—回收—结算」四个阶段。它适合三类人正在找 Java 课程设计案例源码的学生、想用一个小项目把面向对象和事件驱动串起来的初学者、以及需要一份可运行小游戏来验证自己 Java 环境变量配置是否正确的开发者。下面按「能复现」的标准把选型、代码骨架、参数调优和踩坑点一次讲透。2. 为什么用 Swing Timer 而不是游戏引擎选型与工程结构2.1 轻量桌面方案的三个硬理由用 Java 做黄金矿工第一反应可能是 LibGDX 或 JavaFX但真正落地到课程设计或自学练手场景Swing 反而是最稳的选择。原因有三第一JDK 自带不需要额外配依赖java -version能跑就能开发省掉 Maven 拉包失败的玄学问题第二Swing 的JPanel加paintComponent足够画 2D 精灵和文字黄金矿工本身没有复杂粒子特效不需要 GPU 加速第三javax.swing.Timer的ActionListener天然适合做固定步长逻辑更新比手写Thread.sleep循环更容易控制帧率也不会阻塞事件分发线程。常见做法是把整个游戏拆成三层GameFrame负责窗口和主循环GamePanel负责渲染和输入GameModel负责实体状态和规则。这样拆的好处是逻辑和渲染分离后面想换皮肤或加关卡只动 Model 层。我一般会再抽一个GameState枚举把SWINGING、SHOOTING、RETRACTING、SETTLING四个状态显式写出来避免用一堆布尔标志互相打架。2.2 最小可运行骨架窗口、面板与定时器先给一个能跑起来的最小结构包含窗口创建、面板挂载和 60 FPS 定时器。代码里关键注释标了每个参数的作用。import javax.swing.*; public class GameFrame extends JFrame { public GameFrame() { setTitle(Gold Miner - Java Swing); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); // 800x600 是黄金矿工比较舒服的窗口比例矿工在顶部中间 GamePanel panel new GamePanel(); add(panel); pack(); setLocationRelativeTo(null); // 居中显示 setVisible(true); } public static void main(String[] args) { // Swing 组件必须在事件分发线程创建 SwingUtilities.invokeLater(GameFrame::new); } }import javax.swing.*; import java.awt.*; public class GamePanel extends JPanel { private final Timer timer; private int tick 0; public GamePanel() { setPreferredSize(new Dimension(800, 600)); setBackground(Color.BLACK); // 16ms 约等于 60 FPS黄金矿工不需要更高帧率 timer new Timer(16, e - { tick; update(); repaint(); }); timer.start(); } private void update() { // 每帧更新钩子角度、长度、实体位置 } Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 (Graphics2D) g; // 开启抗锯齿线条更顺滑 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setColor(Color.WHITE); g2.drawString(Tick: tick, 20, 20); } }逻辑说明Timer(16, ...)的 16 毫秒是经验值对应约 62.5 次每秒实际渲染会略低但足够流畅。update()里做纯逻辑paintComponent里只读状态画图不要在绘制方法里改数据否则会出现画面撕裂或状态不一致。参数上窗口尺寸建议固定 800×600 或 960×640不要用可缩放窗口因为黄金矿工的碰撞判定依赖固定坐标系缩放后钩子长度和金块位置会错位。2.3 实体类的字段设计矿工、钩子、金块面向对象在这里不是摆设字段设计直接决定后面碰撞检测好不好写。矿工只需要位置和当前钩子引用钩子需要角度、长度、伸缩速度、状态金块需要位置、半径、重量、分值。重量这个字段很关键它决定回收速度是黄金矿工的核心手感来源。public class Hook { public double angle; // 弧度摆动范围约 -75° 到 75° public double length; // 当前绳长像素 public double speed; // 伸缩速度像素/帧 public double swingSpeed; // 摆动角速度弧度/帧 public State state State.SWINGING; public double angleDirection 1; // 1 向右摆-1 向左摆 public enum State { SWINGING, SHOOTING, RETRACTING } }public class Gold { public int x, y; public int radius; public double weight; // 重量越大回收越慢 public int score; public boolean collected; }参数说明swingSpeed建议初始 0.02 弧度/帧约 1.15 度每帧摆动一个来回大概 2 秒节奏不紧不慢。speed发射速度建议 8 像素/帧回收速度用baseSpeed / weight计算baseSpeed取 10 左右这样大金块明显拖得慢玩家能感受到重量差异。角度范围不要超过 ±80°否则钩子会摆到矿工身后视觉上很怪。3. 抓钩状态机与碰撞判定把「摆动—发射—回收」写对3.1 四个状态的切换条件与边界状态机是这个小游戏最容易写乱的地方。血泪经验是不要用if (isShooting !isRetracting)这种组合判断直接上枚举加switch。SWINGING状态下角度按swingSpeed增减碰到 ±75° 就反向按下空格或鼠标点击后进入SHOOTING长度每帧增加speed当钩子碰到金块或到达最大长度时进入RETRACTING长度每帧减少retractSpeed长度回到初始值后回到SWINGING同时结算分数。public void update() { switch (state) { case SWINGING: angle swingSpeed * angleDirection; if (angle MAX_ANGLE) { angle MAX_ANGLE; angleDirection -1; } if (angle -MAX_ANGLE) { angle -MAX_ANGLE; angleDirection 1; } break; case SHOOTING: length shootSpeed; if (length MAX_LENGTH) { state State.RETRACTING; } checkCollision(); break; case RETRACTING: length - retractSpeed; if (length MIN_LENGTH) { length MIN_LENGTH; state State.SWINGING; carriedGold null; } break; } }逻辑说明MAX_ANGLE用Math.toRadians(75)得到MIN_LENGTH是钩子初始长度建议 40 像素太短会穿模太长会显得矿工手臂很怪。checkCollision()只在SHOOTING里调用因为回收时钩子已经带着东西不需要再判定。注意RETRACTING时如果带着金块retractSpeed要动态算不能直接用固定值。3.2 钩子末端坐标与圆形碰撞检测钩子的碰撞点不是矿工位置而是绳子末端。末端坐标用三角函数算endX minerX length * sin(angle)endY minerY length * cos(angle)。这里角度以竖直向下为 0向右为正。金块用圆形近似判定条件就是末端点到金块中心的距离小于金块半径加一个容差。private void checkCollision() { double endX minerX length * Math.sin(angle); double endY minerY length * Math.cos(angle); for (Gold gold : golds) { if (gold.collected) continue; double dx endX - gold.x; double dy endY - gold.y; double dist Math.sqrt(dx * dx dy * dy); // 容差 6 像素避免高速穿透 if (dist gold.radius 6) { gold.collected true; carriedGold gold; state State.RETRACTING; // 回收速度按重量衰减 retractSpeed BASE_RETRACT / gold.weight; break; } } }参数说明容差 6 像素是实测值太小会经常穿过去太大又会隔空吸金。BASE_RETRACT取 12小金块weight为 1回收速度 12 像素/帧大金块weight为 3回收速度 4 像素/帧手感差异明显。如果发现钩子穿过金块没反应先检查shootSpeed是不是太大超过金块半径就会一帧跨过去把shootSpeed降到 6 以下或加子步长检测。3.3 关卡数据与随机生成策略黄金矿工不能每关都手摆金块常见做法是用一个二维数组或列表描述每关的金块类型和大致区域再在区域内加随机偏移。这样既有设计感又不会每局完全一样。public ListGold generateLevel(int level) { ListGold list new ArrayList(); Random rand new Random(); int smallCount 4 level; int bigCount 2 level / 2; for (int i 0; i smallCount; i) { Gold g new Gold(); g.radius 12; g.weight 1; g.score 50; g.x 100 rand.nextInt(600); g.y 250 rand.nextInt(300); list.add(g); } for (int i 0; i bigCount; i) { Gold g new Gold(); g.radius 22; g.weight 3; g.score 200; g.x 100 rand.nextInt(600); g.y 300 rand.nextInt(250); list.add(g); } return list; }逻辑说明小金块数量随关卡线性增加大金块每两关加一个保证难度曲线平滑。x范围 100 到 700y范围 250 到 550避开矿工所在顶部区域。注意金块之间可能重叠生成后要加一次去重或最小间距检查否则会出现两个金块叠在一起、碰撞时只收集到一个的情况。4. 参数调优与手感打磨让抓钩「跟手」的五个关键值4.1 摆动速度、发射速度与回收速度的配比手感好不好九成看这三个速度。摆动太快玩家瞄不准太慢又无聊发射太快像子弹太慢像蜗牛回收速度如果和金块重量脱钩大金块就没有「沉」的感觉。我一般用下面这组起始值再根据试玩微调。参数建议值作用调整方向swingSpeed0.02 弧度/帧控制摆动节奏调大更刺激调小更稳shootSpeed8 像素/帧钩子发射速度超过 10 容易穿透BASE_RETRACT12 像素/帧空钩回收基准配合重量字段MAX_LENGTH520 像素钩子最大伸长按窗口高度调MIN_LENGTH40 像素钩子初始长度太短会穿模这张表不是死的但配比关系要守住shootSpeed必须小于金块最小半径乘以 2否则一帧跨过整个金块。BASE_RETRACT除以最大重量后不能小于 3否则大金块回收慢到玩家以为卡死。4.2 用固定时间步长避免「不同电脑速度不一样」直接用Timer的 16 毫秒做逻辑更新在不同性能的机器上会有细微差异因为Timer不保证精确间隔。更稳的做法是记录deltaTime所有速度乘以deltaTime / 16.0做归一化。这样即使某一帧卡了 30 毫秒钩子也不会突然跳一大截。private long lastTime System.currentTimeMillis(); private void update() { long now System.currentTimeMillis(); double delta (now - lastTime) / 16.0; // 以 16ms 为 1 个标准步长 lastTime now; // 防止切后台后 delta 过大导致瞬移 if (delta 3) delta 3; hook.angle hook.swingSpeed * hook.angleDirection * delta; hook.length hook.shootSpeed * delta; // ... 其余更新同理 }逻辑说明delta上限设为 3是防止窗口最小化再恢复时now - lastTime变成几秒导致钩子瞬间飞到最大长度。这个上限值可以根据实际测试调整但不要超过 5。所有涉及速度的字段都要乘delta位置类字段不用。4.3 分数结算与关卡目标的设计黄金矿工原版的关卡目标是在限定时间内达到目标分数这个机制用 Java 实现很简单但要注意结算时机。常见错误是在钩子还在回收时就加分数导致玩家看到分数先涨、金块还没到手。正确做法是等length回到MIN_LENGTH且carriedGold ! null时再加分并清空携带物。if (length MIN_LENGTH) { length MIN_LENGTH; if (carriedGold ! null) { score carriedGold.score; carriedGold null; } state State.SWINGING; }参数说明目标分数建议按关卡递增第一关 300之后每关加 150。倒计时用另一个Timer或同一个Timer里累加delta换算成秒剩余时间少于 10 秒时把文字变红提醒。注意倒计时归零时要立即停止钩子更新并弹出结算不要等当前动作做完否则会出现时间到了还能再抓一块的漏洞。5. 避坑与排查黄金矿工 Java 版最常见的五个翻车点5.1 钩子穿模金块明明在眼前却抓不到现象钩子末端划过金块视觉上已经重叠但checkCollision没触发。原因通常是shootSpeed太大一帧移动距离超过金块直径碰撞检测只在帧末做中间状态被跳过。解决把shootSpeed降到 6 以下或者在SHOOTING状态里做子步长检测每帧分 2 到 3 次小步移动并逐次判定。5.2 画面闪烁paintComponent里改了状态现象游戏运行几秒后画面开始闪或者金块位置跳动。原因是在paintComponent里调用了update或修改了实体坐标导致渲染和逻辑互相干扰。解决严格分离paintComponent只读不写所有状态变更放在Timer的update里。如果还闪检查有没有在paintComponent里new对象频繁创建对象会触发 GC 导致卡顿。5.3 事件分发线程阻塞Thread.sleep惹的祸现象点击鼠标后窗口无响应或者按钮点不动。原因是在事件回调里用了Thread.sleep做延时阻塞了 EDT。解决所有延时用Timer或SwingUtilities.invokeLater做异步不要在actionPerformed里写循环等待。黄金矿工的钩子伸缩本身就是逐帧更新不需要额外延时。5.4 金块重叠随机生成没做间距检查现象两个金块叠在一起抓走一个后另一个位置很怪或者分数只加了一次。原因是随机坐标没有最小间距约束。解决生成每个金块后遍历已生成的列表如果距离小于两个半径之和加 10 像素就重新生成坐标最多重试 20 次超过就跳过这个金块。5.5 切后台后钩子瞬移delta没设上限现象最小化窗口再恢复钩子直接飞到最大长度或者角度跳到边界。原因是System.currentTimeMillis()的差值在切后台期间累积成几秒乘以速度后位移巨大。解决在计算delta后加if (delta 3) delta 3;并且窗口失去焦点时暂停Timer恢复时重置lastTime。6. 进阶技巧用策略模式替换 if-else 做多类型矿块当你想加更多矿块类型——比如会移动的钻石、带倒计时的炸药桶、抓上来扣分的石头——继续在checkCollision里堆if (gold.type ...)会越来越难维护。我一般会引入策略模式把每种矿块的「碰撞后行为」和「回收速度计算」抽成接口Gold持有一个GoldBehavior引用。这样加新类型只需要新增一个实现类不用改主循环。public interface GoldBehavior { double getRetractSpeed(double baseSpeed); void onCollected(GameModel model); } public class NormalGold implements GoldBehavior { Override public double getRetractSpeed(double baseSpeed) { return baseSpeed / 1.0; } Override public void onCollected(GameModel model) { model.addScore(50); } } public class HeavyGold implements GoldBehavior { Override public double getRetractSpeed(double baseSpeed) { return baseSpeed / 3.0; } Override public void onCollected(GameModel model) { model.addScore(200); } } public class BombGold implements GoldBehavior { Override public double getRetractSpeed(double baseSpeed) { return baseSpeed / 1.5; } Override public void onCollected(GameModel model) { model.addScore(-100); model.triggerShake(); // 屏幕震动反馈 } }逻辑说明getRetractSpeed把重量逻辑从主循环里挪出来onCollected把加分、扣分、特效等副作用集中管理。GameModel提供addScore和triggerShake等接口策略类只依赖模型不直接碰渲染。参数上BombGold的回收速度设为基准的 1/1.5比普通金块慢但比大金块快让玩家在抓之前需要犹豫一下。验证策略模式有没有生效最简单的办法是写一个单元测试构造三种GoldBehavior分别调用getRetractSpeed(12)断言结果符合预期。这样以后调参数不会误伤其他类型。另外策略对象建议做成无状态单例不要每次生成金块都new一个减少 GC 压力。我自己的习惯是每加一种新矿块先写行为类再写测试最后才接到关卡生成里。这样即使关卡数据配错了也能快速定位是生成逻辑问题还是行为逻辑问题。黄金矿工这个项目看着小但状态机、碰撞、手感、扩展性四个点都能练到作为 Java 课程设计案例源码是够格的。希望帮到你。本文还有配套的精品资源点击获取
