☰
Java Swing游戏开发实战:用状态机与碰撞检测实现黄金矿工
2026/10/4 14:18:40 网站建设 项目流程

简介:基于Java Swing的黄金矿工小游戏完整项目,配套视频教程,适合Java初学者、Swing技术爱好者与毕业设计选题学生。整个压缩包包含664个文件,除Java源码与编译后的class文件外,还有121个xml配置用于工程与界面布局,92张png、69张gif和46张jpg图片素材覆盖游戏场景与角色动画,26个mp4教学视频演示开发全过程,整体约258MB,便于边看边练、按步骤理解桌面游戏开发流程。已有111人学习下载。资源按开发步骤组织,可跟着视频从零编写代码,也可直接阅读工程结构,掌握窗口绘制、碰撞检测、游戏循环等Swing核心知识点,为后续Java桌面应用开发打下扎实基础。

1. 一个压缩包能教会你的,比“Java基础”多得多

如果只给你一个写着“Java开发黄金矿工游戏.rar”的压缩包,你能从这个文件名里读出什么?可能是一份课程设计、一套毕业设计源码,或者一个刚学完 Java 基础的人攒出来的第一个完整项目。但在我眼里,黄金矿工这个游戏的代码量不大,却刚好把 Swing 绘图、多线程调度、碰撞检测、状态机四件事全揉在一起了,是典型的“基础都会、一动手就卡”的训练场。你需要注意这游戏真正的难点不在画面多花哨,而在钩爪的运动模型:什么时候伸、什么时候收、勾到金子后回收速度怎么衰减,这些细节直接决定游戏手感。我见过很多人照着教程敲完,结果勾到钻石直接穿过去,或者回收快得像开了加速器,问题全出在对“钩爪状态”的理解上。本文就用一套能跑起来的最小实现,把这个状态模型拆给你看:先立住原理,再给可复现代码,最后是调试经验。适合拿它练手的,是学过 Java 语法和面向对象、但还没独立写完过两百行以上交互程序的人,以及想在手头留一个干净的小项目应对面试提问的开发者。

2. 钩爪的运动机制与多线程模型:动手前先拆清三个状态

黄金矿工的玩法一句话就能说清:一根挂在锚点上的钩爪左右摆动,你按一次发射键,钩爪就沿当前角度射出去,碰倒金子、石头或边界后自动收回,把抓到的东西拖回锚点换钱。所有的趣味都来自“能不能勾准”和“勾到什么后值不值得收”。如果你直接开始写代码,第一版多半是把钩爪位置当普通坐标来算,鼠标点一下就直线射出去——那已经不是黄金矿工了。我一般会在动笔之前先把钩爪的运动拆成状态机,把界面绘图和逻辑更新分开,这一章就按这个顺序讲原理。

2.1 钩爪的三态机:旋转、射出、回收

钩爪的任何时刻只属于三种状态:ROTATING(未发射,绕锚点左右摆动)、EXTENDING(发射后持续向前伸出)、RETRACTING(碰到目标或边界后往回拖)。游戏循环每走一帧,只需要根据当前状态做一次更新:旋转态改角度,伸长态加长度,回收态减长度并顺带把抓到的物品拖回来。这三个状态之间的切换条件值得单独捋一遍,因为后续所有 bug 几乎都出在切换条件的遗漏上。常见做法是用一个 Java 枚举定义状态,Hook 类内部持有当前状态和切换方法,而不是用散落的布尔变量互斥。

public enum HookState { ROTATING, EXTENDING, RETRACTING }

旋转态到伸长态的切换由玩家按键触发,这也是整个游戏里唯一的主动操作入口。伸长态到回收态有两个触发点:一是末端碰到任意物品,二是钩爪长度超过最大值或末端越界。后一个触发点很容易被漏掉,漏掉的典型症状是钩爪会在屏幕边缘越伸越远,永不回头。回收态回到旋转态也有两个触发点:物品被拖回锚点并结算分数,或者空手完全收回。注意单帧内最多只处理一次状态切换,否则会出现“刚碰到钻石瞬间就判定为回收完成”的穿透现象。这些边界情况,我会在第 5 章结合实际现象展开排查。

2.2 为什么选 Swing Timer 而不是 new Thread

