新闻详情

新闻详情

首页 / 资讯中心 / 详情

Java Swing黄金矿工游戏开发实战:从主循环到碰撞检测

发布时间:2026/9/15 4:50:36来源:尧图网络
Java Swing黄金矿工游戏开发实战:从主循环到碰撞检测
简介基于 Java 8 与 Idea 2021 开发环境整理的黄金矿工游戏源码包面向 Java 初学者、对 Swing 桌面应用开发感兴趣的人员以及需要完成毕业设计的在校大学生。资源定位在通过一个完整可运行的游戏项目帮助读者掌握 Swing 组件、事件监听、绘图与线程处理等桌面应用开发核心技能。整套资源共 664 个文件rar 压缩包约 257.84MB其中 java 源码与 xml 工程配置可配合 iml 文件在 Idea 中直接打开class 文件便于对照编译结果png、gif、jpg 图片素材覆盖游戏场景与角色动画mp4 视频教程则按开发步骤演示编码过程另有 zip 与 txt 补充说明。目前已有 111 人学习下载。内容按开发步骤划分既可以直接阅读完整代码也可以跟着视频教程逐行编写从窗口搭建、地图绘制到游戏逻辑与交互反馈均包含在内适合用于课程设计、毕业设计原型或 Swing 技术入门练习。1. 为什么说黄金矿工是 Java 图形编程里最完整的动手题搜索「Java开发黄金矿工游戏」时你多半正处在 java 学习路线上的某个节点语法过了一遍想做一个能跑、能玩、能演示的 java 项目又不想引入 Spring Boot 或数据库。这个游戏刚好卡在这里——纯 Swing 加 Java 2D不加中间件一个周末就能把主体写完。但别因为它小就轻视。钩子在头顶左右摆动本质是角度模型按下空格出钩本质是状态机迁移钩尖碰到金块本质是碰撞检测每一关生成什么物品本质是数据驱动设计。这些概念放在 java 基础八股文里是零散的在这个项目里会被同一个主循环串成一条完整的线。下面按「主循环 → 钩子玩法 → 物品与关卡 → 手感调优」展开每章都给可抄的代码和可调的参数。新手能照着搭出能跑的工程熟手能在状态机和固定步长这几处看到值得抠的边界。2. Java Swing 游戏主循环从窗口创建到帧刷新主循环是这类定时器游戏的地基。窗口创建、画布装载、每帧更新、重绘请求这套流程在任何 Swing 小游戏里都是同一套路。先把它跑通后面的玩法逻辑才有地方挂。2.1 入口类与画布类各管一件事先定一个原则窗口归窗口画布归画布。GoldMinerFrame只负责窗口生命周期GamePanel只负责帧更新和绘制。很多人习惯把所有代码塞进一个继承 JFrame 的类一二百行时还能忍加入物品、关卡、倒计时之后类会迅速膨胀到没法维护。public class GoldMinerFrame extends JFrame { public GoldMinerFrame() { setTitle(黄金矿工); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); GamePanel panel new GamePanel(800, 600); setContentPane(panel); pack(); setLocationRelativeTo(null); panel.start(); setVisible(true); } public static void main(String[] args) { SwingUtilities.invokeLater(GoldMinerFrame::new); } }这里两个细节别省setResizable(false)是为了让逻辑坐标和渲染坐标始终一致窗口一旦拉伸所有碰撞半径和生成区间全部错位SwingUtilities.invokeLater保证窗口创建发生在事件分发线程EDT上这是 Swing 项目最基础的一条线程规则。拿到一个 .rar 压缩包后先看目录src和resources分开、图片资源单独放的基本能直接编译运行所有.java和.png混在一个目录里的先做好重构的心理准备。GoldMiner/ ├── src/com/goldminer/ │ ├── GoldMinerFrame.java │ ├── GamePanel.java │ ├── model/Hook.java │ ├── model/GoldItem.java │ └── model/ItemType.java └── resources/ ├── gold.png ├── diamond.png └── rock.pngGamePanel只持有三样东西Hook、ListGoldItem、LevelState。渲染时只从这三个对象读数据更新时也只改这三个对象后续加功能不用重写窗口。2.2 用 javax.swing.Timer 驱动 update 和 repaint游戏循环最常见的错误写法是while(true) { update(); repaint(); Thread.sleep(16); }它会把 EDT 堵死键盘鼠标事件全部卡住。正确且省事的方案是javax.swing.Timer——它在 EDT 上触发回调天然和 Swing 组件同线程不需要手动做线程切换。public class GamePanel extends JPanel { private final Timer timer; private long lastNanos; public GamePanel(int width, int height) { setPreferredSize(new Dimension(width, height)); timer new Timer(16, this::tick); } public void start() { lastNanos System.nanoTime(); timer.start(); } private void tick(ActionEvent e) { long now System.nanoTime(); float dt Math.min((now - lastNanos) / 1_000_000_000f, 0.05f); lastNanos now; update(dt); // 推进游戏状态dt 是距上一帧的真实秒数 repaint(); // 请求重绘真正的绘制发生在 EDT 空闲时 } Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); render(g2); } }dt是两帧之间的真实时间差用System.nanoTime()而不是currentTimeMillis()因为毫秒级精度在 16ms 一帧的尺度上误差接近 10%游戏速度会忽快忽慢。Math.min(dt, 0.05f)给帧间隔设了 50ms 上限防止窗口拖动或最小化导致某帧间隔 500ms 时钩子一瞬间穿过整个屏幕。提示Swing 的 Timer 回调跑在 EDT 上update 和 repaint 都在同一个线程里执行不需要加锁。如果换成 ScheduledExecutorService 做循环渲染就必须通过 SwingUtilities.invokeLater 切回 EDT否则画面会随机闪动甚至崩溃。2.3 双缓冲和坐标约定JPanel 默认开启双缓冲paintComponent里画的内容先写入后备缓冲再整体交换所以正常流程不会闪屏。真正导致闪烁的通常是在 paintComponent 里new BufferedImage再整张绘制每帧分配大对象或者用getGraphics()在 EDT 之外乱画。坐标约定建议写进常量类整个项目统一遵守锚点挂在顶部中央(400, 60)屏幕坐标系 x 向右、y 向下物品只生成在y ∈ [200, 540]的土层里。public final class GameConfig { public static final int CANVAS_WIDTH 800; public static final int CANVAS_HEIGHT 600; public static final int ANCHOR_X 400; public static final int ANCHOR_Y 60; public static final int GROUND_Y 540; // 物品生成区域下界 }这个约定会在碰撞检测里反复用到。屏幕坐标的 y 轴是向下的很多从数学课本带过来的标准角习惯在这里会直接把钩子带偏。先画一张坐标草图再写代码能省下半小时的调试。3. 黄金矿工的核心玩法逻辑钩子摆动、下探与抓取判定窗口和主循环就位后游戏的灵魂是钩子。这一章把钩子拆成三个可以独立测试的部分摆动模型、状态迁移、碰撞判定。3.1 摆动阶段用恒定角速度加边界反转钩子摆动在物理上是单摆角度理论上按正弦变化。但真正做游戏时很少直接用正弦因为玩家需要快速预判下一秒钩子往哪边走恒定角速度加边界反转更好读、更好调public class Hook { public static final float SWING_SPEED 100f; // 角速度度/秒 public static final float MAX_ANGLE 60f; // 与竖直方向的最大夹角 public static final float MAX_LOAD 100f; // 钩子最大负载 public float angle 0f; // 0° 竖直向下正值偏右负值偏左 public float angularSpeed SWING_SPEED; public float length 0f; // 当前绳长像素 public GoldItem carriedItem; // 当前抓到的物品null 表示空手 public HookState state HookState.SWING; public void updateSwing(float dt) { angle angularSpeed * dt; if (angle MAX_ANGLE) { angle MAX_ANGLE; angularSpeed -SWING_SPEED; } else if (angle -MAX_ANGLE) { angle -MAX_ANGLE; angularSpeed SWING_SPEED; } } }MAX_ANGLE取 60° 比 45° 更有操作空间比 75° 更容易瞄准SWING_SPEED取 100°/s 时一个完整来回约 2.4 秒每次摆到玩家侧都留出了足够的按键反应时间。钩尖的实时位置靠三角函数换算float rad (float) Math.toRadians(hook.angle); int tipX GameConfig.ANCHOR_X (int) (hook.length * Math.sin(rad)); int tipY GameConfig.ANCHOR_Y (int) (hook.length * Math.cos(rad));因为 0° 定义在竖直向下横向偏移用sin纵向延伸用cos。把这个换算单独抽成一个方法后面的碰撞、绘制、绳长显示都会用到同一份坐标避免三处各算一遍出现 1 像素偏差。3.2 用三态状态机管理钩子的行为钩子只有三种状态SWING摆动、EXTENDING下探、RETRACTING回收。任何时候只能处于一种状态状态迁移也只有四条路径摆动到出钩靠空格键下探到回收靠撞到物品或绳长到顶回收回到摆动靠绳长归零。用枚举和 switch 把结构写死public enum HookState { SWING, EXTENDING, RETRACTING } public void update(float dt) { switch (state) { case SWING - updateSwing(dt); case EXTENDING - updateExtending(dt); case RETRACTING - updateRetracting(dt); } }下探和回收的区别不只是方向速度也不同。下探快一点让玩家有出手的爽快感回收慢一点尤其带着重物时要有明显的吃力感private static final float EXTEND_SPEED 380f; // 下探速度像素/秒 private static final float RETRACT_SPEED 300f; // 空手回收速度像素/秒 private static final float MAX_LENGTH 620f; private void updateExtending(float dt) { length EXTEND_SPEED * dt; if (length MAX_LENGTH) { state HookState.RETRACTING; // 探到底空手而归 } } private void updateRetracting(float dt) { float speed RETRACT_SPEED; if (carriedItem ! null) { // 越重拉得越慢体现出物体的重量差异 speed * 1f - carriedItem.spec.weight / (2f * MAX_LOAD); speed Math.max(speed, 60f); } length - speed * dt; if (length 0f) { length 0f; if (carriedItem ! null) { score carriedItem.spec.value; // 绳长归零才算真正拿到 world.removeItem(carriedItem); carriedItem null; } state HookState.SWING; } }重量占负载一半的物品会把回收速度压到 75%占 80% 时降到 60%下限 60 像素/秒保证画面不会完全卡死。分数在绳长归零那一刻结算而不是抓到的瞬间是为了给后面的断裂规则留出条件——半路脱手的东西不能算进分数。3.3 碰撞判定先算距离再谈有没有在范围内抓取检测发生在 EXTENDING 状态检测对象是钩尖和每个物品的圆心。物品统一用圆形建模钩尖看成一个半径 6 像素的小圆两圆圆心距离小于半径之和就判定命中public boolean tryGrab(GoldItem item) { float rad (float) Math.toRadians(angle); int tipX GameConfig.ANCHOR_X (int) (length * Math.sin(rad)); int tipY GameConfig.ANCHOR_Y (int) (length * Math.cos(rad)); int dx tipX - item.x; int dy tipY - item.y; int minDist item.spec.radius HOOK_TIP_RADIUS; return dx * dx dy * dy minDist * minDist; }这里用平方距离比较省掉Math.sqrt。物品数量上限 13每帧对全部物品做一次距离检测几百次浮点乘法对现代 JVM 毫无压力不需要空间索引。抓到东西后还要过重量这一关。超过MAX_LOAD 100的物品直接判定抓取失败钩子弹回并进入短暂僵直这是原版里经典的石头惩罚private float stunTimer; // 僵直时间期间不能再次出钩 public void latch(GoldItem item) { if (item.spec.weight MAX_LOAD) { carriedItem null; stunTimer 1.2f; state HookState.RETRACTING; // 空手回收附加惩罚 } else { carriedItem item; world.removeItem(item); // 从待抓列表移除画面上它挂在钩子上 state HookState.RETRACTING; } }先抓后判重比超重直接穿透更符合直觉玩家看到钩子勾住大石头、心里一紧然后线被一点点拖回惩罚感远比石头无视钩子强。注意物品在 latch 成功那一刻就从世界移除之后回收途中不会再被别的碰撞逻辑误判。这种输入 → 判定 → 状态迁移的结构也正是 java 面试里状态模式最常见的出题场景。4. 物品生成与关卡参数金块、钻石、石头的数据建模钩子会抓东西了接下来要考虑抓什么。物品系统设计的好坏直接决定一个关卡好不好调。4.1 用配置表定义物品属性别在逻辑里写死数值一个物品需要四类信息类型决定画什么价值决定加多少分重量决定拉动难度半径决定碰撞体积。用枚举加静态配置表把它们集中定义public enum ItemType { GOLD_SMALL, GOLD_MEDIUM, GOLD_LARGE, DIAMOND, ROCK } public static class ItemSpec { public final int value; // 价值 public final float weight; // 重量 public final float radius; // 碰撞半径 public final int weightPercent; // 随机生成时的出现权重 public ItemSpec(int value, float weight, float radius, int weightPercent) { this.value value; this.weight weight; this.radius radius; this.weightPercent weightPercent; } } public static final MapItemType, ItemSpec SPECS Map.of( ItemType.GOLD_SMALL, new ItemSpec(50, 8, 12, 32), ItemType.GOLD_MEDIUM, new ItemSpec(120, 20, 18, 24), ItemType.GOLD_LARGE, new ItemSpec(200, 35, 26, 12), ItemType.DIAMOND, new ItemSpec(350, 25, 14, 18), ItemType.ROCK, new ItemSpec(0, 90, 32, 14) );weightPercent是生成时的相对权重不是最终概率后面按比例换算。这组数值里藏着设计意图大金块价值高但 35 的重量基本要单拿钻石比中金块轻 5 点、价值翻倍是划算的选择石头价值为 0 且重量 90 逼近MAX_LOAD 100是纯粹的陷阱。调平衡时改配置表一个数字就行不用翻业务代码——这就是数据与逻辑分离的最朴素版本。4.2 关卡生成策略随机但有边界还要防重叠关卡生成的核心矛盾是随机性和可玩性的平衡。完全随机可能出现全屏石头、或者几个物品叠在一起让玩家没法选。常见做法分三步数量随关卡递增、类型按权重采样、位置用碰撞检测拒绝重叠。private GoldItem createWeightedRandomItem(Random random, int level) { // 第 3 关起出现石头钻石权重随关卡提高但有上限 int rockWeight level 3 ? 14 : 0; int diamondWeight Math.min(8 level * 2, 15); ListItemType candidates new ArrayList(); for (Map.EntryItemType, ItemSpec e : SPECS.entrySet()) { ItemType type e.getKey(); int w switch (type) { case ROCK - rockWeight; case DIAMOND - diamondWeight; default - e.getValue().weightPercent; }; for (int i 0; i w; i) candidates.add(type); } return new GoldItem(candidates.get(random.nextInt(candidates.size()))); } public ListGoldItem generateLevel(int level) { ListGoldItem result new ArrayList(); int count Math.min(5 level, 13); Random random new Random(); for (int i 0; i count; i) { GoldItem item createWeightedRandomItem(random, level); for (int attempt 0; attempt 30; attempt) { item.x 80 random.nextInt(GameConfig.CANVAS_WIDTH - 160); item.y 200 random.nextInt(GameConfig.GROUND_Y - 200); if (!overlaps(item, result)) break; } result.add(item); } return result; } private boolean overlaps(GoldItem c, ListGoldItem exist) { for (GoldItem e : exist) { int dx c.x - e.x; int dy c.y - e.y; int minDist c.spec.radius e.spec.radius 8; if (dx * dx dy * dy minDist * minDist) return true; } return false; }overlaps用半径之和加 8 像素作为最小间距。30 次尝试后如果位置仍冲突就接受最后一个保证不会死循环。物品生成区间限定在y ∈ [200, 540]因为绳长上限 620、锚点在 y60再往下探就直接出画布了。每关固定 60 秒倒计时在update(dt)里递减即可。关卡难度的主要载体是权重和混装比例几张设计表如下关卡物品数石头权重钻石权重目标分160%10%500270%12%9003814%14%14004918%15%1900510-1325%封顶15%封顶每关递增 600注意物品数封顶 13 是有意为之。每帧都要对全部物品做碰撞检测数量翻了性能没问题但屏幕会显得拥挤玩家反而难以瞄准。密度控制也是可玩性的一部分。4.3 键盘输入用 Key Bindings别用 KeyListener新手项目里最常见的两个诡异 bug 是画面不动和按键没反应。后者八成是因为KeyListener要求组件先获得焦点而 JPanel 默认拿不到焦点。Swing 为此提供了更可靠的按键绑定机制 Key BindingsInputMap inputMap getInputMap(WHEN_IN_FOCUSED_WINDOW); ActionMap actionMap getActionMap(); inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), dropHook); actionMap.put(dropHook, new AbstractAction() { Override public void actionPerformed(ActionEvent e) { if (hook.state HookState.SWING hook.stunTimer 0) { hook.state HookState.EXTENDING; } } });WHEN_IN_FOCUSED_WINDOW意味着只要窗口在前台按键就生效不关心焦点在画布哪个角落。触发条件同时检查状态和僵直时间防止玩家在回收途中狂按空格导致状态抖动。KeyStroke可以直接传字符串 SPACE但显式写KeyEvent.VK_SPACE在重构和查错时更直观。第 4 关之后想加按 G 使用炸药的扩展也只需在 InputMap 里加一组 key-action业务逻辑完全不用动。5. 手感调优与排错固定步长、帧耗时统计与难度验证功能全部打通之后真正决定这个游戏好不好玩的是帧率稳定性和参数手感。下面三个点是每个 Swing 游戏项目收尾前都值得过一遍的。5.1 固定时间步长把逻辑从帧率里解放出来第二章的可变时间步长写起来简单但有个隐性缺陷逻辑更新依赖上一帧的真实耗时帧率波动会让行为不可复现。144Hz 屏幕上钩子一帧只走 2.6 像素60Hz 屏幕上每帧走 6.3 像素碰撞检测在高帧率下更容易蹭到物品边缘同一局游戏在不同机器上难度不一样。更稳妥的做法是固定时间步长逻辑永远以 1/60 秒为单位推进多余的时间攒在累加器里private static final float STEP 1f / 60f; private float accumulator; private long lastNanos; Override public void actionPerformed(ActionEvent e) { long now System.nanoTime(); float frameTime Math.min((now - lastNanos) / 1_000_000_000f, 0.1f); lastNanos now; accumulator frameTime; while (accumulator STEP) { update(STEP); // 每一轮推进严格的 1/60 秒 accumulator - STEP; } repaint(); }代价是某帧耗时超过 16ms 时一帧里会连续调用多次update。这是游戏引擎普遍采用的方案理解后再看 LWJGL 或 Unity 的循环代码会非常顺手。第二章里的dt 上限 0.05属于临时止血这里才是治本。5.2 用帧耗时统计验证性能别靠感觉调优之前先建一个能看见帧率的仪表盘。最简单的方式是每秒打印一次平均帧耗时超过 17ms 就输出慢帧日志private long frameStartNanos; private int framesThisSecond; private long accumulatedCost; void beginFrame() { frameStartNanos System.nanoTime(); } void endFrame() { long cost System.nanoTime() - frameStartNanos; accumulatedCost cost; framesThisSecond; if (accumulatedCost 1_000_000_000L) { float avgMs accumulatedCost / 1_000_000f / framesThisSecond; if (avgMs 17f) { System.out.printf(平均帧耗时 %.2f ms超过 16.67ms%n, avgMs); } accumulatedCost 0; framesThisSecond 0; } }黄金矿工的场景复杂度不高绘制很少成为瓶颈。真正值得盯的是内存分配频率——每帧在热路径里new对象多了会触发 minor GC表现就是每隔几秒卡一下。这种卡顿用上面的日志能直接抓出来。如果日志显示帧耗时稳定但画面仍然有节奏性卡顿优先检查paintComponent里有没有创建BufferedImage、Font这类重量级对象。5.3 难度曲线直接写成参数表用一局的时间验证把散落在各处的常量集中到GameConfig里附上设计依据。这既是给后来者的文档也是调参时的清单参数值设计依据MAX_ANGLE60°比 45° 有操作空间比 75° 更易瞄准SWING_SPEED100°/s完整来回约 2.4 秒留给玩家决策时间RETRACT_SPEED300px/s载重 50 时降到 225px/s吃力感明显MAX_LOAD100大石头 90 接近上限制造取舍每关时间60s单次出手约 3~4 秒支持 10~15 次尝试验证方法很朴素自己连打三关记录每次结束时的剩余时间和得分。第一关结束若剩余 20 秒以上说明目标分定低了玩家连续三次在第 3 关的时间耗尽且差 100 分左右说明目标分该降 100。参数是改出来的不是算出来的这组数据够了手感就够了。把这三处改动落到代码里之后重新从第一关跑三遍第一遍盯着剩余时间第二遍盯着慢帧日志第三遍故意去勾大石头观察回收速度和断裂时机。三个数据都合理就说明这套「爪机游戏」的节奏立住了。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

2026降AIGC工具横评:检测原理与论文修改避坑指南 2026/9/15 5:38:39

2026降AIGC工具横评:检测原理与论文修改避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
风储调频中模糊控制与Simulink建模实践 2026/9/15 5:38:39

风储调频中模糊控制与Simulink建模实践

1. 风储调频与模糊控制的背景与挑战在新能源电力系统中,风电的随机性和间歇性给电网频率稳定带来了巨大挑战。传统PID控制在应对这种非线性、时变系统时往往力不从心,而模糊控制凭借其处理不确定性的天然优势,成为了解决这一难题的利器。我曾…

阅读更多 →
WRFDA三维变分同化实战:基于Piggy_Packages环境的完整流程与调参指南 2026/9/15 5:38:39

WRFDA三维变分同化实战:基于Piggy_Packages环境的完整流程与调参指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
RDMA与AI集群网络:从内存搬运到GPU通信的关键技术解析 2026/9/15 5:38:39

RDMA与AI集群网络:从内存搬运到GPU通信的关键技术解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
C# P/Invoke调用Windows API的五大致命陷阱与避坑指南 2026/9/15 5:38:39

C# P/Invoke调用Windows API的五大致命陷阱与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Java Swing图书管理系统实战:MySQL连接、中文支持与JTable刷新 2026/9/15 5:35:39

Java Swing图书管理系统实战:MySQL连接、中文支持与JTable刷新

简介:这是一套基于Java Swing与MySQL开发的GUI图书管理系统完整工程,面向高校计算机专业学生课程设计与Java GUI开发初学者,解决双角色(学生/管理员)场景下的图书借阅、入库、读者管理等核心业务需求。资源包共184个文…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