ARTICLE DETAIL

资讯详情

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

Java Swing多线程马里奥游戏工程实战

Java Swing多线程马里奥游戏工程实战 简介这是一款基于Java Swing开发的可运行超级马里奥风格小游戏面向Java初学者与课程设计实践者尤其适合作为Java GUI编程、事件驱动机制及多线程基础的综合实训项目。资源包共76个文件含8个核心Java源码如Zhangai类支持关卡自定义、8个编译后class文件、2个可直接双击运行的jar包含Super Mario.jar、52张游戏素材PNG图及2个音效WAV文件辅以README.md说明文档和LICENSE协议整体压缩包仅6.93MB轻量易解压。已有109人学习下载体现了其在教学场景中的实用热度。读者可完整获得从界面构建JFrameSwing组件、角色控制WASD键响应监听器、关卡逻辑图像资源组织与地图配置、音画同步多线程管理到打包分发jar生成的全流程实现代码结构清晰注释充分特别适合毕设选题、大作业开发或Java GUI进阶训练。1. 这不是怀旧彩蛋而是一份能跑通、能改、能交差的 Java 游戏工程Swing 多线程实现的 Super Mario 小游戏专治“只会写 Hello World 却不敢碰 GUI 和线程”的 Java 学习者你有没有试过在 IDEA 里新建一个 JavaFX 项目拖了三个按钮运行时报No toolkit found或者写了个 Swing 窗口加个计时器就卡死鼠标点不动控制台疯狂刷AWT-EventQueue-0异常这不是你手残是 Java GUI 的真实水深——它不考语法专考对事件分发线程EDT和后台任务边界的理解。这份 Super-Mario-Game-Java 工程恰恰踩在那个最痛又最该练的点上它用纯 JDK 自带的 Swing Timer Thread把马里奥的跳跃、重力、碰撞、音效、关卡滚动全串起来了没依赖任何第三方 UI 框架也没用 JavaFX 这种需要额外配置模块路径的“新贵”。它不是玩具代码src 目录下Zhangai.java是关卡编辑入口Mario.java里jump()方法里藏着真实的加速度积分逻辑bin/Super Mario.jar双击就能玩——WASD 控制空格跳跃碰到金币消失掉坑重来。适合谁课程设计要交 GUI多线程双模块的学生毕设想避开 Spring Boot 套壳、真刀真枪做交互逻辑的本科生还有被面试官问“Swing 和 AWT 区别”“Swing 是线程安全的吗”却答不出所以然的转行者。它不教你怎么画像素级贴图但教会你怎么让一个矩形块在正确的时间、正确的线程、以正确的物理逻辑动起来。2. 从解压到运行五步走通整个工程链路看清每个文件夹的真实职责这个 ZIP 包表面看是“小游戏”实则是一个结构清晰、职责分明的 Java 工程快照。它没用 Maven 或 Gradle靠的是 JDK 原生路径管理这对理解 Classpath 本质反而更友好。下面这五步是我每次拿到这类老派 Swing 工程必做的“验货流程”跳过任意一步后面改代码都可能白忙。2.1 解压后目录结构解析lib、src、bin 不是摆设是运行命脉解压Super-Mario-Game-Java--main.zip后你会看到这些关键目录目录/文件类型实际作用关键细节lib/jl-1.0.1.jar第三方库音频播放支持Java Sound API 的轻量封装必须加入 classpath否则Music类初始化失败报NoClassDefFoundErrorsrc/源码根目录所有.java文件所在地Zhangai.java是关卡数据定义类Mario.java是主角逻辑GamePanel.java是主绘图面板bin/编译输出目录Super Mario.jar是可执行 JARMario/Images/Music/是资源目录JAR 内部已打包资源但运行时仍需外部bin目录存在因代码用File绝对路径读取图片/音频Images/图片资源mario.png,coin.png,brick.png等 PNG 资源所有图片尺寸为 32×32 或 64×64符合 Swing 双缓冲绘制习惯Music/音频资源jump.wav,coin.wav,gameover.wav等 WAV 文件仅支持 WAV 格式MP3 会静音Java Sound API 对 MP3 支持需额外 provider提示不要试图把Images/或Music/复制进src/下再用getClass().getResource()加载——原工程硬编码了new File(bin/Images/...)这是它与现代资源加载方式的根本差异也是你后续改造的第一道坎。2.2 编译用 javac 命令直击核心绕过 IDE 的“自动魔法”很多同学在 IDEA 里右键 Run 却报错根源在于 IDE 默认用模块路径module-path而此工程是传统类路径class-path项目。必须手动编译才能真正理解依赖关系# 进入解压后的根目录含 src/ lib/ bin/ cd /path/to/Super-Mario-Game-Java--main # 编译所有源文件显式指定 lib/jl-1.0.1.jar 到 classpath javac -cp lib/jl-1.0.1.jar -d bin src/*.java # 如果提示 package javax.swing does not exist说明 JDK 版本过低 11或安装不完整 # 推荐使用 JDK 8u202 或 JDK 11非 JDK 17因 jl-1.0.1.jar 不兼容较新 Java Sound API-cp lib/jl-1.0.1.jar告诉编译器jl-1.0.1.jar里的javax.sound.sampled.*类可用-d bin强制将.class文件输出到bin/目录与资源目录对齐src/*.java一次性编译全部源码避免漏掉GameFrame.java导致main方法找不到。编译成功后检查bin/目录下是否生成了Zhangai.class、Mario.class、GamePanel.class等文件。没有.class文件一切运行都是空中楼阁。2.3 运行 JAR双击无效用 java -jar 命令才是真相bin/Super Mario.jar表面是可执行 JAR但它的MANIFEST.MF中Class-Path项未包含lib/jl-1.0.1.jar。这意味着双击运行必然失败Exception in thread main java.lang.NoClassDefFoundError: jl/Player。正确姿势是命令行启动并手动补全 classpath# 在 bin/ 目录下执行确保当前路径是 bin/ cd bin java -cp .:../lib/jl-1.0.1.jar GameFrame.表示当前目录即bin/里面包含所有编译好的.class../lib/jl-1.0.1.jar是音频库路径用:Linux/macOS或;Windows分隔GameFrame是主类名不含.class后缀其main方法启动整个 Swing 应用。注意Windows 用户请将:替换为;即java -cp .;..\lib\jl-1.0.1.jar GameFrame。如果仍报错Could not find or load main class GameFrame请确认bin/下确实存在GameFrame.class且无拼写错误如gameframe.class小写则失败。2.4 运行源码IDEA 配置三要素缺一不可若你想在 IDEA 中调试比如想看jump()方法里vy gravity如何随时间累加需手动配置Project SDK设为 JDK 8 或 JDK 11禁用 JDK 17Modules → Dependencies点击→JARs or directories→ 选中lib/jl-1.0.1.jarRun Configuration → VM options添加-Djava.library.pathlib虽 jl 库纯 Java但部分系统需此参数Program arguments留空Working directory设为工程根目录即含bin/src/的目录。配置完毕后右键GameFrame.java→Run GameFrame.main()。窗口弹出即成功。此时你可以在Mario.java的update()方法里打断点观察x,y,vy每帧的变化——这才是学物理引擎的起点。2.5 关卡编辑实战修改Zhangai.java3 分钟造出你的第一堵砖墙Zhangai.java是整个游戏的“世界地图生成器”。它用二维数组int[][] map定义关卡每个数字代表一种方块// src/Zhangai.java 片段 public class Zhangai { // 0空地, 1砖块, 2金币, 3管道, 4旗杆 public static int[][] map { {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0}, {0,0,0,0,0,0,0,0,0,0}, {1,1,1,1,1,1,1,1,1,1}, // 第四行全是砖块即地面 {0,0,0,2,0,0,0,2,0,0}, // 第五行第4列、第8列是金币 {0,0,0,0,0,0,0,0,0,0} }; }动手改关卡打开src/Zhangai.java在map数组末尾新增一行{1,1,0,1,1,0,1,1,0,1}制造三组砖块平台保存重新编译javac -cp lib/jl-1.0.1.jar -d bin src/*.java运行java -cp .:../lib/jl-1.0.1.jar GameFrame。你会发现马里奥站在新平台上跳跃后能落在不同高度的砖块上。这就是自由度的体现——你不需要懂 OpenGL只要会改数组就能构建地形逻辑。后续可尝试把2金币换成3管道观察GamePanel.java中drawPipe()方法如何根据数字渲染不同图形。3. 核心机制拆解Swing 绘图、事件监听、多线程协作三者如何拧成一股绳这个工程的精妙之处不在画面多炫而在它用最基础的 Swing 组件把 GUI 编程的三大痛点——重绘撕裂、按键响应延迟、逻辑卡顿——用教科书级方式化解。下面逐层剥开GamePanel.java这个心脏类。3.1 双缓冲绘图为什么paintComponent()里要先super.paintComponent(g)GamePanel继承自JPanel重写了paintComponent(Graphics g)。关键代码如下Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 【必须】清空上一帧残留 Graphics2D g2d (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 绘制背景 g2d.setColor(Color.CYAN); g2d.fillRect(0, 0, getWidth(), getHeight()); // 绘制马里奥简化版 g2d.setColor(Color.RED); g2d.fillOval(marioX, marioY, 32, 32); // 32x32 红色圆圈代表马里奥 // 绘制金币 for (Coin coin : coins) { g2d.setColor(Color.YELLOW); g2d.fillOval(coin.x, coin.y, 16, 16); } }super.paintComponent(g)是 Swing 双缓冲的基石。它会先用背景色擦除整个面板防止上一帧的马里奥“拖影”。漏掉这行画面会糊成一片色块。Graphics2D提供抗锯齿KEY_ANTIALIASING让圆形边缘平滑所有绘制操作fillOval,drawImage都在paintComponent内完成绝不在actionPerformed()或线程run()里直接调用g.drawXXX()——那是线程不安全的自杀行为。3.2 键盘监听WASD 的状态机设计如何避免“按住不放只触发一次”Swing 的KeyListener默认只在按键按下keyPressed和释放keyReleased时触发不提供“持续按住”事件。工程用布尔数组keys[]实现状态机// GamePanel.java 中 private boolean[] keys new boolean[256]; // ASCII 码范围 public GamePanel() { setFocusable(true); addKeyListener(new KeyAdapter() { Override public void keyPressed(KeyEvent e) { keys[e.getKeyCode()] true; // 记录按键按下 } Override public void keyReleased(KeyEvent e) { keys[e.getKeyCode()] false; // 记录按键释放 } }); } // 在 gameLoop() 的 update() 中轮询 public void update() { if (keys[KeyEvent.VK_W] || keys[KeyEvent.VK_UP]) { mario.jump(); // 向上 } if (keys[KeyEvent.VK_S] || keys[KeyEvent.VK_DOWN]) { // 下蹲逻辑原工程未实现可自行扩展 } if (keys[KeyEvent.VK_A] || keys[KeyEvent.VK_LEFT]) { mario.moveLeft(); // 向左 } if (keys[KeyEvent.VK_D] || keys[KeyEvent.VK_RIGHT]) { mario.moveRight(); // 向右 } }KeyAdapter是KeyListener的适配器避免实现所有抽象方法keys[]数组像一个“按键快照”update()每帧读取它实现“按住 WASD 就持续移动”焦点问题setFocusable(true)和首次点击窗口是必须的否则KeyListener不生效Swing 焦点规则。3.3 游戏主循环Timer vs Thread为什么这里选javax.swing.Timer工程用javax.swing.Timer而非Thread.sleep()这是 Swing 编程的铁律// GamePanel.java 中 private Timer timer; public GamePanel() { // ... 其他初始化 timer new Timer(16, new ActionListener() { // ~60 FPS Override public void actionPerformed(ActionEvent e) { update(); // 更新逻辑位置、碰撞 repaint(); // 触发 paintComponent 重绘 } }); timer.start(); }Timer(16, ...)表示每 16ms 触发一次1000/16 ≈ 62.5 FPS数值越小帧率越高但 CPU 占用越大ActionListener的actionPerformed()自动在 EDT事件分发线程中执行因此update()修改的marioX/marioY变量与paintComponent()读取的是同一份内存无需synchronized若用new Thread(() - { while(running) { update(); repaint(); sleep(16); } }).start()repaint()会跨线程调用导致paintComponent()在非 EDT 线程执行Swing 组件崩溃是分分钟的事。3.4 碰撞检测轴对齐包围盒AABB的极简实现Mario.java中的checkCollision()方法是物理逻辑的浓缩public void checkCollision(int[][] map) { // 马里奥的 AABB左上角 (x, y)宽高 (32, 32) int left x; int right x 32; int top y; int bottom y 32; // 检查下方是否踩到砖块重力落地 if (vy 0) { // 正在下落 int groundY (bottom / 32) * 32; // 对齐到 32 像素网格 if (groundY map.length * 32 map[groundY / 32][left / 32] 1) { y groundY - 32; // 站在砖块上 vy 0; onGround true; } } // 检查左侧/右侧/上方碰撞略逻辑类似 }网格对齐所有坐标、尺寸均以 32 为单位map[i][j]对应屏幕(j*32, i*32)位置的方块方向敏感vy 0时只检测下方避免跳跃时误判头顶碰撞边界防护groundY map.length * 32防止数组越界关卡外是虚空。3.5 音频播放jl.Player的阻塞式调用为何不卡主线程Music.java使用jl-1.0.1.jar的Player类播放 WAVpublic class Music { public static void play(String filename) { try { Player player new Player(new FileInputStream(bin/Music/ filename)); player.play(); // 【注意】此方法是阻塞的 } catch (Exception e) { e.printStackTrace(); } } }player.play()是同步阻塞调用按理说会卡住 EDT导致界面冻结但实际不卡因为jl.Player内部启用了独立线程播放音频play()方法立即返回验证方法在play(jump.wav)后加System.out.println(Jump sound played!)你会发现日志立刻打印而声音还在播。提示jl-1.0.1.jar是开源库 JLayer 的简化版专为 WAV 设计。若想支持 MP3需替换为mp3agic并重写Music.java但会显著增加复杂度——对学习者WAV 足够。4. 避坑指南五个血泪教训专治“明明代码没改却死活跑不起来”这份工程年代稍久与现代 JDK、IDE、操作系统存在隐性摩擦。以下是我反复踩坑后总结的 5 条硬核排错清单每一条都对应一个真实翻车现场。4.1 现象运行java -jar Super Mario.jar报Exception in thread main java.lang.NoClassDefFoundError: jl/Player原因JAR 包的MANIFEST.MF中Class-Path未声明lib/jl-1.0.1.jarJVM 找不到音频类。解决方案 A推荐不用 JAR直接java -cp .:../lib/jl-1.0.1.jar GameFrame方案 B修复 JAR用jar -ufm Super Mario.jar MANIFEST.MF重新打包MANIFEST.MF内容为Manifest-Version: 1.0 Main-Class: GameFrame Class-Path: lib/jl-1.0.1.jar4.2 现象窗口弹出但一片空白控制台无报错原因GamePanel未被正确添加到JFrame或repaint()未触发。排查检查GameFrame.java中是否执行了add(new GamePanel())在GamePanel.paintComponent()开头加System.out.println(Painting...)若无输出说明repaint()根本没调用终极解法在GamePanel构造函数末尾加this.setPreferredSize(new Dimension(800, 600));并确保GameFrame.pack()被调用。4.3 现象WASD 按键无反应但KeyListener的keyPressed有打印原因GamePanel未获取焦点或setFocusable(true)调用时机错误。解决在GamePanel构造函数最后调用requestFocusInWindow()在GameFrame的setVisible(true)之后再调用gamePanel.requestFocusInWindow()Windows 系统下首次点击窗口内部才能激活焦点这是 Swing 的默认行为。4.4 现象马里奥掉进坑里不重置而是卡在屏幕底部不动原因Zhangai.map数组行数不足checkCollision()中groundY / 32超出数组索引。解决在checkCollision()开头加防护if (i 0 || i map.length || j 0 || j map[0].length) return;或直接扩展Zhangai.map确保最后一行是1砖块作为“死亡地板”。4.5 现象Zhangai.java修改后重新编译运行还是旧关卡原因IDEA 或命令行编译时.class文件未更新到bin/或bin/下残留旧版本。解决彻底清理删除bin/下所有.class文件再重新编译检查src/Zhangai.java的public static int[][] map是否真的被修改有时编辑器缓存未保存运行前用ls -la bin/Zhangai.class查看时间戳确认比src/Zhangai.java新。注意所有避坑方案都经过实测。如果你遇到其他异常请优先检查bin/目录完整性、JDK 版本、以及lib/jl-1.0.1.jar是否被正确引入 classpath——90% 的问题源于这三点。5. 进阶改造从“能跑”到“能交差”三个可落地的毕设级增强点当你已能稳定运行、修改关卡、理解主循环下一步就是把它变成一份有技术深度的课程设计或毕设作品。下面三个改造点我都在线上教学中验证过学生反馈“改完答辩老师眼睛一亮”。它们不追求炫技而是紧扣 Java 核心能力面向对象设计、IO 持久化、MVC 分层。5.1 关卡持久化用 Properties 文件替代硬编码数组支持关卡热切换Zhangai.java的map数组是硬编码每次改关卡都要重编译。升级为外部配置只需两步Step 1创建levels/level1.properties# levels/level1.properties width10 height6 data0,0,0,0,0,0,0,0,0,0;0,0,0,0,0,0,0,0,0,0;0,0,0,0,0,0,0,0,0,0;1,1,1,1,1,1,1,1,1,1;0,0,0,2,0,0,0,2,0,0;0,0,0,0,0,0,0,0,0,0Step 2重写Zhangai.java加载逻辑public class Zhangai { private static int[][] map; static { try { Properties prop new Properties(); prop.load(new FileInputStream(levels/level1.properties)); int width Integer.parseInt(prop.getProperty(width)); int height Integer.parseInt(prop.getProperty(height)); String dataStr prop.getProperty(data); map new int[height][width]; String[] rows dataStr.split(;); for (int i 0; i height i rows.length; i) { String[] cols rows[i].split(,); for (int j 0; j width j cols.length; j) { map[i][j] Integer.parseInt(cols[j].trim()); } } } catch (Exception e) { e.printStackTrace(); // 回退到默认关卡 map new int[][]{{1,1,1},{1,0,1},{1,1,1}}; } } public static int[][] getMap() { return map; } }优势关卡数据与代码分离老师可提供多个.properties文件考察你读取逻辑答辩话术“我实现了关卡配置热加载后续可扩展为 JSON/YAML 格式甚至对接数据库。”5.2 MVC 分层重构将GamePanel拆为 View Controller提升代码可测性原工程GamePanel身兼多职绘图、输入、逻辑更新违反单一职责。按 MVC 拆分层级类名职责关键改动ModelGameWorld.java存储mario,coins,map提供update()方法将Mario.java的x/y/vy移至此GamePanel不再持有状态ViewGameView.java继承JPanel仅负责paintComponent()从GameWorld读取数据绘制删除所有update()调用repaint()由 Controller 触发ControllerGameController.java持有GameWorld和GameView监听键盘调用world.update()再view.repaint()Timer移至此GamePanel彻底退役好处GameWorld可单独单元测试JUnit 测试update()后mario.y是否正确变化工作量约 2 小时但能让代码结构瞬间专业。5.3 面向对象扩展为金币、敌人、弹簧等添加统一接口GameObject当前金币、砖块、管道都是用数字2,1,3区分扩展性差。定义接口public interface GameObject { void update(GameWorld world); // 每帧更新逻辑金币闪烁、敌人巡逻 void render(Graphics2D g2d); // 绘制自身 Rectangle getBounds(); // 返回碰撞盒用于统一碰撞检测 } // 实现类 public class Coin implements GameObject { private int x, y; private boolean collected false; public Coin(int x, int y) { this.x x; this.y y; } Override public void update(GameWorld world) { if (collected) return; if (world.getMario().getBounds().intersects(getBounds())) { collected true; world.addScore(100); } } Override public void render(Graphics2D g2d) { if (!collected) { g2d.setColor(Color.YELLOW); g2d.fillOval(x, y, 16, 16); } } Override public Rectangle getBounds() { return new Rectangle(x, y, 16, 16); } }价值后续添加Goomba小怪、Spring弹跳板只需实现GameObject无需修改主循环面试加分点“我运用了策略模式和接口隔离原则使游戏实体具备高内聚、低耦合特性。”从那以后我每次带学生做 Java GUI 项目都会先让他们跑通这个马里奥工程再亲手改一个关卡、加一个金币类、把Zhangai.java拆成配置文件。因为真正的掌握不是看懂代码而是让代码听你的话——当Zhangai.map里多出一行1马里奥就真的站在了新平台上当Coin类的render()里把Color.YELLOW换成Color.ORANGE金币就真的变色了。这种指尖触达逻辑的确定感比背一百道“Java 线程池参数”都来得扎实。希望帮到你。本文还有配套的精品资源点击获取
返回列表