很多新手教程会把游戏循环写成“while (true) { update(); repaint(); Thread.sleep(16); }”,然后告诉你这是标准做法。这在大项目里是灾难,因为 Swing 组件不是线程安全的,直接在非事件分发线程里调用 repaint 会间歇性地抛NullPointerException或界面卡死。我的建议是用javax.swing.Timer,它会在事件分发线程(EDT)上按固定间隔触发actionPerformed,你在这个回调里做位置更新和重绘,天然规避了并发问题。

Timer timer = new Timer(16, this); // 约 60 帧/秒 timer.start();

这里的 16 是毫秒间隔,换算下来大约 60 FPS。要区分两个容易混淆的“Timer”:javax.swing.Timer用于界面层,适合本项目的游戏主循环;java.util.Timer是普通后台定时器,回调跑在独立线程里,适合做存档自动保存这类与界面无关的任务。如果是带 Spring 的 Web 项目你会接触 Quartz、XXL-Job 这类 Java 定时任务框架,但 Swing 小游戏完全用不上。黄金矿工只有一个主循环,没必要引入额外调度框架,Swing Timer 的精度对帧率已经足够。如果后面你发现计时不准,那就是用了普通线程去提前更新逻辑,而不是 Timer 本身的问题。

2.3 碰撞检测的两种判定方式

钩爪的碰撞判定对象只有“末端点”和“物品矩形”。旋转和伸长过程中,钩爪画成一条从锚点连到末端点的线段,线段本身不做碰撞,只有末端点参与了交互判定,这是街机版黄金矿工的常见简化处理,也是手感的关键:钩爪头部的“牙”碰到东西才算抓到。实现上用Rectangle.contains(int x, int y)就能完成判定。第二种判定方式是把末端点当成一个小圆,与物品矩形做膨胀矩形碰撞,适合你想让爪子“抓得宽一点”的时候,原理不变,只是矩形外扩几个像素。

Rectangle hitBox = new Rectangle(item.x, item.y, item.width, item.height); boolean hit = hitBox.contains(hookEndX, hookEndY);

但这里有个很实际的连环坑:如果钩爪每帧前进的像素大于物品的边长,末端点可能从物品矩形的一侧直接跳到另一侧,造成“穿过钻石”的现象,这叫隧道效应。我给的参数是钩爪每帧伸出 3 像素,而最小物品(钻石)宽 12 像素,通常不会穿透。如果你调大了速度或者物品尺寸很小,就要用细分步进:把这一帧的位移拆成 N 小段,每段都做一次碰撞判定。这个留到第 5 章排错部分细说。

3. 落地一个可玩的黄金矿工:核心类设计与关键代码

原理讲清楚了,这一章直接落到能复现的工程结构。这个项目我建议只拆三个核心类:Hook管钩爪三态与位置推进,Item管地图上的矿物数据,GamePanel管游戏循环、碰撞与绘制。不要一上来就加 MVC、框架、配置文件,两百行能跑通的最小骨架才是第一步。下面的代码就是可抄作业的版本,我基于 640×480 的面板尺寸来写,锚点放在(320, 60),钩爪初始朝正下方,左右各摆 60 度。

3.1 Hook 类:角度、长度与速度的推进逻辑

Hook 类应该是整个项目里最干净的一个类,它不需要知道 GamePanel 的存在,只维护自己的状态和数值。我把角度定义为“与垂直向下方向的夹角”,用弧度存储,这样Math.cos(angle)控制 y 方向位移,Math.sin(angle)控制 x 方向位移,角度为 0 时正好朝下射。旋转速度设计为每帧 0.02 弧度,约每秒 68 度,摆完正负 60 度一个来回大约 1.77 秒,手感比较适中。

