本文还有配套的精品资源,点击获取
简介:直接可运行的Java推箱子小游戏工程,基于Swing构建,结构清晰易读。主程序入口为Run.java,GameFrame负责窗口创建,GamePanel实现双缓冲绘图,GameMap解析并管理关卡地图(map0.png至map2.png),GamePlayer处理方向键响应与角色移动,GameBox和GameBoxManager协同控制箱子推动逻辑,GamePoint与GamePointManager管理目标点匹配判定。配套资源完整:player.png是玩家角色,box.png和box2.png为两种箱子样式,point.png标识目标位置,所有图片按需加载。源码全部采用标准Java命名规范,每个类职责单一,无外部依赖,支持Eclipse/IntelliJ等主流IDE一键导入编译运行。适合练习事件监听、坐标计算、碰撞检测、图像加载与简单游戏状态维护,也可快速扩展新关卡或修改操作逻辑。
1. 项目概述:一个“能跑、能学、能改”的Java推箱子教学级工程
你有没有试过在学完Swing基础后,对着空白的JFrame发呆——知道怎么画个矩形、加个按钮,但一想到“做个完整的小游戏”,脑子就卡住?不是不会写代码,而是不知道游戏逻辑该怎么拆解、状态该怎么流转、图像和事件该怎么协同工作。这个Java Swing推箱子源码包,就是为解决这个问题而生的。它不是一个炫技的成品,而是一套“看得见、摸得着、改得动”的教学型工程。关键词里说的“Java推箱子”“Swing游戏源码”“推箱子素材”,其实指向三个真实需求:第一,要能立刻运行起来,验证学习成果;第二,代码结构必须像教科书一样清晰,每个类干什么、为什么这么干,一眼就能看明白;第三,所有视觉元素(player.png、box.png、point.png)都已准备就绪,不用再花半天时间抠图或调试ImageIO加载失败。我第一次打开这个项目时,双击Run.java,三秒后窗口弹出,方向键控制小人移动,空格推箱子,回车重置关卡——那种“原来Swing真能做游戏”的踏实感,比任何理论讲解都管用。它适合两类人:一类是刚写完“Hello World”和“计算器”的Java初学者,想通过一个有明确目标的小项目,把事件监听(KeyListener)、坐标系统(x/y像素定位)、碰撞检测(玩家与箱子、箱子与墙壁)、双缓冲绘图(避免闪烁)这些概念串成一条线;另一类是带学生做课程设计的老师或助教,需要一个零外部依赖、无Maven配置陷阱、连图片路径都不用改就能直接导入Eclipse或IntelliJ的参考模板。它不追求3D特效或网络对战,它的价值在于“诚实”:用最朴素的Swing组件,把游戏开发中最核心的“状态管理”和“逻辑分层”讲透。比如GameBoxManager不直接画箱子,只管“箱子能不能推、推到哪、推完后地图状态变没变”;GamePanel也不处理按键,只负责“此刻该画什么”。这种职责切割,正是工业级代码的起点。
2. 整体架构与设计思路:六类协同如何模拟真实物理世界
这套推箱子的精妙之处,不在于用了多酷的算法,而在于它用六个高度内聚的Java类,构建了一个自洽的、可预测的微型物理世界。整个系统没有全局变量,所有状态流转都靠对象间的明确调用完成。我们来一层层剥开它的设计逻辑。
2.1 核心类职责划分:谁该做什么,边界在哪
先看这六个核心类的分工,它们不是随意命名的,而是严格遵循“单一职责原则”:
GameFrame是整个世界的“容器”。它只做三件事:创建JFrame窗口、设置标题和大小、把GamePanel塞进去并显示。它不碰任何游戏逻辑,甚至不知道“箱子”是什么。就像一栋大楼的门面,负责接待访客(用户),但不管楼里的人在干什么。
GamePanel是“画布”兼“调度中心”。它继承JPanel,重写paintComponent()实现双缓冲绘图(这是消除画面撕裂的关键),同时持有GameMap、GamePlayer、GameBoxManager等引用。但它本身不决定“玩家该往哪走”,只负责“接到指令后,把当前地图、玩家、箱子、目标点全部画出来”。它的核心方法是repaint()——当任何状态改变(比如玩家移动了一格),它就触发重绘,确保画面永远反映最新状态。
GameMap是“世界规则制定者”。它不存储图片,而是解析map0.png这类位图资源:读取每个像素的RGB值,约定“白色=空地、黑色=墙、蓝色=起点、红色=目标点”,然后生成一个二维int数组(如mapData[10][15]),每个数字代表该坐标上的地形类型。它还提供isWall(x, y)、isTarget(x, y)等查询方法,让其他类能快速判断“此处能不能站人”。这才是真正的“地图数据”,而不是一张静态图片。
GamePlayer是“动作发起者”。它只管两件事:响应键盘事件(上/下/左/右键),计算自己下一步想去的坐标(nextX, nextY),然后向GameMap和GameBoxManager“申请通行”。它不直接修改地图数组,而是调用GameBoxManager.tryPushBox()来试探推箱子是否可行。如果可行,才更新自己的坐标;否则,原地不动。这种“申请-批准”机制,天然避免了状态冲突。
GameBoxManager是“物理引擎”。它维护一个GameBox对象列表,每个GameBox记录自己的坐标和是否已被推到目标点。它的核心方法tryPushBox(playerX, playerY, direction)会做一连串原子判断:玩家前方是否有箱子?箱子前方是否为空地或目标点?箱子前方是否是墙或另一个箱子?只有全部满足,才执行“箱子坐标+1,玩家坐标+1”,并返回true。这个方法被GamePlayer调用,但GamePlayer不关心内部怎么算,只看返回值决定自己动不动。
GamePoint和GamePointManager是“胜利条件裁判”。GamePoint只存一个坐标(x, y),GamePointManager则持有一个GamePoint列表,并提供checkAllBoxesOnTargets()方法:遍历所有箱子,检查其坐标是否与任一GamePoint完全重合。只有全部匹配,才返回true,触发胜利逻辑(比如弹窗提示)。它不参与绘制,只做判定。
提示:这种设计让扩展变得极其简单。比如你想增加“冰面”关卡(箱子推上去会滑行),只需修改GameBoxManager.tryPushBox()的逻辑,其他五个类完全不用动。这就是好架构的力量——变化被锁死在一个最小范围内。
2.2 为什么选择Swing而非JavaFX或LibGDX?
可能有人会问:现在都2024年了,为什么还用Swing?答案很务实:教学成本最低,环境依赖最少。JavaFX需要额外引入jmods或打包jlink,LibGDX更是要配Gradle、处理OpenGL上下文,对初学者简直是劝退三连。而Swing是JDK自带的,只要装了Java 8+,就能跑。更重要的是,Swing的事件模型(AWTEvent → KeyEvent → KeyListener)和绘图模型(Graphics → Graphics2D → BufferedImage)足够原始,能让你看清“按键按下”到“屏幕刷新”之间的每一步。比如,GamePanel.paintComponent(g)里的g.drawImage(playerImg, playerX32, playerY32, null)这行代码,直接对应“在坐标(320, 256)处画一个32x32像素的角色图”。没有抽象层遮挡,你才能真正理解坐标系、像素、缩放这些底层概念。这不是技术怀旧,而是刻意选择的“透明性”。
2.3 地图资源的设计哲学:PNG即数据,像素即逻辑
很多人以为map0.png只是张背景图,其实它是可执行的数据文件。GameMap类用ImageIO.read()加载它后,并不把它当图片渲染,而是逐像素扫描:
BufferedImage mapImg = ImageIO.read(new File("map0.png")); int width = mapImg.getWidth(); int height = mapImg.getHeight(); int[][] mapData = new int[height][width]; // 行=纵坐标y,列=横坐标x for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { int rgb = mapImg.getRGB(x, y); int r = (rgb >> 16) & 0xFF; int g = (rgb >> 8) & 0xFF; int b = rgb & 0xFF; if (r == 255 && g == 255 && b == 255) mapData[y][x] = GameMap.EMPTY; // 白色=空地 else if (r == 0 && g == 0 && b == 0) mapData[y][x] = GameMap.WALL; // 黑色=墙 else if (r == 0 && g == 0 && b == 255) mapData[y][x] = GameMap.START; // 蓝色=起点 else if (r == 255 && g == 0 && b == 0) mapData[y][x] = GameMap.TARGET; // 红色=目标点 } }这段代码揭示了关键设计:颜色即语义。你用PS或画图软件编辑map0.png时,改一个像素的颜色,就等于在改游戏规则。比如把某个红色目标点涂成白色,那个位置就不再是胜利条件了。这种“所见即所得”的地图编辑方式,极大降低了关卡设计门槛。我试过让学生自己画map3.png,他们花十分钟就做出了带斜坡和传送点的新关卡(虽然传送点逻辑要自己加,但地图数据已经存在了)。
3. 核心细节解析与实操要点:从加载图片到判定胜利的全链路
光知道类名没用,真正卡住新手的,永远是那些文档里不会写的细节。比如“为什么图片加载总报空指针?”“方向键按了没反应是不是监听器没注册?”“箱子推到一半卡住了怎么调试?”。下面我把从项目导入到首次运行的全流程,拆解成可落地的操作要点。
3.1 资源路径与图片加载:别让FileNotFound毁掉第一印象
这是90%新手栽的第一个坑。源码里写的是ImageIO.read(new File("player.png")),但直接运行会抛FileNotFoundException。原因很简单:Java的相对路径,是以“当前工作目录”为基准的,不是以.java文件所在目录。当你在IDE里右键Run.java,工作目录通常是项目根目录(即包含src文件夹的那个文件夹),所以player.png必须放在根目录下,而不是src里。
注意:不要把图片放进src文件夹!Swing加载图片用的是文件系统路径(File),不是类路径(Class.getResource())。放进src会导致编译后图片被复制到out/production/项目名/下,但代码还是去项目根目录找,必然失败。
正确做法:
1. 将所有png文件(player.png、box.png、map0.png等)直接拖到你的项目根目录(和src文件夹同级);
2. 在IDE中刷新项目(Eclipse按F5,IntelliJ按Ctrl+Alt+Y),确保它们出现在项目视图里;
3. 检查Run.java中的main方法,确认它调用的是new GameFrame(),而不是new JFrame()——GameFrame的构造函数里会初始化所有资源。
如果你坚持要把图片放进resources文件夹(比如为了整洁),就必须改代码:
// 替换原来的 new File("player.png") URL playerUrl = getClass().getClassLoader().getResource("player.png"); if (playerUrl != null) { playerImg = ImageIO.read(playerUrl); } else { throw new RuntimeException("player.png not found in classpath!"); }然后把resources设为“Sources Root”(IntelliJ右键文件夹→Mark as→Sources Root;Eclipse需在Build Path里添加)。
3.2 键盘事件监听:为什么方向键有时失灵?
GamePlayer类实现了KeyListener接口,但仅仅实现还不够,必须把它注册到GamePanel上:
public class GamePlayer { private GamePanel panel; // 构造时传入GamePanel引用 public GamePlayer(GamePanel panel) { this.panel = panel; this.panel.addKeyListener(this); // 关键!必须主动注册 this.panel.setFocusable(true); // 关键!面板必须可获取焦点 this.panel.requestFocusInWindow(); // 关键!首次获得焦点 } }这三个panel.调用缺一不可。常见错误:
- 忘记setFocusable(true):JPanel默认不可聚焦,按键事件根本不会传递给它;
- 忘记requestFocusInWindow():即使可聚焦,也需要主动获取焦点,否则第一次点击窗口后才生效;
- 在GamePanel的paintComponent里调用this.requestFocusInWindow():这是大忌!paintComponent会被频繁调用(每秒60次),导致焦点反复抢夺,键盘响应错乱。
实操心得:我曾经调试一个“按左键没反应”的bug,花了两小时。最后发现是同事在GamePanel的构造函数里写了
this.addKeyListener(new KeyAdapter(){...}),但这个匿名内部类根本没实现keyPressed,只写了keyReleased。方向键是长按触发的,必须监听keyPressed。所以,永远用implements KeyListener,并完整实现三个方法(哪怕空着),比匿名类更可控。
3.3 双缓冲绘图:为什么不用它,游戏会像老电视一样闪烁?
Swing的默认绘图是单缓冲的:每次repaint(),先清空整个面板,再重画所有元素。如果玩家在移动,箱子在滑动,这个“清空-重画”过程就会产生肉眼可见的闪烁。解决方案是双缓冲:先在内存里画一张完整的图(BufferedImage),再一次性把这张图贴到屏幕上。
GamePanel的核心绘图逻辑如下:
private BufferedImage offScreenImage; // 后备缓冲区 private Graphics2D g2d; @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // 1. 初始化缓冲区(仅首次或窗口大小改变时) if (offScreenImage == null || offScreenImage.getWidth() != getWidth() || offScreenImage.getHeight() != getHeight()) { offScreenImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB); g2d = offScreenImage.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } // 2. 在缓冲区里画所有东西 drawBackground(g2d); drawPlayer(g2d); drawBoxes(g2d); drawTargets(g2d); // 3. 把缓冲区一次性画到屏幕 g.drawImage(offScreenImage, 0, 0, null); }这里的关键点是g2d.setRenderingHint(...),它开启了抗锯齿,让角色边缘不那么毛刺。如果你注释掉这行,再对比运行,就能直观感受到差异——这不是玄学,是实实在在的视觉体验提升。
3.4 碰撞检测与箱子推动:四步原子操作的严谨性
推箱子看似简单,但背后是四步不可分割的原子操作。GameBoxManager.tryPushBox()的伪代码逻辑是:
1.定位箱子:根据玩家坐标(playerX, playerY)和方向(比如direction=RIGHT),计算玩家前方坐标:boxX = playerX + 1, boxY = playerY;
2.验证箱子存在:检查mapData[boxY][boxX]是否为箱子类型(比如值为2);
3.验证箱子前方:计算箱子前方坐标nextX = boxX + 1, nextY = boxY,检查mapData[nextY][nextX]是否为可通行区域(空地或目标点),且该位置没有其他箱子;
4.执行推动:如果3成立,则同时更新:boxes.get(i).setX(nextX)和player.setX(boxX)。
这四步必须作为一个整体成功或失败。如果只做了第1、2、4步,忘了第3步检查,箱子就会穿墙。源码里用一个boolean返回值封装了整个流程,GamePlayer只看结果:
if (gameBoxManager.tryPushBox(playerX, playerY, direction)) { player.move(direction); // 玩家坐标也跟着动 } else { // 检查是否能单纯移动(前方是空地或目标点) if (gameMap.isPassable(playerX + dx, playerY + dy)) { player.move(direction); } // 否则,原地不动 }这种“先试探,再行动”的模式,是游戏逻辑稳定性的基石。
4. 实操过程与核心环节实现:从零开始导入、运行、调试与扩展
现在,我们把前面所有的理论,变成你电脑上可触摸的操作。我会以Eclipse为例(IntelliJ步骤类似),手把手带你走完从解压源码包到新增第四个关卡的全过程。
4.1 IDE导入与首次运行:三分钟见证奇迹
步骤1:解压与整理
- 下载源码包,解压到一个纯英文路径的文件夹,比如D:\sokoban-java;
- 删除所有无关文件:.gitignore、.inscode、WzR2g8SYCCJs3L3K2OUp-master-bbd4d6eef02ccac29f2bb2f3c2121341594f1a61(这明显是GitHub下载的冗余文件夹);
- 确保根目录下有:Run.java、GameFrame.java、GamePanel.java、GameMap.java、GamePlayer.java、GameBoxManager.java、GamePoint.java、GamePointManager.java、GameBox.java、Tools.java、map0.png、map1.png、map2.png、player.png、box.png、box2.png、point.png。
步骤2:Eclipse导入
- 打开Eclipse,File → New → Java Project;
- 项目名填sokoban,取消勾选Use default location,点击Browse...,选中你解压的D:\sokoban-java文件夹;
- 点击Finish。Eclipse会自动识别src文件夹(如果有),但我们的源码是扁平结构(所有.java都在根目录),所以需要手动设置源文件夹:右键项目→Properties → Java Build Path → Source → Add Folder,勾选根目录(即sokoban项目本身);
- 点击OK,等待Eclipse编译。如果出现红叉,大概率是图片路径问题,回头检查3.1节。
步骤3:运行
- 在Package Explorer里找到Run.java,右键→Run As → Java Application;
- 如果一切顺利,一个标题为“推箱子”的窗口会弹出,里面是map0.png解析出的地图,蓝色小人站在起点,红色方块是目标点。按方向键,小人移动;按空格,尝试推箱子。恭喜,你已成功运行!
实操心得:如果窗口弹出但全是黑屏,99%是
paintComponent()里没调用super.paintComponent(g)。这行代码负责清空上一帧的残影,漏掉它,所有绘制都会叠加,最终糊成一片黑。这是Swing绘图的铁律,务必牢记。
4.2 新增关卡:制作map3.png并接入游戏
这才是体现项目扩展性的时刻。我们来做一个带“传送门”的新关卡(纯脑洞,不改代码,只改地图)。
步骤1:制作map3.png
- 用画图或Photoshop新建一个320x240像素的画布(保持和map0.png一致);
- 用黑色画笔画几堵墙,白色画空地;
- 用蓝色画一个起点(START);
- 用红色画两个目标点(TARGET);
-关键创新:用绿色(R=0,G=255,B=0)画两个相同大小的正方形,标记为“传送门A”和“传送门B”。记住它们的坐标,比如A在(5,3),B在(12,8)(注意:x是列,y是行,从0开始数)。
步骤2:修改GameMap加载逻辑
- 打开GameMap.java,找到加载地图的方法(通常是loadMap(String fileName));
- 在switch或if-else判断地图编号的地方,增加对map3.png的支持:
if ("map3.png".equals(fileName)) { // 加载map3.png BufferedImage img = ImageIO.read(new File("map3.png")); // ... 解析像素,同map0.png ... // 额外:遍历所有像素,记录绿色点的坐标到一个List<Point> List<Point> portals = new ArrayList<>(); for (int y = 0; y < img.getHeight(); y++) { for (int x = 0; x < img.getWidth(); x++) { int rgb = img.getRGB(x, y); if ((rgb & 0xFFFFFF) == 0x00FF00) { // 绿色 portals.add(new Point(x, y)); } } } // 把portals存到GameMap的一个新字段里,供后续使用 }步骤3:修改GamePlayer移动逻辑(可选)
- 如果你想让玩家走到传送门A就瞬间到B,需要在GamePlayer的移动方法里加判断:
// 在move(direction)方法末尾添加 Point currentPos = new Point(playerX, playerY); if (gameMap.getPortals().contains(currentPos)) { // 找到配对的传送门(简单起见,假设列表里只有两个,索引0和1互为配对) Point otherPortal = gameMap.getPortals().get(0).equals(currentPos) ? gameMap.getPortals().get(1) : gameMap.getPortals().get(0); playerX = otherPortal.x; playerY = otherPortal.y; }这样,你只改了三处代码,就凭空增加了一个新机制。这就是良好架构的价值:功能扩展不等于代码爆炸,而是精准的微创手术。
4.3 调试技巧:当箱子卡住时,如何快速定位?
游戏逻辑复杂,难免出bug。以下是我在调试时总结的“三板斧”:
第一斧:日志输出法
在GameBoxManager.tryPushBox()开头,加一行:
System.out.printf("Try push: player(%d,%d) dir=%s -> box(%d,%d) -> next(%d,%d)%n", playerX, playerY, direction, boxX, boxY, nextX, nextY);运行时按方向键,控制台会打印每一步的坐标计算。如果发现nextX超出了地图范围(比如-1或100),立刻就知道是边界检查没做好。
第二斧:断点调试法
在Eclipse里,在GamePlayer.move()的第一行打个断点,然后按F5(Step Into)一步步跟进:
- 进入GameBoxManager.tryPushBox(),观察isPassable()返回true还是false;
- 如果是false,F5进入isPassable(),看它检查的是哪个坐标,再去mapData里查那个坐标的值;
- 这样,你能在5分钟内定位到是“地图数据错了”,还是“坐标计算偏移了1”。
第三斧:可视化辅助法
临时在GamePanel.paintComponent()里加一段:
// 画出所有箱子的坐标(调试用,上线前删掉) g.setColor(Color.YELLOW); for (GameBox box : gameBoxManager.getBoxes()) { g.drawString(String.format("(%d,%d)", box.getX(), box.getY()), box.getX()*32 + 5, box.getY()*32 + 15); }这样,每个箱子上方会显示它的精确坐标,再也不用靠目测猜位置。
5. 常见问题与排查技巧实录:那些踩过的坑,都给你填平了
基于我带过十几届学生的实战经验,整理出这份“推箱子Swing版排坑指南”。这些问题,网上搜不到标准答案,都是血泪教训。
5.1 图片加载失败的七种死法与解法
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
NullPointerExceptionatImageIO.read() | 文件路径错误,或文件被其他程序占用(如用PS打开着) | 关闭所有图片编辑软件,确认文件在项目根目录,用绝对路径测试:new File("D:/sokoban-java/player.png") |
IllegalArgumentException: input == null | ImageIO.read()返回null,通常因为图片格式损坏或非PNG/JPG | 用Windows照片查看器打开player.png,能正常显示才算有效;或者用在线工具转成PNG-24 |
| 图片显示为灰色方块 | 图片是PNG-32(带Alpha通道),但Swing加载后Alpha值异常 | 在ImageIO.read()后加一行:playerImg = createCompatibleImage(playerImg);(createCompatibleImage是Swing工具方法) |
控制台报sun.awt.image.ImageFormatException | PNG文件用了不支持的压缩方式(如Zopfli) | 用TinyPNG网站重新压缩,选择“标准PNG” |
| 多个图片加载,只有第一个成功 | ImageIO.read()是阻塞IO,如果网络慢或磁盘卡顿,后续调用超时 | 把所有图片加载放到单独线程,或在GameFrame构造函数末尾加Thread.sleep(100)让IO缓一缓 |
5.2 键盘响应失灵的四大元凶
元凶一:焦点丢失
现象:第一次运行正常,切换到其他窗口再切回来,按键失效。
解决:在GamePanel的keyPressed()方法末尾,加this.requestFocusInWindow();。虽然不优雅,但对教学项目够用。元凶二:KeyTyped事件干扰
现象:按字母键(如A)时,玩家也移动了。
解决:KeyListener有三个方法:keyPressed(按键按下)、keyReleased(按键松开)、keyTyped(字符输入)。方向键不触发keyTyped,但字母键会。所以,只在keyPressed里处理方向键,keyTyped留空。元凶三:重复触发
现象:按住右键,玩家不是匀速右移,而是“跳着走”。
解决:这是操作系统按键重复的锅。在GamePlayer里加一个布尔标志:java private boolean isKeyDown = false; public void keyPressed(KeyEvent e) { if (isKeyDown) return; // 忽略重复 isKeyDown = true; // 处理按键... } public void keyReleased(KeyEvent e) { isKeyDown = false; }元凶四:中文输入法劫持
现象:开了搜狗输入法,方向键变成切换候选词。
解决:在GameFrame构造函数里加this.setInputMethodEnabled(false);,彻底禁用输入法。
5.3 绘图闪烁与性能瓶颈的终极优化
问题:窗口最大化后,绘图严重延迟
原因:双缓冲的BufferedImage尺寸没随窗口改变而更新,导致每次都要缩放渲染。
方案:重写GamePanel的componentResized()方法:java @Override public void componentResized(ComponentEvent e) { offScreenImage = null; // 强制下次paint时重建缓冲区 }问题:箱子数量多时(>20个),移动卡顿
原因:paintComponent()里循环绘制每个箱子,CPU占用高。
方案:用Graphics2D.drawImage()一次绘制所有箱子到一个BufferedImage,再把这个图贴到屏幕。即“缓冲区套缓冲区”,牺牲一点内存,换取流畅度。
5.4 关卡通关判定失效的隐蔽陷阱
陷阱:目标点坐标偏移1像素
现象:箱子明明“看起来”在红点上,但不判定胜利。
原因:GameMap解析PNG时,坐标系是(x,y),但绘图时drawImage(x*32, y*32),如果箱子图片宽高不是32x32,或者目标点图标不是严格居中,就会有1像素误差。
排查:在checkAllBoxesOnTargets()里加日志,打印每个箱子和每个目标点的坐标差值。如果差值是(0,1)或(1,0),就是图片尺寸问题。陷阱:地图解析时整数除法截断
现象:map1.png里明明画了3个目标点,但GamePointManager只加载了2个。
原因:解析像素时用了int x = (int)(pixelX / 32.0),但pixelX是整数,/32.0结果可能是4.999999,强制转int变成4,漏掉第5列。
方案:改用Math.round(pixelX / 32.0)或Math.floorDiv(pixelX, 32)。
最后分享一个小技巧:如果你想快速测试新关卡,不必每次都重启程序。在GameMap里加一个热重载方法:
java public static void reloadMap(String fileName) { // 重新加载mapData和portals等所有数据 // 然后调用GamePanel.repaint() }
然后在GamePanel里监听Ctrl+R,调用它。改完map3.png,按Ctrl+R,地图秒变,效率翻倍。
这个Java Swing推箱子项目,远不止是一个小游戏。它是一份用代码写就的交互式教材,每一个类名、每一行注释、每一张PNG,都在无声地告诉你:好的软件工程,始于清晰的边界,成于克制的耦合,终于可预期的行为。我见过太多学生,在成功推动第一个箱子的那一刻,眼睛亮起来——那不是因为游戏多好玩,而是因为他们第一次亲手,把脑海中的逻辑,变成了屏幕上可触摸的真实。这,才是编程最本真的魅力。
本文还有配套的精品资源,点击获取
简介:直接可运行的Java推箱子小游戏工程,基于Swing构建,结构清晰易读。主程序入口为Run.java,GameFrame负责窗口创建,GamePanel实现双缓冲绘图,GameMap解析并管理关卡地图(map0.png至map2.png),GamePlayer处理方向键响应与角色移动,GameBox和GameBoxManager协同控制箱子推动逻辑,GamePoint与GamePointManager管理目标点匹配判定。配套资源完整:player.png是玩家角色,box.png和box2.png为两种箱子样式,point.png标识目标位置,所有图片按需加载。源码全部采用标准Java命名规范,每个类职责单一,无外部依赖,支持Eclipse/IntelliJ等主流IDE一键导入编译运行。适合练习事件监听、坐标计算、碰撞检测、图像加载与简单游戏状态维护,也可快速扩展新关卡或修改操作逻辑。
本文还有配套的精品资源,点击获取