Java迷宫游戏设计与实现:递归回溯算法与Swing界面实战
简介这份基于JAVA的迷宫游戏毕业设计源码包完整覆盖从迷宫生成到GUI交互的典型开发流程适合Java初学者、毕业设计学生及后端开发爱好者用于理解面向对象设计、经典算法应用与桌面应用界面搭建。包内共11个文件包含4个Java核心源文件如Maze、Player、GameLoop、GUI、4个GIF演示动图、1份课程设计报告doc、1份README说明及License信息压缩包仅460KB轻量便于快速下载与部署。已有265人学习可用于课程设计参考或游戏开发入门实践。通过源码和报告可掌握深度优先或Prim等迷宫生成算法、碰撞检测、游戏主循环等关键实现并结合动图直观对比运行效果。1. 拿到这份迷宫游戏源码能不能直接当毕设交先给结论基于 JAVA 的迷宫游戏设计与实现是 Java 课程设计和本科毕业设计里最稳妥的选题之一。它不依赖第三方框架不碰数据库不涉及网络编程核心就是算法和 GUI正好卡在「有复杂度但可控」的位置上。你手上这份完整源码如果结构清晰、能跑通完全可以作为主程序直接提交也可以抽出其中一两块做二次开发再写成论文。但「能跑」和「能答辩」是两回事。迷宫游戏这类项目的翻车点通常不在算法而在边界处理、界面卡死、生成算法选错、源码结构混乱。这篇文章我会从迷宫生成算法讲起到 Swing 界面落地再到运行时踩坑和答辩加分项全部按可复现的步骤来。适合两类人一是 Java 基础比较薄、想找一份能看懂的完整源码做底子的新手二是已经能写单文件程序、但没做过完整项目拆分想搞明白游戏主循环怎么和界面配合的进阶读者。2. 迷宫怎么生成的从递归回溯到 Prim 的选型与核心实现2.1 两种主流生成算法怎么选递归回溯和 Prim 的区别迷宫游戏的核心不是画界面而是「迷宫地图从哪来」。如果地图是写死的二维数组那这个项目基本没有毕业设计该有的技术含量答辩老师一问生成过程就露馅。所以完整源码里一定包含迷宫生成算法最常见的两种是递归回溯和随机 Prim。递归回溯Recursive Backtracking本质是深度优先搜索的随机化版本从起点出发随机打乱方向往没走过的格子掘进走投无路就原路退回。它的特点是生成的迷宫「通路少但长」从入口到出口往往只有一条主路加上一些短分叉玩家走起来感觉像是在绕远路。代码短理解成本低是本人最推荐放在论文第 3 章重点讲的一种。随机 Prim 的颜色是广度优先思路维护一个候选墙列表随机选一堵墙打通让迷宫扩张。它生成的迷宫分支更多、更均匀视觉上更像传统迷宫但实现时要维护一个边集合代码比递归回溯稍长一点。选型建议很简单如果你的论文主题是「算法设计与实现」用递归回溯好讲、好画图、好证明如果你想让迷宫看起来更复杂、截图效果更饱满用随机 Prim但要在论文里把它与递归回溯做一个对比表格这样反而成了加分项。二者生成的都是完美迷宫即任意两个格子之间有且仅有一条路径这是迷宫算法的标准结论答辩时会被问到先记住。2.2 递归回溯生成器的 Java 实现一个类搞定地图数据我一般会把迷宫的数据结构和生成逻辑单独放到一个MazeGenerator类里不跟界面混在一起。先看核心代码package com.maze.game.model; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; /** * 迷宫生成器递归回溯算法。 * 地图用二维数组表示cell[x][y] 为 true 表示该格子已访问/可通行。 */ public class MazeGenerator { private boolean[][] visited; // 是否已访问 private boolean[][] rightWalls; // 格子右墙true 表示有墙 private boolean[][] bottomWalls; // 格子下墙true 表示有墙 private int rows, cols; private Random random; public MazeGenerator(int rows, int cols, long seed) { this.rows rows; this.cols cols; this.random new Random(seed); visited new boolean[rows][cols]; rightWalls new boolean[rows][cols]; bottomWalls new boolean[rows][cols]; // 初始时所有格子四周都有墙因此右墙和下墙初始化全为 true for (int i 0; i rows; i) { for (int j 0; j cols; j) { rightWalls[i][j] true; bottomWalls[i][j] true; } } generate(0, 0); } private void generate(int x, int y) { visited[x][y] true; // 四个方向右、下、左、上每次随机打乱顺序 Integer[] directions {0, 1, 2, 3}; ListInteger dirs new ArrayList(List.of(directions)); Collections.shuffle(dirs, random); for (int dir : dirs) { int nx x, ny y; if (dir 0 y 1 cols) ny y 1; // 右 else if (dir 1 x 1 rows) nx x 1; // 下 else if (dir 2 y - 1 0) ny y - 1; // 左 else if (dir 3 x - 1 0) nx x - 1; // 上 else continue; if (!visited[nx][ny]) { // 打通当前格子与目标格子之间的墙 if (nx x ny y 1) rightWalls[x][y] false; // 拆右墙 else if (nx x 1 ny y) bottomWalls[x][y] false; // 拆下墙 else if (nx x ny y - 1) rightWalls[x][y - 1] false; // 拆左边格子的右墙 else if (nx x - 1 ny y) bottomWalls[x - 1][y] false; // 拆上面格子的下墙 generate(nx, ny); } } } public boolean isRightWallOpen(int x, int y) { return !rightWalls[x][y]; } public boolean isBottomWallOpen(int x, int y) { return !bottomWalls[x][y]; } public int getRows() { return rows; } public int getCols() { return cols; } }这里有一个非常关键的约定我没有用标准的boolean[][] wall四方向数组而是只用rightWalls和bottomWalls两个数组。因为每堵墙属于相邻两个格子中的左边那个或上边那个用两个数组天然避免重复记录同时省掉一倍的存储。刚开始写迷宫的人最容易踩的坑就是想当然地用wall[4][x][y]结果拆墙时上下左右逻辑混乱后面我会在避坑章节展开。参数说明构造器的rows和cols决定迷宫规模新手先跑new MazeGenerator(15, 15, System.currentTimeMillis())体验效果。seed这个参数是隐藏答辩加分点——给它传固定值每次生成的迷宫一样方便调试和写测试传System.currentTimeMillis()每次都是新迷宫。考场演示时建议传当前时间戳显得真实。2.3 生成结果怎么自检用 BFS 验证迷宫可达性迷宫生成完不能直接拍脑袋说没问题。我见过的血泪经验是算法写错了但界面画出来看着像迷宫直到玩家一走到死角才发现整个地图根本不通。所以在写界面之前先写一个 BFS 连通性验证package com.maze.game.util; import java.util.LinkedList; import java.util.Queue; public class MazeValidator { /** * 从(0,0)出发验证(x,y)是否可达。 * 返回 true 表示迷宫从入口到出口连通。 */ public static boolean isReachable(MazeGenerator maze, int targetX, int targetY) { int rows maze.getRows(); int cols maze.getCols(); boolean[][] visited new boolean[rows][cols]; Queueint[] queue new LinkedList(); queue.offer(new int[]{0, 0}); visited[0][0] true; int[][] delta {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; while (!queue.isEmpty()) { int[] pos queue.poll(); int x pos[0], y pos[1]; if (x targetX y targetY) { return true; } // 依次检查右、下、左、上四个方向的墙是否通 for (int[] d : delta) { int nx x d[0], ny y d[1]; if (nx 0 || nx rows || ny 0 || ny cols) continue; if (visited[nx][ny]) continue; // 拆墙判断必须和生成逻辑一致 if (d[0] 0 d[1] 1 maze.isRightWallOpen(x, y)) { visited[nx][ny] true; queue.offer(new int[]{nx, ny}); } else if (d[0] 1 d[1] 0 maze.isBottomWallOpen(x, y)) { visited[nx][ny] true; queue.offer(new int[]{nx, ny}); } else if (d[0] 0 d[1] -1 maze.isRightWallOpen(x, y - 1)) { visited[nx][ny] true; queue.offer(new int[]{nx, ny}); } else if (d[0] -1 d[1] 0 maze.isBottomWallOpen(x - 1, y)) { visited[nx][ny] true; queue.offer(new int[]{nx, ny}); } } } return false; } }这段代码的作用是在写任何界面代码之前先用纯逻辑验证迷宫左下角入口到右上角出口是否连通。我在手把手带选题时反复强调一句话算法类项目验证代码比业务代码更容易拿答辩分。你把MazeValidator的测试结果截图放进论文里再写一句「经 BFS 验证迷宫可达性为 100%」评审老师基本不会再追问生成算法的正确性。参数说明delta数组定义的是四方向移动向量顺序无所谓但每个方向的拆墙判断必须和生成器里的一致——右移看当前格子右墙是否开下移看当前格子下墙是否开左移看左侧格子的右墙上移看上方格子的下墙。这个一致性是整个算法最脆弱的环节改任何一边都要同步改另一边。3. 把它跑起来游戏主循环与 Swing 界面的最小实现3.1 从 MainFrame 到 GamePanel 的结构划分迷宫生成的底层逻辑做好了接下来是让玩家能实际玩起来。JAVA 做桌面小游戏最常见的 GUI 选型就是 Swing即便已经有很多新学生在学 JavaFX对毕设来说 Swing 仍然是投入回报比最高的选择JDK 自带不需要额外配置 maven 依赖答辩现场用一台装了 JDK 的电脑就能跑。完整的源码一般会拆成至少三个类MainFrame窗口、GamePanel画布和游戏主循环、Player玩家状态。我强烈建议你不要把这三个塞成一个类哪怕答辩老师不看代码自己之后调试也会疯掉。package com.maze.game.ui; import javax.swing.JFrame; public class MainFrame extends JFrame { public MainFrame(int rows, int cols) { setTitle(基于 JAVA 的迷宫游戏); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); GamePanel panel new GamePanel(rows, cols); add(panel); pack(); // 根据面板的 preferred size 自动调窗口大小 setLocationRelativeTo(null); // 居中显示 } }这里pack()是新手经常漏掉的方法。如果不调用它窗口尺寸可能不会按GamePanel的getPreferredSize()计算结果面板被拉伸或者出现白边。还有一个细节setResizable(false)不写的话玩家拖动窗口放大后迷宫不会跟着缩放画面会错位不如直接禁掉。GamePanel是重头戏它不只是绘图还承担了游戏主循环的角色。Swing 里做循环不能用while(true)死磕那会直接把事件分发线程EDT卡死窗口立刻变成「无响应」白框。正确做法是用javax.swing.Timerpackage com.maze.game.ui; import javax.swing.JPanel; import javax.swing.Timer; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; public class GamePanel extends JPanel implements ActionListener { private final int cellSize 30; // 每格像素大小 private final Timer timer; public GamePanel(int rows, int cols) { setPreferredSize(new Dimension(cols * cellSize, rows * cellSize)); setBackground(Color.WHITE); setFocusable(true); // 关键让面板能响应键盘 // 每 100ms 触发一次重绘 timer new Timer(100, this); timer.start(); } Override public void actionPerformed(ActionEvent e) { // 更新游戏状态移动、计步、胜负判定 repaint(); } Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 绘制迷宫和玩家下一小节实现 } }Timer的第一个参数是刷新间隔单位毫秒。对于迷宫游戏这种低速帧率场景100ms 完全够用也就是每秒 10 帧。不要贪心调到 16ms60 帧迷宫游戏没有高速运动对象调高反而增加 CPU 占用。这个paintComponent是 Swing 的官方绘制入口记得用protected修饰符和super.paintComponent(g)开头否则界面会出现残留残影的黑匣子问题。3.2 键盘控制与碰撞检测让玩家真正走进迷宫光有定时器还不能操作游戏。键盘监听是迷宫游戏里最容易翻车的环节因为KeyListener必须注册到「当前获得焦点的组件」上而JFrame默认的焦点在内容面板上直接给GamePanel加监听经常按了没反应。解决手段就一行字setFocusable(true)然后在MainFrame构造器末尾加panel.requestFocusInWindow()。Override public void actionPerformed(ActionEvent e) { // 键盘方向移动dx, dy 在 keyPressed 里更新 switch (direction) { case UP - { if (canPass(playerX, playerY, -1, 0)) playerX--; } case DOWN - { if (canPass(playerX, playerY, 1, 0)) playerX; } case LEFT - { if (canPass(playerX, playerY, 0, -1)) playerY--; } case RIGHT - { if (canPass(playerX, playerY, 0, 1)) playerY; } } repaint(); } private boolean canPass(int x, int y, int dx, int dy) { int nx x dx, ny y dy; if (nx 0 || nx rows || ny 0 || ny cols) return false; // 越界不可走 // 和前面 BFS 验证一致的拆墙判断 if (dx 0 dy 1) return maze.isRightWallOpen(x, y); // 向右 if (dx 1 dy 0) return maze.isBottomWallOpen(x, y); // 向下 if (dx 0 dy -1) return maze.isRightWallOpen(x, y - 1); // 向左 if (dx -1 dy 0) return maze.isBottomWallOpen(x - 1, y); // 向上 return false; }这段碰撞检测的逻辑和 2.3 节 BFS 验证里的拆墙判断完全同构这是一个非常值得在论文里点出的设计游戏移动规则和算法验证规则共用同一套墙状态查询只要改一处两处同步生效。答辩时讲到这里老师会认为你对「单一数据源」有概念比单纯贴代码的陈述高出一个段位。playerX、playerY我用 int 存储玩家所在格子坐标真正的像素位置由playerX * cellSize计算。不要为了画图方便直接拿像素坐标做碰撞检测那样会出现玩家走到墙里一半的图面 bug。3.3 计时计步与通关判定答辩演示最出效果的模块迷宫游戏如果只有走来走去演示时很干。加一个「计时 计步 通关弹窗」答辩现场效果立刻不一样。计步很简单每次成功移动到新格子就steps。计时就用Timer本身在actionPerformed里维护一个elapsedTime变量注意不要新建Thread.sleep来计时那会再次卡死 EDT。private int steps 0; private int elapsedSeconds 0; private boolean gameOver false; Override public void actionPerformed(ActionEvent e) { if (gameOver) { timer.stop(); return; } // 每触发一次代表 100ms累积到 1000ms 加一秒 elapsedSeconds; if (elapsedSeconds % 10 0) { // 把秒数显示在窗口标题栏或者面板下方 updateStatusLabel(); } // 检查是否到达出口 if (playerX exitX playerY exitY) { gameOver true; timer.stop(); JOptionPane.showMessageDialog(this, 恭喜通关用时 (elapsedSeconds / 10) 秒步数 steps); } repaint(); }这个模块的代码量不大但它是从「简单迷宫」到「完整游戏」的关键分水岭。建议在JFrame底部加一个JLabel实时显示「时间 / 步数」比只在弹窗里展示更直观。出口的判定条件要把出口坐标设置在(rows - 1, cols - 1)和入口(0, 0)正好对角线视觉上也最合理。再加一个小建议通关弹窗别用JOptionPane的默认 OK 框把标题改成「迷宫游戏 - 通关」内容分成两行显示时间与步数。这个细节虽然技术含量不高但演示时截图提论文里会显得你认真做了产品而不是只交了作业。4. 看懂并改造这份完整源码目录结构、替换参数与复用策略4.1 一份标准的毕设迷宫源码长什么样现在把视角从「自己从零写」切换到「我拿到一份陌生源码怎么快速看懂」。一份合格的基于 JAVA 的迷宫游戏毕设源码目录结构通常遵循最朴素的包分层不追求过度设计src/ com/maze/game/ Main.java // 程序入口 model/ MazeGenerator.java // 迷宫生成 Player.java // 玩家位置与步数 ui/ MainFrame.java // 窗口 GamePanel.java // 画布、事件、循环 util/ MazeValidator.java // BFS 可达性验证 resources/ images/ // 可选玩家/出口图标 README.md // 运行说明看源码有个顺序技巧先读Main.java找入口再读MainFrame看窗口装配了哪些组件然后进GamePanel看actionPerformed这个主循环最后回到MazeGenerator看数据怎么生成。如果这份源码连Main入口都没有只能在 IDE 里右键某个类单独运行那它的结构是不合格的你要么选个更规整的版本要么自己补一个入口类。真正的「完整源码」包里还应该包含一份运行说明文档写明 JDK 版本要求一般 Java 8 及以上都能跑如果用了 switch 箭头表达式比如本文代码里的-则需要 Java 14 以上、IDE 导入步骤和主类路径。如果你拿到的包没有 README花 20 分钟补一份答辩时主动展示「工程化意识」这句经验值回票价。4.2 把迷宫改成你要的样子这 3 个入口是核心拿到源码之后大部分人不是从头复写而是想改参数换效果。迷宫相关的改动入口其实只有三个规模、造型、难度。规模在new MainFrame(rows, cols)处改行列数。普通毕设演示用 15×15 已经够看想截图显得迷宫复杂可以给GamePanel的构造器传 25×25。但是注意格子数变多后cellSize也要相应缩小否则窗口超出屏幕。我通常让cellSize根据屏幕分辨率动态计算Math.min(30, 800 / cols)这样 15 和 25 都能完整显示。造型如果源码用的是递归回溯你想体验 Prim 生成的分叉迷宫就把 2.1 节的随机 Prim 实现替换MazeGenerator内部的generate方法。对外接口isRightWallOpen(row, col)和isBottomWallOpen(row, col)完全不用变GamePanel零改动直接适配。难度把出口判定从「走到终点格子」改成「走到终点格子且步数小于等于 N」或「时间少于 T 秒」这不加算法复杂度但论文里可以多写一节「关卡难度设计」这正好是毕设评分表里被单独列出来的指标。4.3 增量开发给迷宫加入入口出口动画不从零写而是在现有源码上做增量是最安全的毕设策略。这里给一个性价比极高的增量例子入口出现时绘制一个闪烁的光标出口绘制一个小旗帜。改动只集中在paintComponentOverride protected void paintComponent(Graphics g) { super.paintComponent(g); // 绘制迷宫墙体省略具体线条绘制 drawMaze(g); // 绘制出口菱形标志 g.setColor(Color.RED); int exitPx exitY * cellSize cellSize / 2; int exitPy exitX * cellSize cellSize / 2; g.fillRect(exitPx - cellSize / 4, exitPy - cellSize / 4, cellSize / 2, cellSize / 2); // 绘制玩家圆形便于和墙体区分 g.setColor(Color.BLUE); int px playerY * cellSize cellSize / 2; int py playerX * cellSize cellSize / 2; g.fillOval(px - cellSize / 3, py - cellSize / 3, cellSize * 2 / 3, cellSize * 2 / 3); }注意这里playerX是行号对应绘制时的 y 方向像素playerY是列号对应 x 方向像素。这就是 2.1 节埋下的伏笔——如果之前把行列搞混这里绘制出来的玩家和迷宫会整体转置。增量开发的验收标准是原有功能完全不动只加视觉层不碰逻辑层。这条标准写进论文的「测试方法」一节比空谈代码规范有力得多。5. 运行时避坑迷宫游戏最常见的 5 个翻车现场做迷宫游戏选题的学生几乎都会在答辩前一周遇到同样几个问题。我把这些年见到的最高频的翻车现场整理成清单每一个都按「现象 → 原因 → 解决」的套路写直接照做就能排雷。5.1 键盘按了没反应面板压根没拿到焦点现象窗口能正常显示迷宫玩家角色就站在原地按方向键完全无响应。原因KeyListener没有注册到获得焦点的组件上。JFrame的内容面板content pane默认抢占了焦点GamePanel作为子组件无法响应键盘事件。解决给GamePanel增加setFocusable(true)并在MainFrame构造器最后调用panel.requestFocusInWindow()。如果还不行检查MainFrame里是不是调用了addKeyListener——那会把监听加到窗体上而窗体本身经常不持有焦点。监听器必须加到面板上。5.2 玩家走到墙里去了行列坐标混用现象上下移动正常左右移动时玩家能直接穿过墙体或者迷宫画出来是「卧倒」的。原因二维数组row是第一个下标纵向col是第二个下标横向。在paintComponent里绘制时x 坐标对应coly 坐标对应row。如果代码里写成了g.fillRect(playerX * cellSize, playerY * cellSize)等于把行、列对调碰撞检测查的是(playerX, playerY)的墙而绘制用的也是(playerX, playerY)就会出现「玩家移动和墙的位置完全不可预测」的玄学 bug。解决统一命名——数据层用row、col绘制层用px、py并写一行注释「row 对应 y 像素col 对应 x 像素」。同时在canPass里故意走一步越界测试看调试输出是否和预期一致。5.3 生成的迷宫存在孤立区域visited 标记时机不对现象用 BFS 验证时出口不可达或者界面里某个区域玩家永远走不进去。原因递归回溯里visited[nx][ny] true必须发生在「递归调用之前」而不是在递归函数的第一行。如果写成「先递归、进递归后再标记」同一个格子可能会被两个邻居同时选中导致拆墙逻辑和实际访问状态不一致产生断壁。解决把标记语句放在generate方法的开头如 2.2 节代码所示。这其实是 DFS 的通用血泪经验图遍历算法的 visited 标记必须与入队/入栈同步而不是等到处理时才标记否则会重复访问或漏访问。5.4 窗口卡顿和画面闪烁在 EDT 里做了重活或者乱用 paint现象运行后窗口白屏无响应或者移动时画面一卡一卡、出现残影。原因两个高频问题。第一直接在actionPerformed或paintComponent里写了Thread.sleep把事件分发线程阻塞了第二重写了paint(Graphics)而不是paintComponent(Graphics)导致绘制没有经过 Swing 的双缓冲机制出现闪烁。解决所有耗时操作生成迷宫、BFS 验证放在main线程或SwingWorker里游戏循环只用Timer。绘制只重写paintComponent方法体第一行调用super.paintComponent(g)。5.5 每次跑出来迷宫一模一样Random 没加种子现象重启程序两次迷宫完全相同。原因如果MazeGenerator用的是Math.random()并且不依赖系统时间那理论上每次不同但如果内部new Random()没传种子默认确实每次不同结果一样说明很可能传了固定 seed 没被发现或者直接用了常量取模的伪随机逻辑。解决构造器里保留 seed 参数Main入口处传System.currentTimeMillis()想复现问题做调试时才传固定值。这一条同时也是答辩加分项——你可以演示两次运行结果对比告诉老师「固定种子用于测试回归随机种子用于真实游戏」。6. 让答辩更有分量的三个进阶验证技巧迷宫能在界面上跑通只是 60 分的水平。想让这份基于 JAVA 的迷宫游戏设计与实现达到 85 分以上我建议你在现有源码上加三个层面的验证或增强每个都控制在半天工作量以内。第一个给迷宫设计「自动求解」演示。在已有MazeValidator的 BFS 基础上把 visited 数组换成prev前置节点数组求解完成后从出口回溯到入口把路径上的格子标出来然后在paintComponent里用一条折线绘制最优路线。代码量大概 30 行但答辩时你可以现场演示「一键生成迷宫 一键自动求解」这直接对应论文里的「系统测试与结果分析」章节比任何图表都直观。第二个做迷宫规模的性能对比表。跑 15×15、31×31、101×101 三档用System.currentTimeMillis()记录生成耗时和 BFS 求解耗时填进一个三行表格。这里有个预期结论递归回溯的耗时和迷宫规模近似线性增长但 BFS 求解在最坏情况下呈线性增长也能从中看出迷宫规模对两种算法的影响。这个表格不用真实跑分估算也行但跑一遍更稳妥数据放在论文里是硬货。第三个给代码写单元测试。把MazeGenerator和MazeValidator各写两个断言比如「15×15 迷宫出口可达」「101×101 迷宫生成时间小于 1 秒」。不需要引入 JUnit直接在main方法里用if (!result) throw new RuntimeException()也能达到演示效果。这一点极其反常识——很多人以为毕设项目不需要测试实际上老师最怕看到「只演示不验证」的代码。最后说句个人习惯我做这个选题时吃过一次大亏差一点在答辩前夜把项目推倒重来原因就是在第 5 章那个「键盘焦点」问题上卡了三天最后不是靠查资料解决的而是用一个最笨的调试法——在每个监听方法入口打印日志才发现监听器根本没绑定到面板上。所以我现在带任何一个迷宫项目第一件事就是先验证输入事件链路通不通再去看界面漂不漂亮。如果你的源码拿过来也能跑通、能看出结构那它的底子是好的如果跑不通别急着换项目先按第 5 章的清单一项项排查大部分问题都在那五个坑里。希望帮到你。本文还有配套的精品资源点击获取