
简介这是一份面向高校计算机相关专业学生的Java程序设计实践项目资料以经典泡泡糖游戏为选题适合用作课程设计、期末大作业或编程实训参考。项目已通过导师指导并获得97分高分评价代码结构完整、可直接运行无需二次修改即可提交使用。压缩包共包含201个文件整体约174.03MB其中35个java源文件构成游戏核心逻辑64个png与8个jpg提供界面与素材资源16个ttf字体、16个css样式及8个fxml文件支撑界面布局另有14个md文档与8个xml配置说明项目结构与运行方式并附带score.conf等配置文件用于分数记录。已有134人学习关注。读者可获得一套完整的游戏开发范例涵盖图形界面搭建、事件处理、分数统计与资源管理等模块文档说明与源码相互对照便于理解设计思路、梳理代码逻辑并快速完成课程任务。1. 泡泡糖游戏到底在练什么从 Java 程序设计实践说起很多人第一次看到「泡泡糖游戏」这个题目会以为它只是个画几个彩色圆球、点一下消掉的小玩具。真动手写才发现它把 Java 程序设计实践里最核心的几块能力全串起来了面向对象建模、集合与泛型、事件驱动、图形绘制、状态机、碰撞与匹配判定最后还要落到一份能讲清楚设计思路的文档说明。这也是为什么它常被选作课程设计或实训项目——代码量不大但每一层都能拉开差距。这篇笔记面向两类人一类是要交 Java 程序设计实践作业、想拿到 95 分以上评价的学生另一类是想用一个小项目把 Java 基础、Swing 绘图、面向对象设计重新捋一遍的转行者。我会按「先立住设计、再动手复现、最后讲坑」的顺序把泡泡糖游戏从零到能跑、能演示、能写进文档的完整路径讲清楚。你照着做至少能得到一个结构清晰、可扩展、答辩时经得起追问的版本。2. 先定架构再写代码泡泡糖游戏的领域模型怎么拆泡泡糖游戏看着简单但如果一上来就new JFrame()然后往paint()里堆逻辑写到一半必然乱。血泪经验是先把「谁是对象、谁负责什么」想清楚代码量能少三分之一后期加功能也不至于推倒重来。2.1 用面向对象把棋盘、泡泡、规则拆成三类职责泡泡糖游戏的核心对象其实只有三个层次。第一层是棋盘Board它管的是二维网格、行列数、每个格子当前放的是哪个泡泡。第二层是泡泡Bubble它管颜色、位置、是否被选中、是否处于消除状态。第三层是规则引擎MatchEngine它只做一件事给定棋盘找出所有满足消除条件的连通块。为什么要把规则单独抽出来因为消除判定是这个项目里最容易写错、也最值得写进文档的部分。把它独立成类你就能单独给它写单元测试答辩时也能说清楚「我的匹配算法复杂度是多少、边界怎么处理」。常见做法是让Board只存数据MatchEngine只读Board返回结果GamePanel负责把结果画出来。三者不互相持有对方的内部状态改起来才不牵一发动全身。具体到类设计我一般会这样定类名职责关键字段Bubble单个泡泡的颜色与状态color、row、col、selectedBoard二维网格容器与增删改查Bubble[][] grid、rows、colsMatchEngine连通块查找与消除判定minMatch最小消除数GamePanel绘制与鼠标事件board、engine、scoreGameFrame窗口与主循环入口JFrame、定时器这张表建议直接放进你的文档说明里评审一眼就能看出你有没有做设计而不是堆代码。2.2 匹配判定用 BFS 还是 DFS连通块算法的选型理由泡泡糖游戏的消除规则通常是「同色相邻上下左右且数量达到 N 个即可消除」。这就是典型的**连通块connected component**问题。选 BFS 还是 DFS我的建议是 BFS原因是它天然按层扩展不会因为递归深度过大在超大棋盘上爆栈而且用队列实现时逻辑更直观写进文档也好解释。核心思路遍历每个格子如果它还没被访问过就以它为起点做一次广度优先搜索收集所有同色相邻格子。搜索结束后如果这个集合大小 ≥minMatch就标记为待消除。// MatchEngine.java public ListListBubble findMatches(Board board) { int rows board.getRows(); int cols board.getCols(); boolean[][] visited new boolean[rows][cols]; ListListBubble result new ArrayList(); int[][] dirs {{-1,0},{1,0},{0,-1},{0,1}}; // 上下左右 for (int r 0; r rows; r) { for (int c 0; c cols; c) { if (visited[r][c]) continue; Bubble start board.get(r, c); if (start null) continue; ListBubble group new ArrayList(); Queueint[] queue new LinkedList(); queue.offer(new int[]{r, c}); visited[r][c] true; while (!queue.isEmpty()) { int[] cur queue.poll(); Bubble curBubble board.get(cur[0], cur[1]); group.add(curBubble); for (int[] d : dirs) { int nr cur[0] d[0]; int nc cur[1] d[1]; if (nr 0 || nr rows || nc 0 || nc cols) continue; if (visited[nr][nc]) continue; Bubble next board.get(nr, nc); if (next ! null next.getColor() curBubble.getColor()) { visited[nr][nc] true; queue.offer(new int[]{nr, nc}); } } } if (group.size() minMatch) { result.add(group); } } } return result; }逻辑说明外层双循环保证每个格子只作为起点被处理一次visited数组防止同一个连通块被重复统计队列里存的是坐标而不是对象避免对象引用带来的状态污染。参数说明minMatch是最小消除数量常见设为 3调大游戏更难调小更容易连消。方向数组dirs只含上下左右如果你要做「斜向也能连」的变体往这里加{-1,-1}这类偏移即可其余逻辑不用动。2.3 棋盘数据结构二维数组还是 List什么时候该换新手最容易纠结的就是「用Bubble[][]还是ListListBubble」。我的结论很直接固定行列的泡泡糖游戏用二维数组。原因是访问是 O(1)绘制时双重循环直观内存布局连续缓存友好。List的优势在动态增删但泡泡糖棋盘大小基本固定用List反而多一层装箱和边界判断。不过有一个例外如果你要做「泡泡下落、整列塌缩」这种动态效果二维数组在删除中间元素后需要手动搬移写起来啰嗦。这时可以保留二维数组作为「逻辑棋盘」另外用一个ListBubble作为「渲染列表」逻辑和渲染分离。这也是我在文档里会强调的一点数据结构服务于操作频率不是越灵活越好。// Board.java 关键片段 public class Board { private final Bubble[][] grid; private final int rows; private final int cols; public Board(int rows, int cols) { this.rows rows; this.cols cols; this.grid new Bubble[rows][cols]; fillRandom(); } public Bubble get(int r, int c) { if (r 0 || r rows || c 0 || c cols) return null; return grid[r][c]; } public void set(int r, int c, Bubble b) { grid[r][c] b; } private void fillRandom() { Random rand new Random(); Color[] palette {Color.RED, Color.BLUE, Color.GREEN, Color.YELLOW}; for (int r 0; r rows; r) { for (int c 0; c cols; c) { grid[r][c] new Bubble(palette[rand.nextInt(palette.length)], r, c); } } } }逻辑说明get里做了边界保护返回null而不是抛异常这样匹配算法里可以直接判空跳过少写 try-catch。参数说明rows、cols建议设成 8×8 或 10×10太小一局秒结束太大屏幕放不下。palette颜色数量决定难度4 色是经典配置5 色以上连通概率下降游戏会明显变难。3. 从零跑起来Swing 绘制、事件与主循环的最小实现架构定完接下来是让它真的能跑、能点、能消。这一章是纯落地每一步都能直接抄。3.1 用 JPanel 重写 paintComponent 画出棋盘和泡泡Swing 绘图的核心是重写paintComponent(Graphics g)并且第一行一定要调用super.paintComponent(g)否则会出现残影。这是新手翻车率最高的一行。// GamePanel.java Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 必须否则残影 Graphics2D g2 (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); int cell CELL_SIZE; for (int r 0; r board.getRows(); r) { for (int c 0; c board.getCols(); c) { Bubble b board.get(r, c); if (b null) continue; g2.setColor(b.getColor()); int x c * cell PADDING; int y r * cell PADDING; g2.fillOval(x, y, cell - GAP, cell - GAP); if (b.isSelected()) { g2.setColor(Color.WHITE); g2.setStroke(new BasicStroke(3f)); g2.drawOval(x, y, cell - GAP, cell - GAP); } } } }逻辑说明RenderingHints开抗锯齿泡泡边缘才不会是锯齿状。fillOval画实心圆drawOval画选中描边。参数说明CELL_SIZE是每格像素建议 5060GAP是泡泡之间的间隙留 46 像素视觉更舒服PADDING是棋盘外边距防止贴边。改这三个参数就能整体缩放不用动逻辑。3.2 鼠标点击怎么映射到行列坐标换算与选中状态点击事件里最容易错的是坐标换算。鼠标给的是像素坐标你要把它转成行列索引还要判断点是否落在泡泡圆内。// GamePanel 构造函数中注册监听 addMouseListener(new MouseAdapter() { Override public void mouseClicked(MouseEvent e) { int col (e.getX() - PADDING) / CELL_SIZE; int row (e.getY() - PADDING) / CELL_SIZE; if (row 0 || row board.getRows() || col 0 || col board.getCols()) return; Bubble b board.get(row, col); if (b null) return; b.setSelected(!b.isSelected()); repaint(); } });逻辑说明先减PADDING再整除CELL_SIZE顺序反了就会整体偏移一格。边界判断防止数组越界。repaint()触发重绘选中状态才会显示出来。参数说明如果你把CELL_SIZE改成动态计算比如根据窗口大小这里的换算公式要同步改建议抽成一个toRow(int y)、toCol(int x)方法避免两处不一致。3.3 消除、下落、补位一次完整回合的状态流转一个完整回合是选中若干泡泡 → 判定是否成组 → 消除 → 上方泡泡下落 → 顶部补新泡泡 → 重新判定连锁。这个顺序不能乱否则会出现「消完不补、补完不判」的尴尬。public void resolveTurn() { ListListBubble matches engine.findMatches(board); if (matches.isEmpty()) return; for (ListBubble group : matches) { for (Bubble b : group) { board.set(b.getRow(), b.getCol(), null); score 10; } } applyGravity(); refillTop(); repaint(); // 连锁递归或循环再判一次 resolveTurn(); }逻辑说明先收集所有匹配组再统一清除避免边清边判导致索引错乱。applyGravity让每列从下往上把非空泡泡压到底部refillTop在空位生成新泡泡。递归调用实现连锁消除但要注意加终止条件——findMatches返回空时自然结束。参数说明score 10是每消一个泡泡的得分可以按组大小给加成比如group.size() * 10文档里写清楚计分规则会加分。4. 避坑与排查泡泡糖游戏最容易翻车的 5 个地方这一章是我踩过的坑合集每条都按「现象 → 原因 → 解决」写你对照排查能省大量时间。现象一窗口最小化再恢复泡泡全花了。原因是paintComponent里没调super.paintComponent(g)旧画面没被清掉。解决永远把super.paintComponent(g)放在第一行这是 Swing 绘图的铁律。现象二点击位置总是偏一格。原因是坐标换算时先整除后减PADDING或者PADDING在绘制和点击两处取值不一致。解决把PADDING、CELL_SIZE定义成static final常量绘制和点击共用换算顺序固定为「先减后除」。现象三消除后上方泡泡不下落出现空洞。原因是只做了清除没做重力。解决applyGravity按列处理从最底行往上扫描遇到空位就把上方最近的泡泡移下来写成一个独立方法别混在消除循环里。现象四连锁消除停不下来程序卡死。原因是递归没有终止条件或者refillTop补的新泡泡又恰好成组无限循环。解决给连锁加最大层数限制比如 10 层或者补位时避免生成立即成组的颜色文档里说明这个设计取舍。现象五答辩时被问「你的匹配算法复杂度是多少」答不上来。原因是只写了代码没分析。解决BFS 遍历每个格子一次时间复杂度 O(rows × cols)空间复杂度 O(rows × cols) 用于visited。把这个写进文档评审印象分直接拉高。提示这五条里前三条是必现问题建议写完第一版就逐条自测别等到演示当天才发现。5. 把 95 分拿到手文档说明的写法与可扩展方向代码能跑只是及格线想拿到 95 分以上的评价文档说明和扩展性才是拉开差距的地方。我一般会按这个结构写文档需求分析、类设计附上面那张职责表、核心算法说明BFS 连通块 复杂度、关键代码片段、测试用例、可扩展点。测试用例部分别偷懒至少写三条正常三连消、边界单格点击、连锁消除每条写清输入和预期输出。可扩展方向是答辩加分项挑一两个真正实现的写别列一堆没做的。比如加计时模式用javax.swing.Timer每 100 毫秒刷新倒计时时间到弹结算框或者加关卡系统用配置文件定义每关的minMatch和颜色数量读进来初始化Board。这两个改动都不大但能让评审看到你的架构是真的可扩展。// 计时模式最小实现 Timer timer new Timer(1000, e - { remainingSeconds--; if (remainingSeconds 0) { ((Timer) e.getSource()).stop(); JOptionPane.showMessageDialog(this, 时间到得分 score); } repaint(); }); timer.start();逻辑说明Timer每秒触发一次递减倒计时归零时停表并弹窗。参数说明1000是毫秒间隔改成100可以做更平滑的进度条。注意Timer的回调在 EDT 线程上执行弹窗是安全的但别在里面做耗时计算。最后一个具体技巧把游戏参数全部外置成常量或配置文件。CELL_SIZE、minMatch、颜色数量、初始分数、连锁上限这些一旦散落在代码各处改一个就要全局搜。我习惯在项目根目录放一个config.properties启动时读进来文档里附上每个参数的含义和推荐值。这样评审问「难度怎么调」你直接指配置文件比翻代码有说服力得多。我自己做这类课程项目的习惯是先花半小时画类图再写匹配算法并单独测通最后才接界面。顺序反了返工是必然的。希望帮到你。本文还有配套的精品资源点击获取