public class Hook { private int anchorX, anchorY; // 锚点坐标 private double angle = 0; // 当前角度,弧度 private double length = 0; // 钩爪当前长度 private HookState state = HookState.ROTATING; private static final double MAX_ANGLE = Math.toRadians(60); private static final int MAX_LENGTH = 420; private static final double ROTATE_SPEED = 0.02; // 弧度/帧 private static final double EXTEND_SPEED = 3.0; // 像素/帧 private static final double RETRACT_SPEED = 4.0; // 像素/帧 public void update() { switch (state) { case ROTATING -> { angle += ROTATE_SPEED; if (angle > MAX_ANGLE) { angle = MAX_ANGLE; // 反向摆动:通过切换增量方向实现 rotateDirection = -1; } else if (angle < -MAX_ANGLE) { angle = -MAX_ANGLE; rotateDirection = 1; } angle += rotateDirection * ROTATE_SPEED; } case EXTENDING -> { length += EXTEND_SPEED; if (length >= MAX_LENGTH) { state = HookState.RETRACTING; } } case RETRACTING -> { length -= RETRACT_SPEED * speedFactor; if (length <= 0) { length = 0; state = HookState.ROTATING; speedFactor = 1.0; // 复位 } } } } // 省略 getter、fire()、attachItem() 等方法 }

上面代码里有几个参数需要注意。speedFactor是回收速度系数,默认 1.0,抓到金块后我会把它调成 0.4,抓到大石头调成 0.15,这个数字决定拖拽的“沉重感”。EXTEND_SPEED是每帧伸长像素数,调大一些会让钩爪射得更快,但碰撞穿透风险同步上升;调的太小则会让玩家觉得发射绵软无力。MAX_LENGTH取 420 是为了保证钩爪在 640×480 面板里不会超出左右边界太多,实际你可以让钩爪越过屏幕底边一小段再回收,那要单独加越界判断。旋转时注意rotateDirection字段的初始值必须为 1,且每次碰到边界要翻转方向,否则钩爪只会在左边或右边单侧摆动。

3.2 Item 类:把金子石头钻石抽象成对象

Item 类存的是静态数据和绘制所需信息。重量感体现在刚才的speedFactor上,但 Item 自身还要记录价值、矩形范围、是否已被抓取。抓取状态必须单独标记,否则回收过程中钩爪末端还压在物品矩形上,每一帧都会触发一次新的碰撞判定,造成分数重复计算。

public class Item { private int x, y, width, height; private ItemType type; private boolean captured; public enum ItemType { GOLD_NUGGET("金块", 30, 0.5), GOLD_BAR("金条", 60, 0.3), DIAMOND("钻石", 100, 0.9), STONE("石头", 1, 0.15); private final String name; private final int value; private final double retractFactor; ItemType(String name, int value, double retractFactor) { this.name = name; this.value = value; this.retractFactor = retractFactor; } } public Item(ItemType type, int x, int y) { this.type = type; this.x = x; this.y = y; this.width = 30; this.height = 24; // 石头更大,碰撞面积也更大 if (type == ItemType.STONE) { this.width = 40; this.height = 34; } } }

这里有个设计取舍:如果把retractFactor直接写在 Hook 里,每次抓到新物品就要用 if-else 判断类型再赋值,代码会越写越长;把重量感定义在枚举里,新增一种矿物只需要加一个枚举值。你以后想加“炸药桶”或者“宝物袋”,直接扩展 ItemType 就行。物品的宽高我故意让石头大于钻石,这不仅是视觉真实感,也影响碰撞难度:钻石小但价值高、回收轻快,石头大而笨重,适合用来卡钩爪。生成物品时还需要防止重叠,做法循环很简单——随机位置生成后遍历已有物品做矩形相交检测,相交则重新生成,我一般限制每关最多 12 个物品,超过就直接放弃生成,避免死循环。

3.3 GamePanel 主循环:把核心逻辑组装起来

GamePanel 继承JPanel并实现ActionListener,在 16ms 一次的actionPerformed里依次推进 Hook、检测碰撞、更新界面。绘制时用Graphics2D,把物品先画出来,再画钩爪线段和末端爪子。这里有一个小细节:先调用super.paintComponent(g)清背景,否则画面会留下上一帧的残影。完整的绘制代码比较长,下面只贴主循环和碰撞这一段,这是组装逻辑时最容易错的位置。

public void actionPerformed(ActionEvent e) { hook.update(); if (hook.getState() == HookState.EXTENDING) { checkCollision(); } if (hook.getState() == HookState.RETRACTING && hook.hasItem()) { moveItemWithHook(); } repaint(); } private void checkCollision() { int endX = hook.getEndX(); int endY = hook.getEndY(); for (Item item : items) { if (item.isCaptured()) continue; Rectangle rect = new Rectangle(item.getX(), item.getY(), item.getWidth(), item.getHeight()); if (rect.contains(endX, endY)) { hook.attachItem(item); // 将 speedFactor 设为 item 的 retractFactor item.setCaptured(true); return; // 一帧只抓一个物品 } } }

checkCollision里那两个 if 是这套实现的关键:只有在伸长态才允许新捕获;只有已经带着物品进回收态,才把物品坐标往锚点方向拉。如果漏了return,同一帧内钩爪末端可能同时压在两个物品的重叠区域上,导致一次发射抓到两块。物品跟随钩爪移动的方法也很直接,每次更新把物品坐标设回钩爪末端坐标即可。注意一个容易忽视的点:物品一旦被标记captured,就不能再参与任何碰撞判定,否则拖回途中会因为末端再次碰到它而重复进入捕获流程。主循环里的repaint()只负责请求重绘,真正的绘制逻辑在paintComponent里,不要试图在这里直接调用绘图。

4. 游戏数值与难度曲线:用参数把“好玩”这件事量化

代码跑通之后,很快会遇到一个尴尬阶段:钩爪能伸能缩,碰撞也正常,但玩起来就是没意思。不是逻辑错了,是数值没调。黄金矿工的好玩程度几乎完全取决于“能不能勾到好东西”和“勾到一个烂东西要浪费多少时间”之间的张力。这块没有标准答案,但有一些通用原则,我整理了常用数值表,能让你在 30 分钟内调出第一版可玩的手感。

4.1 物品价值与回收系数的搭配逻辑

游戏的紧张感来源是时间压力,因此每个物品真正消耗的资源不是“勾它花了几秒”,而是“收回来花了几秒”。如果回收都很快,玩家就会无脑发射,没有任何取舍。拿到参数后逐项核对,确认“重量”和“价值”确实成反比安排。

物品类型参考价值回收速度系数勾取建议
小钻石1000.9最值得优先勾,又快又贵,但体积小容易空枪
金块300.5体积中等,是前期稳定收益来源
金条600.3又大又重,一次回收耗时较长,注意时间余量
石头10.15明确负收益,只会在钩爪够不到好物品时出现
宝物袋2001.0同时加 5 秒游戏时间,后期核心目标

这里必须解释一个反直觉现象:金条价值 60,但回收系数只有 0.3,比金块慢得多,所以单个金条的单位时间收益反而低于金块。我一般不会把价值比较迷的数值写进需求文档,直接用“单位时间收益 = 价值 × 回收速度系数 ÷ 路程”来算。在这个公式下,如果距离锚点 200 像素处有一个 60 分的金条,它的收益大约等于 150 像素处一个 30 分的金块乘以它的速度系数比值,你会发现金条没那么划算。于是玩家自然形成策略:优先勾钻石和宝物袋,金块作为兜底,石头能不碰就不碰。这种“数值逼出策略”的设计就是这类小游戏好玩的底层来源。

4.2 难度递进:石头占比、深度与时间压力的配比

单关数值定下来后,难度曲线才是让玩家持续玩下去的关键。第 1 关如果全是石头,玩家三分钟就卸游戏;全是钻石又撑不过两关就腻。我用的参数是:开局只放金块和少量金条,第 2 关开始出现石头,第 3 关加入钻石,第 4 关起每关按比例增加石头数量,同时适度提高物品生成深度。所谓深度,就是物品中心的 y 坐标均值,第 1 关大约在 250 像素附近,第 4 关可以推到 380 像素以上——更深意味着钩爪行程变长,往返耗时翻倍,时间压力自然上来。

public List<Item> generateLevel(int level) { Random rand = new Random(); int itemCount = Math.min(6 + level * 2, 12); int stoneRatio = Math.min(level * 10, 40); // 百分之多少 List<Item> result = new ArrayList<>(); for (int i = 0; i < itemCount; i++) { int roll = rand.nextInt(100); Item.ItemType type; if (roll < stoneRatio) { type = Item.ItemType.STONE; } else if (level >= 3 && roll < stoneRatio + 15) { type = Item.ItemType.DIAMOND; } else if (level >= 2 && roll < stoneRatio + 25) { type = Item.ItemType.GOLD_BAR; } else { type = Item.ItemType.GOLD_NUGGET; } int x = 40 + rand.nextInt(560); // 避开左右边缘 int y = 180 + rand.nextInt(280); // 避开锚点附近 result.add(new Item(type, x, y)); } return result; }

这个生成器要注意几个显式参数。itemCount从 6 个起步,每关加 2 个,封顶 12 个,防止后期画面挤成一团。stoneRatio是核心难度控制变量,我用level * 10让它线性增长到 40% 就不再涨,避免满屏石头直接击穿玩家的耐心。钻石条件设定为level >= 3,给新手保留了前两关的学习时间。y最小值设为 180 是为了避免物品和锚点重叠,而rand.nextInt(280)的最大值是由面板高度 480 减去物品高度和安全边距估算出来的,改面板分辨率时这里要跟着调,否则物品会出界。经验是随机生成后打印一次所有物品坐标,肉眼检查最值和重叠情况,比写复杂的单元测试更快。

4.3 时间奖励怎么给才不失控

宝物袋是后期常见的“续命”道具,价值高还给额外时间,但如果每关都给太多,时间压力就失去意义。我一般把时间奖励控制在 3 到 5 秒之间,且宝物袋出现的概率独立计算,不参与上面那段比率判断。原因在于时间奖励会影响玩家决策:剩余 10 秒时出现一个宝物袋,玩家会赌一把,这种局部随机正是游戏反复游玩的钩子。需要注意实现上的细节是“加时间”不能在游戏主 Timer 回调里直接让倒计时变量倒退,因为当时可能处于回收态,玩家看到时间减少又突然增加会有数值回跳的怪异感。我把时间奖励也做成一个“待生效”队列,在本帧结束时统一结算,保证界面上的倒计时逐秒单调递减。

5. 黄金矿工开发避坑:5 个常见问题与排查方法

到这一章,我假设你的游戏已经能跑了,只是有不满意的地方。下面五条是从这个类型的小项目里反复爬出来的问题,每条都按现象、原因、解决三段给到排查思路。如果你遇到现象对不上,先别急着怀疑代码,多半是某个参数越界引起的连锁反应。

5.1 画面闪烁像幻灯片,残影严重

现象:钩爪移动时尾巴拖了长长一条,或者整个面板疯狂闪烁,看起来像几十年前的卡顿画面。原因:paintComponent里没有先调用super.paintComponent(g)清空画布,或者你手动用this.setBackground()在每帧开头刷新背景,导致背景重绘和内容重绘互相覆盖。Swing 默认启用了双缓冲,正常情况下不该闪,出现闪烁基本是绘制代码破坏了缓冲机制。解决:重写paintComponent时第一行写super.paintComponent(g),背景色在构造函数里设好,不要在绘制方法里改。如果仍闪,检查是不是你另外开了一个Thread循环调用repaint(),那是把非 EDT 线程的重绘请求和 EDT 的绘制挤在一起了,换成 Swing Timer 会立刻好转。

5.2 钩爪穿过小钻石而抓不到

现象:钩爪末端明显从小钻石的矩形里穿过去了,但没有任何反应。原因:大概率是隧道效应——钩爪每帧移动 3 像素,小钻石宽 12 像素,正常不该穿,但你如果把EXTEND_SPEED改成 6 像素甚至更大,而钻石被缩到 8 像素宽,末端点就会从钻石一侧越到另一侧。解决:降低每帧步长,或者用连续碰撞检测。常见做法是在checkCollision里把上一帧末端到这一帧末端连成一条线段,用lineIntersectsRect判断线段和物品矩形是否相交。

boolean hit = new Line2D.Double( prevEndX, prevEndY, endX, endY ).intersects(rect);

用线段检测要注意一个边界条件:上一帧和当前帧都在矩形内部时,线段检测会失效,因为两个端点都在矩形里,intersects返回 false。稳妥做法是同时保留点包含和线段检测两个判断,任一命中即视为抓到。代码逻辑并不复杂,值得实现,因为这是唯一能根治穿透问题的方案。

5.3 不同电脑上游戏速度差很多

现象:同一份代码,你的机器上钩爪摆速正好,朋友电脑上慢得离谱。原因:有人用了while(true) { Thread.sleep(10); }做主循环,Thread.sleep的参数只是最短等待时间,实际唤醒间隔取决于操作系统调度;显示器刷新率不同也会影响 Swing Timer 的实际触发节奏。解决:把“每帧移动多少像素”改成“每秒移动多少像素”,通过System.nanoTime()计算两帧间隔,用间隔乘以每秒像素数得到本帧位移。这个修改在代码里体现为把EXTEND_SPEED等常量从“每帧值”改成“每秒值”,并做一次换算。

long now = System.nanoTime(); double deltaSec = (now - lastTime) / 1_000_000_000.0; lastTime = now; hook.extend(deltaSec * EXTEND_SPEED_PER_SEC);

这样改完,游戏在 60Hz 和 144Hz 显示器上转速基本一致。注意deltaSec可能因为系统休眠或切换窗口出现一次很大跳变,比如从 16 毫秒跳到 200 毫秒,如果直接把位移乘上去,画面会出现瞬移。我一般会给deltaSec设一个上限,超过 0.05 秒就按 0.05 秒算。这也是 online 游戏同步逻辑里常见的“钳位 deltaTime”做法。

5.4 打包成可执行 Jar 后游戏图标全部丢失

现象:在 IDE 里运行一切正常,打出 jar 包后启动,背景是黑的,物品和钩爪都不见了,但程序没有报错。原因:图片资源用了new ImageIcon("src/main/resources/images/gold.png")这种基于文件系统路径的写法,运行时工作目录一变,路径就失效。解决:改用类路径加载,把图片放进 jar 包,用ClassLoader读取资源。这一步是 Java 游戏开发里从“跑通”到“能交付”的分水岭。

ImageIcon icon = new ImageIcon( GamePanel.class.getResource("/images/gold.png") );

这里有个重要提醒:ClassLoader.getResource路径以/开头表示从 classpath 根目录查找,如果你打包时资源在resources/images下,最终放进 jar 里的路径一般就是/images/gold.png。如果你的构建工具把 resources 和 classes 分开打包,先解压 jar 确认一下路径再改代码,不要凭空猜测。项目交付给别人的时候,这个坑几乎必踩,所以我习惯在 README 里直接写清楚“JDK 8+ 并配置好 java 环境变量”和启动命令,避免用户解压后双击 jar 没反应就先来问我环境问题。

5.5 界面卡死或日志报 Not on Event Dispatch Thread

现象:运行几秒后界面整体冻住,控制台偶尔出现Not on Event Dispatch Thread的异常。原因:你大概率在某个监听器回调里启动了new Thread,并在这个新线程里更新了 Swing 组件。Swing 是单线程模型,所有 UI 更新必须发生在 EDT 上。解决:把主循环统一放进 Swing Timer,业务线程只计算结果,通过SwingUtilities.invokeLater把界面更新提交回 EDT。这个项目里最常见的场景是“倒计时归零后自动停止游戏”,有人在 Timer 回调里 sleep 了一秒再更新界面,卡顿就是 sleep 阻塞了 EDT。别在任何回调方法里做耗时操作,文件保存、图片缩放这种事放到普通线程做,再回 EDT 刷新。

6. 从能玩到能展示:存档、关卡生成与代码体检

当游戏基本可玩,你应该把精力转向两个方向:一是让这个项目能放进简历和代码仓库里“见人”,二是加上一点超出课程设计水平的机制。黄金矿工这类小游戏的进阶并不是无限加新玩法,而是把“手感细节”和“工程质量”做扎实。它不需要硬塞 Spring Boot、MyBatis 之类的企业框架,坚持用纯 Java 和 Swing 反而更聚焦。

存档系统是最值得先做的一项。常见做法是把“关卡、当前得分、剩余时间、物品清单”写成 Properties 文件,退出游戏时保存,下一次启动读档恢复。

Properties prop = new Properties(); prop.setProperty("level", String.valueOf(level)); prop.setProperty("score", String.valueOf(score)); try (OutputStream out = Files.newOutputStream(Paths.get("goldminer.properties"))) { prop.store(out, "Gold Miner Save Data"); }

存档有一点要设计好:读档时所有物品的captured状态必须重置,否则会出现“上次关游戏前抓到一半的金块,这次打开卡在半空回不来”。我吃过这个亏,存档系统上线后第一天上手就翻了车,从那以后凡是恢复对象状态,我都会在读取后额外做一遍合法性校验,比如物品坐标必须在面板范围内、得分不能为负数。写单元测试时也顺手给存档的写入和读取写一个往返用例,保证所有字段序列化后能原样还原。

关卡生成器建议继续做深一步:把“完全随机”改成“种子随机”。Random(seed)固定种子之后,同一关每次生成结果一致,这对调试非常有用,玩家报“第 5 关太容易出石头”时,你可以直接复现他的关卡去排查,不用反复刷运气。

代码体检的自查清单也是交付前必须过一遍的:所有运动参数是否已改成时间驱动;所有图片是否通过 classpath 加载;钩爪状态切换是否覆盖了全部边界路径;物品绘制是否在paintComponent内完成;是否还有Thread.sleep出现在 UI 回调里。照着这份清单过一遍,哪怕只做到一半,整个项目的可读性都会上一个台阶。我给自己刚写的代码做体检时,习惯把 Hook 类的三个状态切换点逐个画个日志,跑到什么条件切到什么状态直接看得见,这比盯着代码猜快得多。希望这套流程对你也有用,做完体检之后,黄金矿工这个项目就真正从“照着敲”变成“自己会改”了。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询