简介:本资源是一套面向高校计算机专业本科生的Java课程设计实践项目合集,聚焦基础编程能力训练与小型应用开发实战,特别适合作为《Java程序设计》《面向对象编程》等课程的期末综合实训材料。压缩包内含15个完整可运行的小游戏项目,涵盖贪吃蛇、俄罗斯方块、扫雷、飞机大战、五子棋等经典案例,每个项目均包含源码、编译说明及简易运行指南,开箱即用,无需额外配置。资源以RAR格式封装,共11.48MB,虽未提供具体文件总数与类型明细,但根据内容预览可知主体为.java源文件、class字节码及配套资源文件(如图片、音频),结构清晰,便于分项目学习与代码剖析。目前已有1277人下载学习,适合初学Java后需巩固语法、理解MVC结构、掌握Swing/AWT图形界面开发及事件驱动编程的学生。读者可直接导入IDE运行调试,深入理解游戏逻辑设计、线程控制、碰撞检测与用户交互实现等核心知识点。
1. 用 Java Swing 写一个能跑通、能交差、还能加点小亮点的课程设计小游戏:不是炫技,是让老师一眼看到“这学生真懂封装和事件驱动”
“Java 课程设计小游戏”——这个标题在高校计算机类专业每学期末都会密集出现,它背后不是“做个贪吃蛇就完事”的应付逻辑,而是一次对面向对象建模能力、GUI 事件响应理解、资源组织规范性的综合检验。我带过几届某高校的 Java 实训课,发现学生翻车最集中的地方,从来不是“不会画窗口”,而是:代码全塞在一个类里,图片路径硬编码到 C 盘,按下按钮没反应却死盯着 actionPerformed 方法找错,最后交上去的 jar 包双击闪退还查不出 main 方法在哪。本篇不讲 JavaFX 或 JavaFX Scene Builder 这类进阶工具,就用最基础、最稳定、JDK 8~21 全兼容的Swing + AWT 图形绘制 + 内置音频支持组合,带你从零搭出一个结构清晰、可调试、可演示、能写进报告“系统设计”章节的小游戏框架——比如一个带计分、音效、暂停/重开、难度递增逻辑的《打地鼠》简化版。它不追求像素级还原,但每个模块都对应课程大纲里的核心知识点:JFrame容器管理、JPanel双缓冲绘图、Timer定时调度、MouseListener与ActionListener分工、ImageIcon资源加载规范、AudioClip简单音效集成。你照着做,能跑;改两行参数,能换皮肤;加一个 if 判断,就能拓展新关卡。这才是课程设计该有的样子:可控、可讲、可延展,而不是玄学式拼凑。
2. 搭建可运行的最小骨架:从 JFrame 初始化到主循环 Timer 的三步落地
2.1 创建主窗口容器并设置基础属性:别让窗口一启动就“消失”在任务栏
很多学生写的第一个问题就是:程序运行了,控制台有输出,但桌面找不到窗口。根源往往在JFrame的可见性、默认关闭操作、布局管理器三者没配齐。下面这段代码是所有 Swing 游戏的起点,必须严格按顺序执行:
import javax.swing.*; import java.awt.*; public class WhackAMoleGame extends JFrame { private GamePanel gamePanel; public WhackAMoleGame() { // 1. 设置窗口标题(报告里“系统名称”就写这里) setTitle("Java课程设计:打地鼠小游戏"); // 2. 设置窗口大小(注意:这是内容区尺寸,不含标题栏边框) setSize(800, 600); // 3. 设置关闭行为:点击右上角 × 时退出 JVM(课程设计必须!) setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // 4. 设置窗口居中显示(用户体验加分项) setLocationRelativeTo(null); // 5. 禁止用户手动拉伸窗口(避免画布变形,课程设计建议固定尺寸) setResizable(false); // 6. 创建并添加游戏主面板(核心绘图区域) gamePanel = new GamePanel(); add(gamePanel); // 7. 最关键一步:setVisible(true) 必须放在所有组件添加完毕之后! setVisible(true); } public static void main(String[] args) { // Swing 线程安全要求:所有 GUI 创建必须在 Event Dispatch Thread 中执行 SwingUtilities.invokeLater(() -> new WhackAMoleGame()); } }逻辑说明:
SwingUtilities.invokeLater()是强制要求,不是可选项。它确保JFrame和其子组件都在事件分发线程(EDT)中创建,否则后续鼠标点击、键盘输入等事件可能完全不触发,或者出现随机崩溃。setResizable(false)在课程设计中强烈推荐——它规避了因窗口缩放导致Graphics坐标计算错乱的问题,也让你在报告“界面设计”部分能明确写出“采用固定分辨率 800×600,保障 UI 一致性”。
2.2 实现 GamePanel:用双缓冲解决闪烁问题,把绘图逻辑从窗口解耦
JPanel是 Swing 中最常用的自定义绘图容器。直接在paintComponent(Graphics g)中绘图会导致严重闪烁(尤其动画频繁时),必须启用双缓冲。同时,所有游戏状态(如地鼠位置、分数、游戏状态)必须定义在GamePanel类内部,而非JFrame,这是面向对象封装的第一道门槛。
import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; import java.util.Random; public class GamePanel extends JPanel implements ActionListener { private static final int PANEL_WIDTH = 800; private static final int PANEL_HEIGHT = 600; private static final int MOLE_COUNT = 9; // 3×3 地洞格子 // 游戏状态变量(全部私有!) private int score; private boolean isRunning; private Timer gameTimer; private Random random; private BufferedImage moleImage; private BufferedImage holeImage; private int[] moleX = new int[MOLE_COUNT]; private int[] moleY = new int[MOLE_COUNT]; private boolean[] isMoleUp = new boolean[MOLE_COUNT]; private long lastMoleChangeTime; public GamePanel() { // 设置面板尺寸(必须!否则布局管理器可能忽略) setPreferredSize(new Dimension(PANEL_WIDTH, PANEL_HEIGHT)); setBackground(Color.LIGHT_GRAY); setFocusable(true); // 允许接收键盘事件(如空格暂停) // 初始化资源(图片加载放在构造器,避免每次绘图都读磁盘) initResources(); // 初始化游戏状态 resetGame(); // 创建定时器:每 33ms 触发一次(约 30 FPS,课程设计足够流畅) gameTimer = new Timer(33, this); gameTimer.start(); } private void initResources() { // 使用 getClass().getResource() 加载资源,路径相对于 classpath // 假设图片放在 src/main/resources/images/ 下,打包后仍有效 try { moleImage = ImageIO.read(getClass().getResource("/images/mole.png")); holeImage = ImageIO.read(getClass().getResource("/images/hole.png")); } catch (Exception e) { // 关键容错:资源缺失时用纯色矩形替代,保证程序不崩溃 System.err.println("警告:图片资源未找到,使用占位图形"); moleImage = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = moleImage.createGraphics(); g2d.setColor(Color.RED); g2d.fillOval(0, 0, 64, 64); g2d.dispose(); } } private void resetGame() { score = 0; isRunning = true; random = new Random(); lastMoleChangeTime = System.currentTimeMillis(); // 初始化所有地鼠为“未出现”状态 for (int i = 0; i < MOLE_COUNT; i++) { isMoleUp[i] = false; } } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 启用双缓冲:创建内存图像,在内存中绘制完成后再一次性贴到屏幕 BufferedImage buffer = new BufferedImage(PANEL_WIDTH, PANEL_HEIGHT, BufferedImage.TYPE_INT_ARGB); Graphics2D g2d = buffer.createGraphics(); // 1. 绘制背景(可替换为背景图) g2d.setColor(getBackground()); g2d.fillRect(0, 0, PANEL_WIDTH, PANEL_HEIGHT); // 2. 绘制所有地洞(静态背景) drawHoles(g2d); // 3. 绘制所有地鼠(动态元素) drawMoles(g2d); // 4. 绘制分数(文本渲染) drawScore(g2d); // 5. 将内存图像一次性绘制到屏幕,消除闪烁 g.drawImage(buffer, 0, 0, null); g2d.dispose(); } private void drawHoles(Graphics2D g2d) { // 3×3 网格,每个洞宽高 120px,间距 40px,起始偏移 (100, 100) for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { int x = 100 + col * 160; int y = 100 + row * 160; if (holeImage != null) { g2d.drawImage(holeImage, x, y, 120, 120, null); } else { g2d.setColor(Color.DARK_GRAY); g2d.fillOval(x, y, 120, 120); } } } } private void drawMoles(Graphics2D g2d) { for (int i = 0; i < MOLE_COUNT; i++) { if (isMoleUp[i]) { // 地鼠坐标基于地洞中心计算(视觉更自然) int holeCol = i % 3; int holeRow = i / 3; int x = 100 + holeCol * 160 + 20; // 洞内偏移 int y = 100 + holeRow * 160 + 20; if (moleImage != null) { g2d.drawImage(moleImage, x, y, 80, 80, null); } else { g2d.setColor(Color.RED); g2d.fillOval(x, y, 80, 80); } } } } private void drawScore(Graphics2D g2d) { g2d.setColor(Color.BLACK); g2d.setFont(new Font("微软雅黑", Font.BOLD, 24)); g2d.drawString("分数: " + score, 20, 40); } @Override public void actionPerformed(ActionEvent e) { // 定时器每 33ms 调用一次,这里是游戏主逻辑入口 if (!isRunning) return; long currentTime = System.currentTimeMillis(); // 每 1.5 秒随机刷新一个地鼠状态(简单难度) if (currentTime - lastMoleChangeTime > 1500) { int index = random.nextInt(MOLE_COUNT); isMoleUp[index] = !isMoleUp[index]; // 切换出现/隐藏 lastMoleChangeTime = currentTime; } } }参数说明:
Timer(33, this):33 毫秒 ≈ 30 帧/秒,是 Swing 动画的黄金值。太小(如 10ms)CPU 占用高且无实际提升;太大(如 100ms)动画明显卡顿。课程设计选 30~40 FPS 足够。BufferedImage双缓冲:paintComponent中先在内存图像buffer上绘制所有元素,再用g.drawImage(buffer, ...)一次性刷屏。这是解决闪烁的唯一可靠方案,比setDoubleBuffered(true)更底层、更可控。getClass().getResource():绝对禁止用new File("images/mole.png")。课程设计要打包成 jar 提交,相对路径会失效。getResource()从 classpath 查找,只要图片放在src/main/resources/下,Maven/Gradle 打包后自动包含。setPreferredSize():JPanel默认尺寸为 0×0,必须显式设置,否则BorderLayout.CENTER会将其压缩为不可见。
3. 加入交互逻辑:鼠标点击响应、分数更新与音效反馈
3.1 用 MouseListener 实现精准点击判定:坐标映射到 3×3 网格
Swing 的MouseListener是处理点击的核心。关键难点在于:如何把鼠标点击的屏幕坐标(x, y),准确映射到 3×3 的地洞网格索引i?很多学生写if (x > 100 && x < 220 && y > 100 && y < 220)这种硬编码,一旦调整洞的位置就全崩。正确做法是抽象出坐标转换函数:
// 在 GamePanel 类中添加 private void initMouseHandling() { addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { // 获取鼠标点击在面板内的相对坐标(已自动排除标题栏偏移) int clickX = e.getX(); int clickY = e.getY(); // 将点击坐标转换为网格行列号 int gridCol = getGridColumn(clickX); int gridRow = getGridRow(clickY); // 验证是否在有效网格内(防止点到空白区域) if (gridCol >= 0 && gridCol < 3 && gridRow >= 0 && gridRow < 3) { int index = gridRow * 3 + gridCol; handleMoleClick(index); } } }); } private int getGridColumn(int x) { // 地洞左上角起始 X=100,每个洞宽 120,间隔 40 → 实际单元格宽 160 int relativeX = x - 100; if (relativeX < 0 || relativeX >= 3 * 160) return -1; return relativeX / 160; } private int getGridRow(int y) { // 地洞左上角起始 Y=100,每个洞高 120,间隔 40 → 实际单元格高 160 int relativeY = y - 100; if (relativeY < 0 || relativeY >= 3 * 160) return -1; return relativeY / 160; } private void handleMoleClick(int index) { if (isMoleUp[index]) { // 地鼠被敲中:加分、播放音效、重置状态 score += 10; playHitSound(); isMoleUp[index] = false; // 敲中后立即消失 } else { // 敲空:扣分(可选惩罚机制,增加策略性) score = Math.max(0, score - 5); playMissSound(); } }逻辑说明:
getGridColumn和getGridRow是核心抽象。它们把物理像素坐标转换为逻辑网格索引,彻底解耦了绘图位置和交互逻辑。如果后期要把 3×3 改成 4×4,只需改MOLE_COUNT和这两个函数的除数(160→140),其余代码零修改。mousePressed比mouseClicked更可靠——后者需要鼠标按下并释放在同一位置,而mousePressed只要按下就响应,符合“敲地鼠”的直觉操作。
3.2 集成 AudioClip 实现轻量音效:无需第三方库,JDK 自带支持
Java 的Applet.newAudioClip()是最简音效方案,无需引入javax.sound.sampled这类复杂 API。它支持.wav格式,课程设计用几个短促音效(敲中、敲空、游戏结束)完全够用,且打包进 jar 后仍可播放。
// 在 GamePanel 类中添加音效字段 private AudioClip hitSound; private AudioClip missSound; private AudioClip gameOverSound; private void initAudio() { try { // 同样使用 getResource() 加载音频文件 hitSound = Applet.newAudioClip(getClass().getResource("/audio/hit.wav")); missSound = Applet.newAudioClip(getClass().getResource("/audio/miss.wav")); gameOverSound = Applet.newAudioClip(getClass().getResource("/audio/gameover.wav")); } catch (Exception e) { System.err.println("音效加载失败:" + e.getMessage()); } } private void playHitSound() { if (hitSound != null) hitSound.play(); // 异步播放,不阻塞主线程 } private void playMissSound() { if (missSound != null) missSound.play(); } private void playGameOverSound() { if (gameOverSound != null) gameOverSound.play(); }注意:
.wav文件必须是PCM 编码、单声道、16bit、22050Hz 采样率。用 Audacity 导出时选择 “WAV (Microsoft) signed 16-bit PCM”。其他格式(如 MP3)newAudioClip不支持,会静音。课程设计音效文件体积小(每个 100KB 内),直接放在src/main/resources/audio/下即可。
3.3 添加暂停/继续与重新开始功能:用 ActionListener 统一管理按钮事件
课程设计报告常要求“具备基本游戏控制功能”。用JButton实现暂停/继续比键盘监听更直观,且ActionListener与Timer的start()/stop()天然匹配。
// 在 WhackAMoleGame 构造器中,添加按钮到窗口顶部 public WhackAMoleGame() { // ... 前面的初始化代码 ... // 创建控制面板(放在窗口顶部) JPanel controlPanel = new JPanel(new FlowLayout()); JButton pauseBtn = new JButton("暂停"); pauseBtn.addActionListener(e -> { if (gamePanel.isRunning()) { gamePanel.pauseGame(); pauseBtn.setText("继续"); } else { gamePanel.resumeGame(); pauseBtn.setText("暂停"); } }); JButton restartBtn = new JButton("重新开始"); restartBtn.addActionListener(e -> { gamePanel.resetGame(); pauseBtn.setText("暂停"); // 重置后恢复暂停按钮状态 }); controlPanel.add(pauseBtn); controlPanel.add(restartBtn); add(controlPanel, BorderLayout.NORTH); // 放在窗口顶部 // ... 后续代码 ... } // 在 GamePanel 类中添加控制方法 public void pauseGame() { isRunning = false; gameTimer.stop(); } public void resumeGame() { isRunning = true; gameTimer.start(); } public boolean isRunning() { return isRunning; }设计理由:
pauseGame()和resumeGame()将Timer的启停与游戏状态isRunning同步,避免了“暂停后地鼠还在动”的逻辑错误。JButton放在BorderLayout.NORTH是最稳妥的布局方式,不依赖复杂的GridBagLayout,课程设计够用且易解释。
4. 避坑指南:课程设计提交前必须排查的 5 个高频翻车点
4.1 现象:jar 包双击无反应,控制台也不报错
原因:main方法所在类没有被正确指定为Main-Class,或main方法签名错误(如少了static、参数名不是String[] args)。
解决:检查MANIFEST.MF文件,确保包含Main-Class: WhackAMoleGame(类名必须完整,含包名如com.example.WhackAMoleGame)。用 Maven 打包时,在pom.xml中配置maven-jar-plugin:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.3.0</version> <configuration> <archive> <manifest> <mainClass>WhackAMoleGame</mainClass> </manifest> </archive> </configuration> </plugin>4.2 现象:图片显示为一片空白或红色方块
原因:ImageIO.read()加载失败返回null,但绘图代码未做空指针检查,导致drawImage(null, ...)静默失败。
解决:在drawMoles()和drawHoles()中,所有drawImage调用前必须加if (image != null)判断,如示例代码所示。同时确认图片路径:/images/mole.png表示resources/images/mole.png,开头的/表示从 classpath 根目录开始找。
4.3 现象:鼠标点击地洞没反应,但控制台打印了坐标
原因:MouseListener添加到了JFrame而非GamePanel,或GamePanel未设置setFocusable(true)导致无法接收事件。
解决:MouseListener必须addMouseListener(...)到GamePanel实例上(即gamePanel.addMouseListener(...)),且GamePanel构造器中必须调用setFocusable(true)。JFrame本身不处理鼠标事件。
4.4 现象:游戏运行一段时间后越来越卡,CPU 占用飙升
原因:Timer未在gamePanel销毁时停止,或paintComponent中创建了大量临时对象(如反复new BufferedImage)。
解决:在GamePanel的resetGame()方法中,必须调用gameTimer.stop()并gameTimer.restart(),而非新建 Timer。同时,paintComponent中的BufferedImage buffer是局部变量,每次调用都会创建,但 Swing 会自动回收,无需手动dispose();但切勿在paintComponent中new ImageIcon()或ImageIO.read()—— 这些必须在构造器中一次性加载。
4.5 现象:打包后的 jar 双击运行,音效不播放
原因:Applet.newAudioClip()在非 Applet 环境下(如独立 jar)对某些 JDK 版本支持不稳定,或.wav文件编码格式不兼容。
解决:优先使用 JDK 11+,并确保.wav是 PCM 格式。若仍无效,降级方案:在initAudio()中捕获异常后,用Toolkit.getDefaultToolkit().beep()作为备用提示音(系统蜂鸣声,100% 兼容):
} catch (Exception e) { System.err.println("音效加载失败,使用系统蜂鸣"); // 备用:系统提示音 Toolkit.getDefaultToolkit().beep(); }5. 让课程设计脱颖而出:三个可快速实现的“加分项”技巧
5.1 用 Properties 文件管理游戏参数:让“难度调节”变成报告里的“系统可配置性”
硬编码MOLE_COUNT = 9、TIMER_DELAY = 33、SCORE_PER_HIT = 10在报告里会被批“缺乏灵活性”。用config.properties文件解耦,是体现工程素养的低成本高回报操作。
步骤:
- 在
src/main/resources/下创建config.properties:
# 游戏配置文件 game.moleCount=9 game.timerDelay=33 game.scorePerHit=10 game.scorePerMiss=-5 game.holeWidth=120 game.holeHeight=120 game.gridSpacing=40- 在
GamePanel构造器中加载:
private void loadConfig() { Properties props = new Properties(); try (InputStream is = getClass().getResourceAsStream("/config.properties")) { if (is != null) { props.load(is); MOLE_COUNT = Integer.parseInt(props.getProperty("game.moleCount", "9")); // 其他参数同理... } } catch (Exception e) { System.err.println("配置文件加载失败,使用默认值"); } }价值点:报告中可写:“系统支持外部配置文件
config.properties,通过修改参数可快速调整游戏难度与界面布局,体现了良好的可维护性与可配置性设计原则。”——这句话能让老师立刻意识到你懂软件工程。
5.2 实现简单的“难度递增”逻辑:用时间戳驱动状态变化,而非固定帧数
很多学生用frameCount % 100 == 0控制地鼠出现频率,结果帧率波动时难度忽高忽低。正确做法是用System.currentTimeMillis()计算真实流逝时间,与游戏状态绑定:
// 在 GamePanel 中添加 private long difficultyStartTime; private double baseInterval = 1500.0; // 初始间隔 1.5 秒 private void updateDifficulty() { if (difficultyStartTime == 0) { difficultyStartTime = System.currentTimeMillis(); } long elapsedSeconds = (System.currentTimeMillis() - difficultyStartTime) / 1000; // 每过 30 秒,间隔缩短 100ms(最多缩短到 500ms) double currentInterval = Math.max(500.0, baseInterval - elapsedSeconds / 30 * 100); if (System.currentTimeMillis() - lastMoleChangeTime > currentInterval) { // ... 随机切换地鼠 ... lastMoleChangeTime = System.currentTimeMillis(); } }效果:游戏越久越快,但速度变化平滑、可预测。报告中可描述为:“难度随游戏时长线性增长,符合认知心理学中的‘渐进挑战’原则,提升玩家持续参与度。”
5.3 添加“游戏结束”弹窗与分数存档:用 JOptionPane 和 FileWriter 完成闭环体验
课程设计的终点不该是窗口关闭,而应是“玩家获得反馈”。一个JOptionPane弹窗 + 本地分数记录,瞬间提升完成度。
// 在 GamePanel 中添加 private void checkGameOver() { // 示例:分数达到 100 分触发胜利 if (score >= 100) { isRunning = false; gameTimer.stop(); playGameOverSound(); String message = String.format("恭喜通关!\n最终分数:%d\n用时:%d 秒", score, (int)((System.currentTimeMillis() - difficultyStartTime) / 1000)); JOptionPane.showMessageDialog(this, message, "游戏结束", JOptionPane.INFORMATION_MESSAGE); // 保存最高分到 local_highscore.txt saveHighScore(score); } } private void saveHighScore(int score) { try (FileWriter writer = new FileWriter("local_highscore.txt", true)) { writer.write(String.format("日期:%s,分数:%d%n", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), score)); } catch (IOException e) { System.err.println("最高分保存失败:" + e.getMessage()); } }细节讲究:
FileWriter的第二个参数true表示追加模式,避免覆盖历史记录;SimpleDateFormat格式化时间,让报告截图更专业;JOptionPane使用INFORMATION_MESSAGE而非ERROR_MESSAGE,传递正向情绪。这些细节,老师在验收时一眼就能感受到你的用心。
我带过的某高校课程设计答辩中,有个学生只做了基础打地鼠,但加了这三项:配置文件、难度曲线、带时间戳的分数存档。老师当场问:“这个难度算法是你自己设计的吗?”——他如实回答“参考了游戏设计文档”,老师点点头说:“很好,有主动学习意识。” 这就是技术细节带来的信任感。课程设计不是代码越长越好,而是每个功能点都经得起追问:为什么这么设计?有没有更好方案?边界情况怎么处理?希望帮到你。
本文还有配套的精品资源,点击获取