Java推箱子实战:从二维数组到BFS寻路与界面化
简介一份基于Java实现的推箱子小游戏完整工程包适合Java入门学习者练习面向对象、事件监听、Swing图形界面与地图编辑。压缩包共60个文件约248KB包含36个map关卡地图、10张gif炮炮兵游戏素材、6个class编译类、4个doc开发文档以及java源文件、音乐mid、Thumbs.db和启动bat脚本结构清晰解压即可对照源码学习。已有224人浏览学习。关卡地图覆盖1~36关gif图片用于角色与箱子显示class与java文件展示从地图读取、键盘操作到碰撞检测的完整逻辑doc文档含开发文档、用户文档、测试文档、程序员文档适合作为课程设计或自学实战参考。整体小巧但五脏俱全对想快速上手Java小游戏开发的初学者很有价值。1. 推箱子Java版到底在做什么从课程设计到可玩的完整闭环拿到一个叫PP.rar的压缩包里面是一份 Java 写的推箱子Sokoban源码——这种组合在 Java 课程设计、软考练手题、蓝桥杯备赛里太常见了。推箱子不是一个需要图形引擎的复杂游戏它的全部规则就两条玩家能上下左右走箱子只能被推不能拉。但正是因为规则简单它成了练习 Java 基础语法、面向对象编程和基础算法的绝佳载体一个二维数组就是地图一个switch就是一个方向控制器一套while(true)加状态刷新就是主循环。这篇文章不打算复述某份神秘源码而是顺着“用 Java 实现推箱子”这条主线把地图数据结构、移动碰撞判定、通关算法、打包发布这些环节逐个拆开。你手里的 RAR 文件不管长什么样读完都能对照着跑通、改得动甚至自己重写一份。新手可以跟步骤走熟手可以直接跳到第 5 章的踩坑清单和最后的扩展方向。2. 地图数据模型二维数组背后的仓库与货架2.1 一张地图就是一个 int 二维数组推箱子的地图天然是网格化的所以用二维数组存储是绝对主流的选择也是 Java 课程设计里最常见的地图表达方式。地图上有四类静态元素墙、空地、目标点、玩家和箱子所在的空地。设计上最常用的是用一个int值代表一类元素比如 0 表示空地、1 表示墙、2 表示目标点、3 表示箱子、4 表示玩家。public class GameMap { // 地图元素的静态常量 public static final int EMPTY 0; public static final int WALL 1; public static final int TARGET 2; public static final int BOX 3; public static final int PLAYER 4; // 地图容器第一维是行第二维是列 private int[][] map; public GameMap(int rows, int cols) { // 所有元素默认是空地 0 map new int[rows][cols]; } public int getCell(int row, int col) { return map[row][col]; } public void setCell(int row, int col, int value) { map[row][col] value; } }这段代码定义了地图的核心数据结构。逻辑说明很简单map[row][col]中row对应地图的纵向坐标从上往下数col对应横向坐标从左往右数。初学 Java 的人最容易在这里搞混的是map[row][col]不是数学坐标系里的(x, y)而是第row行第col列所以“向右移动一格”是col1“向下移动一格”是row1。参数说明rows和cols是地图的尺寸。课程设计里的地图通常是 10×10 到 15×15 之间不要开得太大因为数组越界是这类项目最常见的运行时异常——你访问了map[15][10]但地图只有 12 行程序直接抛ArrayIndexOutOfBoundsException。所以后面所有移动逻辑里第一步永远是边界检查。2.2 用字符串关卡文件初始化地图从资源到内存市面上流传的 Java 推箱子源码关卡地图大多不是写死在代码里而是放在一个.txt或.dat关卡文件中这属于常见的工程做法。课程设计如果只交源码地图写死也能过但如果你想让老师眼前一亮把关卡外置到文件是值得做的——后面加关卡、换地图都不用重新编译。读取关卡文件用BufferedReader按行读每一行字符串的每个字符对应地图的一列。常见字符约定是#墙空格或.空地$箱子玩家*表示箱子已经在目标点上。import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.List; public class MapLoader { public static int[][] loadFromFile(String filePath) throws IOException { ListString lines new ArrayList(); BufferedReader reader new BufferedReader(new FileReader(filePath)); String line; while ((line reader.readLine()) ! null) { // 跳过空行防止关卡文件末尾多余换行 if (line.trim().isEmpty()) { continue; } lines.add(line); } reader.close(); int rows lines.size(); int cols 0; for (String s : lines) { cols Math.max(cols, s.length()); } int[][] map new int[rows][cols]; for (int i 0; i rows; i) { for (int j 0; j lines.get(i).length(); j) { char ch lines.get(i).charAt(j); switch (ch) { case #: map[i][j] GameMap.WALL; break; case : case .: map[i][j] GameMap.EMPTY; break; case $: map[i][j] GameMap.BOX; break; case : map[i][j] GameMap.PLAYER; break; case *: // 箱子压住目标点暂且按箱子处理 map[i][j] GameMap.BOX; break; default: map[i][j] GameMap.EMPTY; } } } return map; } }逻辑说明先按行读取所有字符串再用rows和最大行宽cols初始化二维数组。逐字符映射的核心是switch分支——每个关卡文件里的可见字符都被翻译成一个整数存进数组。特别要注意*这个符号它表示一个已经推到目标点上的箱子在内存模型里仍然存储为箱子目标点的信息需要单独保存否则后面做通关判定时你区分不了“箱子在目标点上”和“箱子在普通空地上”。参数说明trim().isEmpty()跳过空行是为了兼容关卡文件末尾多余的换行符。文件路径建议用相对路径不要把C:/Users/xxx/...这种绝对路径写进代码。课程设计里常见的要求是用InputStream从 classpath 读取这样打包成 jar 后也能加载到关卡文件我个人建议用getClass().getResourceAsStream(/level/level1.txt)替代FileReader因为后者在打包后经常会因为路径不存在抛FileNotFoundException这种文件读取的方式在发布环节能省掉一个坑。2.3 地图合法性检查与目标点列表地图不是随便填一个数组就能当关卡的。上面说到*箱子在目标点在读取压缩包里的标准关卡文件时如果一张地图缺少目标点玩家推完所有箱子后发现永远无法过关这属于死关卡。所以读过地图之后要做几个合法性校验目标点数量与箱子数量是否相等地图是否被墙完整包围地图内不能有孤立于主区域的空地。最常见的做法是单独维护一份目标点坐标列表public class TargetManager { // 保存所有目标点的坐标 private Listint[] targets new ArrayList(); // 记录每个位置是否为目标点用于快速判断 private boolean[][] targetFlag; public TargetManager(int rows, int cols) { targetFlag new boolean[rows][cols]; } public void addTarget(int row, int col) { targetFlag[row][col] true; targets.add(new int[]{row, col}); } public boolean isTarget(int row, int col) { return targetFlag[row][col]; } public int targetCount() { return targets.size(); } public Listint[] getTargets() { return targets; } }逻辑说明targetFlag是一个布尔型镜像数组专用于O(1)查询某个格子是不是目标点。这在通关判定时会配合箱子坐标使用每推一个箱子就检查箱子的新位置是否落在isTarget上。为什么要单独维护而不是在地图数组里存一个TARGET值因为地图中玩家的位置和箱子的位置是动态变化的而目标点是静态的。如果地图数组里玩家走过的地方把TARGET值覆盖掉下一次判定就找不到目标点在哪了这种状态丢失的经典场景会让人在排错时怀疑人生。合法性检查的具体条件至少包括三条箱子数量必须等于目标点数量否则这个关卡物理上就不可能完成地图最外层一圈必须是墙否则玩家能走出界箱子不能初始就卡在死角位置——比如墙和墙之间的夹角处这种情况玩家无论如何也推不出来属关卡设计缺陷。第一条是最基础的标准检查不通过就打印错误并拒绝加载地图比游戏运行到一半再翻车要省心得多。3. 玩家移动与碰撞判定推箱子的核心机制3.1 坐标变换把方向按键翻译成数组下标变化推箱子的交互逻辑集中在玩家的移动上。每按一次方向键玩家坐标需要按方向做一次加减法。四个方向对应的坐标变换为上-1, 0、下1, 0、左0, -1、右0, 1。用switch处理方向是 Java 基础阶段最常见的写法也是这套代码里最好懂的部分。public enum Direction { UP(-1, 0), DOWN(1, 0), LEFT(0, -1), RIGHT(0, 1); public final int rowOffset; public final int colOffset; Direction(int rowOffset, int colOffset) { this.rowOffset rowOffset; this.colOffset colOffset; } }参数说明rowOffset和colOffset就是坐标增量。用枚举而不是用四个魔法数字的好处是后面move()方法里不需要逐个switch写四遍移动逻辑直接取枚举的偏移量做加法。这是一步到位封装的做法即便课程设计不需要特别复杂的结构这个枚举也能让你的代码在老师那里印象分高出不少。移动的边界判定也很直接private boolean isValidPosition(int row, int col) { return row 0 row rows col 0 col cols map[row][col] ! GameMap.WALL; }这段是移动前的安全阀。逻辑说明行和列都必须落在数组的有效范围内并且目标格不能是墙。注意很多初学实现只判断了墙忘了数组越界——尤其是地图左侧和顶部的边界因为数组下标从 0 开始往左走到col -1不会报错走进别的内存区域是不存在的但 Java 会直接抛异常。isValidPosition这层保护是推箱子移动逻辑里成本最低的一个防线。3.2 推箱子的三个分支空地、推箱子、撞墙移动逻辑要处理的核心情况有三种玩家前方是空地直接走过去玩家前方是箱子且箱子前方是空地或目标点箱子被推一格玩家前方是箱子但箱子前方是墙或另一个箱子玩家原地不动。这个判定顺序在代码里体现为public boolean movePlayer(Direction dir) { int newRow playerRow dir.rowOffset; int newCol playerCol dir.colOffset; if (!isValidPosition(newRow, newCol)) { // 情况一目标格是墙或超出边界移动失败 return false; } if (map[newRow][newCol] GameMap.BOX) { // 情况二前方是箱子需要继续检查箱子前一个格子 int boxNewRow newRow dir.rowOffset; int boxNewCol newCol dir.colOffset; if (!isValidPosition(boxNewRow, boxNewCol) || map[boxNewRow][boxNewCol] GameMap.BOX) { // 箱子前是墙或另一个箱子推不动 return false; } // 可以推动箱子先把箱子移到新位置玩家再走到箱子原位置 map[boxNewRow][boxNewCol] GameMap.BOX; map[newRow][newCol] GameMap.PLAYER; map[playerRow][playerCol] GameMap.EMPTY; } else { // 情况三前方是空地/目标点玩家直接移动 map[newRow][newCol] GameMap.PLAYER; map[playerRow][playerCol] GameMap.EMPTY; } playerRow newRow; playerCol newCol; return true; }逻辑说明这个方法的灵魂是“先检查再移动”。第一层isValidPosition管住墙和边界进入推箱子分支后还要检查箱子的下一个格子因为玩家不能推动一列箱子。三种情况的分支顺序也很关键先处理超界/撞墙返回 false再处理推箱子最后是普通移动。如果把普通移动写在前面箱子就会被玩家“穿过去”这种 bug 属于最典型的推箱子翻车现场。参数说明dir枚举同时影响玩家和箱子的位移量所以用同一个rowOffset和colOffset。在装箱子之前记得把玩家旧位置设为EMPTY。这里有个细节如果玩家原本站在目标点上、离开时要把旧位置恢复成TARGET而不是EMPTY——这一步很多简版代码会漏掉导致走过后目标点在地图上消失关卡判定永远差一个。处理办法是额外维护一份“初始地图快照”或在TargetManager中查询后决定写入值。这个细节会在第 5 章避坑清单里再强调一遍。3.3 多状态图层与数据同步课程设计版本一般不需要复杂的状态管理但如果你想让代码更健壮可以把“地图静态层”和“动态层”分开。静态层只存墙和目标点动态层只存玩家和箱子。推箱子里的一个常见 bug 是地图数组里玩家的位置、箱子位置、目标点位置混在一起判断时容易拿错数据。我的做法是拆成两个数组staticLayer存墙和目标点永不改变dynamicLayer存玩家和箱子随时更新。两个图层的好处在于通关判定逻辑干净了判断箱子是否到位直接看dynamicLayer中该格子是箱子、且staticLayer对应格子是目标点。public boolean isBoxOnTarget(int boxRow, int boxCol) { return dynamicLayer[boxRow][boxCol] GameMap.BOX staticLayer[boxRow][boxCol] GameMap.TARGET; }参数说明staticLayer在地图加载后初始化一次就不再变动。dynamicLayer每次移动后同步更新。这种拆分在按键重做悔棋功能时会非常有用——重做时只需要恢复dynamicLayer的快照不用考虑静态数据被意外覆盖的问题。很多 RAR 包里的源码没有拆图层直接一个数组走天下能跑但后期加功能会越改越乱这也是为什么市面上很多课程设计代码让人看不懂——不是算法难是状态混在一起没法读。4. 通关判定与路径提示让游戏从“能玩”到“玩得明白”4.1 遍历目标点逐一核对箱子位置通关条件的标准定义是所有目标点上都站着箱子。实现方式有两种一种是在每次移动后遍历整个地图找箱子另一种是维护一个计数器每把一个箱子推上目标点就 1推出来就 -1。经验之谈第二种方式简单高效推荐优先实现。public class GameState { private TargetManager targetManager; private int boxOnTargetCount; // 每次移动后调用更新计数 public void updateBoxOnTarget(int boxRow, int boxCol, boolean nowOnTarget) { // 如果箱子移动到目标点上计数1离开目标点计数-1 boxOnTargetCount nowOnTarget ? 1 : -1; } public boolean isWin() { // 所有箱子都在目标点上 return boxOnTargetCount targetManager.targetCount(); } }逻辑说明updateBoxOnTarget方法的参数nowOnTarget表示“这次移动箱子是否压在了目标点上”。调用时机很关键在movePlayer方法成功推动箱子之后、更新playerRow之前调用。如果箱子从一个目标点推出来、又推进另一个目标点你会先减一再加一计数器不会出错。这就是它的巧妙之处。参数说明boxOnTargetCount初始值在加载地图时统计遍历所有箱子位置检查哪些箱子已经在目标点上。这里的注意点不要每次isWin()都遍历全图因为推箱子游戏的步数和箱子数都比较小遍历全图性能上没问题但如果后续要加“统计玩家移动步数”功能计数器方案更容易一起维护。全套代码里这种方案也比较好讲清楚适合软考或蓝桥杯的答题思路。4.2 加入 BFS 寻路提示玩家下一步往哪走在课程设计级别的 Java 推箱子里寻路不是必须的。但如果你用“给玩家提示最短推箱子路径”作为亮点功能BFS广度优先搜索是一个非常适合用来讲解的算法。这里做一个降低难度的版本用 BFS 计算玩家到某个箱子旁边的空地的最短路径忽略推箱子过程只找“人能走到的最近格子”。import java.util.*; public class PathFinder { // 四个方向用于扩展节点 private static final int[][] DIRECTIONS {{-1,0},{1,0},{0,-1},{0,1}}; public static Listint[] bfsPath(int[][] map, int startRow, int startCol, int targetRow, int targetCol) { int rows map.length; int cols map[0].length; // visited 记录是否访问过防止重复入队 boolean[][] visited new boolean[rows][cols]; // prev 记录每个格子来自哪个格子用于回溯路径 int[][][] prev new int[rows][cols][2]; Queueint[] queue new LinkedList(); queue.offer(new int[]{startRow, startCol}); visited[startRow][startCol] true; prev[startRow][startCol][0] -1; prev[startRow][startCol][1] -1; while (!queue.isEmpty()) { int[] current queue.poll(); int r current[0]; int c current[1]; if (r targetRow c targetCol) { // 回溯路径 Listint[] path new ArrayList(); while (r ! -1 c ! -1) { path.add(0, new int[]{r, c}); int pr prev[r][c][0]; int pc prev[r][c][1]; r pr; c pc; } return path; } for (int[] d : DIRECTIONS) { int nr r d[0]; int nc c d[1]; // 越界、撞墙、已访问的格子跳过 if (nr 0 || nr rows || nc 0 || nc cols || map[nr][nc] 1 || visited[nr][nc]) { continue; } visited[nr][nc] true; prev[nr][nc][0] r; prev[nr][nc][1] c; queue.offer(new int[]{nr, nc}); } } return null; // 没有可达路径 } }逻辑说明BFS 按层扩展每一层代表一步。visited数组保证每一个格子最多入队一次避免死循环。prev三维数组保存路径信息——它的每一项是“上一个格子的行列”最后从终点回溯到起点就能得到一条完整路径。代码里map[nr][nc] 1是跳过墙推箱子地图里空地、目标点、箱子都不能阻挡玩家脚步所以只需要排除墙。如果你把玩家和箱子之外的元素也都当成可走格子那 BFS 只管找路不管推箱动作。参数说明DIRECTIONS数组的顺序不影响最短路径的结果只影响同一步数下路径的偏向优先向上还是向下。队列用LinkedList实现是 Java 惯例ArrayDeque也行推荐用ArrayDeque性能略好。路径结果是一个Listint[]每一步是坐标数组主程序可以直接用这个路径做步数提示或自动寻路演示。这个 BFS 模块可以作为“面向对象编程java”的课堂讲解素材比单独背算法书生动很多。4.3 死锁检测在玩家推箱子前提前预判推箱子有个比寻路更实用的话题死锁检测。当一个箱子被推到某个位置后永远无法再到达目标点这个关卡就废了。最基础的死锁检测是“角落检测”——箱子处在墙与墙围成的死角里且这个位置不是目标点。public boolean isDeadLock(int boxRow, int boxCol) { // 如果这个位置本身是目标点不算死锁 if (targetManager.isTarget(boxRow, boxCol)) { return false; } // 上下都是墙或左右都是墙箱子被卡死 boolean upDownBlocked !isValidPosition(boxRow - 1, boxCol) !isValidPosition(boxRow 1, boxCol); boolean leftRightBlocked !isValidPosition(boxRow, boxCol - 1) !isValidPosition(boxRow, boxCol 1); return upDownBlocked || leftRightBlocked; }逻辑说明这个检测利用了推箱子规则的一个硬约束——箱子只能水平或垂直移动。如果上下两个方向都被墙挡住箱子就再也不能纵向移动同时左右再被墙挡住就完全锁死。注意此时如果该位置正好是目标点说明箱子已到位不能判为死锁。这种角落检测属于简化版做不到全局死锁判断但对课程设计级别完全够用能拦截掉大量“推了一会儿发现白用功”的局。参数说明isValidPosition复用了 3.1 节的边界与墙判定。要注意isValidPosition里把“箱子”也当成障碍吗我们这里的实现只把墙当障碍箱子不作为障碍处理。因为检测死锁时你是在判断箱子与墙的相对位置关系旁边站不站另一个箱子不影响这个箱子的移动能力。这个函数的调用时机每次箱子移动后调用返回true就提示玩家“这一步走进死局了”可以配合悔棋功能使用。5. 常见问题与避坑跑不起来、乱码、箱子穿墙的排查5.1 关卡文件读取乱码与控制台中文变问号现象从 RAR 包里解压出来的.txt关卡文件用BufferedReader读取后控制台打印的地图全是。原因关卡文件不是 UTF-8 编码而是 GBK 或 ANSI。Java 的FileReader默认使用系统默认字符集不同操作系统表现不一致Windows 下默认 GBK 读 UTF-8 文件会乱码反过来 Ubuntu 上读 GBK 文件也乱。解决读取时显式指定字符集用InputStreamReader包一层FileInputStreamBufferedReader reader new BufferedReader( new InputStreamReader(new FileInputStream(filePath), UTF-8));注意如果关卡文件本身是 GBK把UTF-8换成GBK。不确定文件是什么编码用编辑器打开看一眼再决定。经验做法是统一把所有关卡文件转成 UTF-8无 BOM代码里只写 UTF-8从根上消灭这类乱码问题。5.2 玩家路过目标点后目标点消失了现象本来地图上的目标点标记还在玩家走过去再走开原来的目标点标记变成空地最后箱子推到目标点上也不触发胜利判定。原因地图数组直接原地修改。玩家移动到目标点时代码里写的是map[newRow][newCol] GameMap.PLAYER覆盖了原来的TARGET玩家离开时又把旧位置写为EMPTY目标点信息就永久丢失。解决把“静态元素”和“动态元素”分层管理。或者退一步在移动逻辑里做查询——玩家离开旧位置时调用targetManager.isTarget(oldRow, oldCol)是目标点就写回TARGET否则写EMPTY。我 3.3 节推荐的图层方案就是为这个准备的静态层不动动态层随便改通关判定永远引用静态层的数据。这个 bug 的隐蔽性在于“第一次玩不一定触发”等你玩到中后期才发现目标点少了几个回溯过去已经来不及。5.3 箱子重叠一个格子上出现两个箱子现象推箱子过程中一个箱子可以“走进”另一个箱子的格子两个箱子的图标叠在一起闪动了半天后操作越来越诡异。原因移动逻辑里只检查了“箱子前面的格子是不是墙”没检查“箱子前面的格子是不是箱子”就执行了推箱操作。代码 3.2 节里写的map[boxNewRow][boxNewCol] GameMap.BOX这个条件是后加的如果最初版本写的是isValidPosition就漏掉了这个判断——isValidPosition只排除了墙箱子是合法格。解决判断“是否可以推箱子”必须检查两点箱子的下一个格子不越界、不是墙、不是另一个箱子。这也是 3.2 节代码里|| map[boxNewRow][boxNewCol] GameMap.BOX这一段存在的意义。5.4 按方向键没反应事件监听与线程阻塞现象Swing 版的推箱子窗口能显示地图也正常但按方向键后玩家不动。控制台版本则是按了方向键没有任何输出。原因控制台版本常见的是没用Scanner读取键盘输入或者用了Scanner.nextLine()在阻塞等待回车而你以为按一下方向键就提交了。Swing 版本则是KeyListener没有正确注册到JFrame上焦点被其他组件抢走。解决控制台版用Scanner.nextLine()读取一个方向单词up/down/left/right或一个字母w/s/a/d按回车确认这是最稳的做法。Swing 版要在JFrame上调用setFocusable(true)并addKeyListener且在paintComponent后调用requestFocusInWindow()。如果你正在做蓝桥杯或课程设计可视化界面还有一个常见坑KeyListener只响应拥有焦点的组件你把监听器加到面板上但面板不可焦点化按键永远不会触发。5.5 打包成可运行 jar 后找不到关卡文件现象在 IDE 里运行好好的FileReader读取相对路径的关卡文件正常。导出成 jar 后双击运行闪退或控制台报FileNotFoundException。原因new FileReader(levels/level1.txt)读取的是文件系统路径。jar 包里的文件不是磁盘路径而是 classpath 资源路径。你打包时关卡文件确实是进了 jar但文件系统里并不存在levels/level1.txt这个路径。解决改用 classpath 资源加载。典型做法是把关卡文件放在源码目录下的resources/levels/目录构建时它会进入 classpath然后用getClass().getResourceAsStream(/levels/level1.txt)读取。这一步几乎每个做发布的学生都会踩一次区别只是你自己写代码时踩还是答辩时当着老师的面踩。6. 从控制台到界面化Swing 改造与自定义关卡编辑器如果上面的内容你已经全部跑通最后这一步是让项目从“能玩”变得“像样”的加分项把控制台版改成 Swing 图形界面顺便做一个关卡编辑器。改造的重点不是重写逻辑——而是把movePlayer从“按键驱动”改成“事件驱动”其余地图、判定、寻路代码完全复用。Swing 推箱子的最小框架是这样用一个JPanel重写paintComponent(Graphics g)遍历地图数组遇到不同值画出不同的颜色或小图标JFrame注册KeyListener在keyPressed里调用movePlayer然后调用repaint()刷新画面。这样改动量很小但视觉效果比起控制台完全是两个量级。具体做法是public class GamePanel extends JPanel { private GameMap gameMap; Override protected void paintComponent(Graphics g) { super.paintComponent(g); int cellSize 40; for (int row 0; row gameMap.getRows(); row) { for (int col 0; col gameMap.getCols(); col) { int value gameMap.getCell(row, col); // 按元素值画不同颜色的方块 g.setColor(colorForValue(value)); g.fillRect(col * cellSize, row * cellSize, cellSize, cellSize); } } } }逻辑说明paintComponent是 Swing 绘图的入口每次repaint()都会触发重绘。相对于控制台版本逐行打印图形界面的优势一目了然而且调试地图布局时能直接看到墙的位置是否合理比看字符矩阵直观得多。colorForValue是一个映射函数建议用switch墙画深灰色空地画白色目标点画浅黄色箱子画棕色玩家画蓝色——辨识度最高的一组配色。关卡编辑器是另一个很实用的扩展。最简单的方式是用鼠标点击网格来切换元素第一次点击放墙第二次点击变箱子第三次变目标点第四次清空。这样你不需要手工数空格去写关卡文件。核心逻辑可以复用 2.2 节的MapLoader反向操作——把int[][]数组按约定字符逐行写成字符串再保存为.txt。编辑器最关键的一点是保存前要做一遍合法性检查特别是“箱子数量等于目标点数量”这条硬规则否则你导出的关卡自己都过不了关。我在做过几个课程设计辅导之后的一个习惯是先把纯逻辑的推箱子核心写好并用控制台验证再开始做界面。如果直接在 Swing 里画地图、写键盘监听、调布局撞墙的定位会变得又慢又痛苦。控制台版本跑通了核心规则再套界面就是纯体力活——这段流程留着以后你给 Java 八股文里“如何设计一个小项目”这类问题举例时也能讲得比别人扎实。希望这篇 Java 推箱子的拆解帮到你愿你手上的 RAR 打开后不止能跑通还能改出自己的版本。本文还有配套的精品资源点击获取

相关新闻

用Sealos在离线环境快速拉起K8s集群:大模型私有化部署完整指南

用Sealos在离线环境快速拉起K8s集群:大模型私有化部署完整指南

1. 为什么在企业离线环境里我会选 Sealos1.1 先盘清楚:离线私有化到底难在哪我这两年一直在做企业级大模型私有化部署,说白了就是帮客户把大模型能力搬进内网。在外网环境里装一个 K8s 集群已经不算什么新鲜事,真正的麻烦全在离线环境&#x…

2026/10/9 6:09:04 阅读更多 →
社区团购毕设源码拆解:微信小程序+SSM从环境搭建到答辩

社区团购毕设源码拆解:微信小程序+SSM从环境搭建到答辩

简介:面向计算机相关专业学生的毕业设计项目,一套基于微信小程序的社区团购完整源码涵盖后台、管理页面与小程序端:后台采用SSM框架,管理页面使用Vue,小程序端配合MySQL数据库与JDK1.8环境,可适配Eclipse、…

2026/10/9 6:09:04 阅读更多 →
Docker与Containerd架构关系及命令对比:容器运行时的底层逻辑

Docker与Containerd架构关系及命令对比:容器运行时的底层逻辑

前两天有同事跑来问我一个问题:我们在服务器上明明装了 Docker,为什么生产环境里看到容器进程却是放在 containerd 目录下的?还有人拿docker run跑起来的容器,用crictl ps居然看不到,反过来也一样。这个问题其实困扰过…

2026/10/9 6:09:04 阅读更多 →

最新新闻

Swift常量let深度解析:不可变绑定、编译优化与并发安全

Swift常量let深度解析:不可变绑定、编译优化与并发安全

学习Swift的人越来越多,但真正能把let用明白的,说实话不多。很多同行写了几年Swift,提到"常量"仍然只会说"let就是不可变的var",可一旦深问下去——常量什么时候能延迟初始化、常量和并发安全有什么关系、为什…

2026/10/9 6:36:28 阅读更多 →
Swift常量正确打开方式:从let原理到编译期优化与并发安全实践

Swift常量正确打开方式:从let原理到编译期优化与并发安全实践

接手一个维护了三年的iOS项目,你最想吐槽的往往不是架构本身,而是散落在代码各个角落的魔法字符串和魔法数字。同一个key四处复制,隔三差五手滑拼错,改一处漏三处。这时候大家才会想起来,Swift里有个再基础不过的东西—…

2026/10/9 6:36:28 阅读更多 →
MiMo-V2.6自我改进强化学习规模化:MoE与因果推断实战解析

MiMo-V2.6自我改进强化学习规模化:MoE与因果推断实战解析

1. 从"自我改进"这个词说起:MiMo-V2.6到底想解决什么问题第一次看到"自我改进的强化学习规模化"这个说法,我的反应是:又是一个造词运动。但把技术报告翻了两遍之后,我改主意了——这次他们想解决的是一个真实…

2026/10/9 6:36:28 阅读更多 →
观察者模式实战:直播间送礼系统的解耦设计与实现

观察者模式实战:直播间送礼系统的解耦设计与实现

打开任何一个直播App,走进一个正在热闹开播的直播间。观众点了一下礼物栏,送出一发火箭。接下来几秒钟里,弹幕频道滚出"XXX 送出了火箭",全屏特效炸开,主播的语音感谢响起,礼物榜排名跳动&#x…

2026/10/9 6:36:28 阅读更多 →
学嵌入式Day1:先啃Linux基础命令,别急着点亮屏幕

学嵌入式Day1:先啃Linux基础命令,别急着点亮屏幕

学嵌入式第一天,很多人恨不得马上点亮一块LCD屏幕、跑一个MQTT协议栈,结果折腾三天连板子都没识别出来。我的建议反着来:第一天什么都别做,先把Linux基础命令在终端里敲熟。说句不太好听的话,嵌入式开发的大部分时间其…

2026/10/9 6:36:28 阅读更多 →
生产级Coding Agent调优实战:Harness工程化决定落地下限

生产级Coding Agent调优实战:Harness工程化决定落地下限

1. 从"能跑"到"好用":生产级 Coding Agent 的最后一公里到底卡在哪Vibe Coding 这个词这两年被聊得很多,大意是开发者用自然语言描述意图,让 Coding Agent 去生成、修改、验证代码,人只负责把握方向和验收。听…

2026/10/9 6:35:27 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

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

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

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

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

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

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/9 6:17:20 阅读更多 →