简介:这是一份基于Android Studio开发的推箱子小游戏完整项目源码,面向计算机相关专业在校学生、教师及企业员工,尤其适合作为毕业设计、课程设计、作业或项目初期立项演示的参考素材,也适合Java与Android初学者进阶练手。压缩包共59个文件,约10.11MB,包含9个java源文件、13个xml布局与配置、18个png与9个jpg图片素材、6个mp4演示视频、2个mp3音效、1个pdf文档及1个md说明文件,覆盖代码、界面、资源与文档多个层面。项目代码均经过测试运行成功后才上传,答辩评审平均分达96分,已有174人学习。下载后可通过README.md了解结构,借助视频与文档快速理解推箱子核心逻辑、界面布局与资源组织方式,并在此基础上修改扩展功能,用于学习参考。
1. 推箱子小游戏在 Android Studio 里到底怎么跑起来
很多人第一次拿到「基于 Android Studio 开发的推箱子 Java 小游戏 + 源代码 + 文档说明」这类资源,卡住的地方往往不是游戏逻辑,而是环境。Android Studio 下载完、Java 环境变量配好、Gradle 一同步就报红,模拟器起不来,最后连推箱子长什么样都没看到。这篇笔记就按一线实操的顺序,把 Android Studio 里跑通一个 Java 版推箱子小游戏的全过程拆开:从工程结构、地图数据表示、移动与碰撞判定,到按键/手势控制、关卡切换,再到打包和常见翻车点。适合刚学完 Java 基础、想拿一个完整小游戏练手的人,也适合需要课程设计案例源码、想看清 Android 项目骨架的开发者。核心不是把代码抄一遍,而是让你能自己改地图、加关卡、换素材,最后能独立判断这套方案值不值得继续投入。
2. 先看清工程骨架:Android Studio 项目里哪些文件真正决定推箱子能不能跑
2.1 一个能跑的推箱子工程最少需要哪些文件
Android Studio 新建项目时会生成大量模板文件,但推箱子小游戏真正需要你关心的只有几类。先明确一点:标题里说的是 Java 小游戏,所以语言选 Java,不要被 Kotlin 模板带偏。常见做法是在新建 Empty Views Activity 时把语言切成 Java,这样生成的 MainActivity 就是 Java 文件,后续改起来和网上大多数 Java 小游戏源码对得上。
一个最小可运行的推箱子工程,核心文件大致如下:
| 文件/目录 | 作用 | 是否必须改 |
|---|---|---|
app/src/main/java/.../MainActivity.java | 游戏主入口,承载自定义 View | 必须 |
app/src/main/java/.../GameView.java | 自绘游戏画面、处理触摸/按键 | 必须 |
app/src/main/java/.../LevelData.java | 关卡地图数据与解析 | 必须 |
app/src/main/res/layout/activity_main.xml | 主界面布局,放 GameView | 视方案 |
app/src/main/AndroidManifest.xml | 声明 Activity、屏幕方向 | 建议改 |
app/build.gradle | 编译版本、依赖 | 视环境 |
这里有个选型理由要说清楚:推箱子是典型的网格回合制游戏,画面元素少、刷新频率低,用 Android 原生的 Canvas 自绘完全够用,不需要引入游戏引擎。引入引擎反而会让工程变重、Gradle 依赖变多,对只想练 Java 逻辑的人不划算。所以下面所有实现都基于自定义 View + Canvas 这条路线。
2.2 用自定义 View 承载游戏循环,而不是 Activity 直接画
Activity 负责生命周期,真正的绘制和输入交给 GameView。这样做的原因是:Activity 频繁重建时,游戏状态可以放在 View 或独立的数据类里,逻辑更清晰。下面是一个可直接抄的骨架:
// GameView.java public class GameView extends View { private Paint paint; // 画笔,控制颜色和样式 private LevelData level; // 当前关卡数据 private int cellSize; // 每个格子的像素边长 public GameView(Context context, AttributeSet attrs) { super(context, attrs); paint = new Paint(); paint.setAntiAlias(true); // 抗锯齿,圆角方块更顺眼 level = new LevelData(); level.loadDefault(); // 载入内置关卡 } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // 根据屏幕宽度和地图列数算出格子大小,保证地图完整显示 cellSize = Math.min(w / level.getCols(), h / level.getRows()); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawMap(canvas); // 画墙、地板、目标点 drawBoxes(canvas); // 画箱子 drawPlayer(canvas);// 画玩家 } }逻辑说明:onSizeChanged里算cellSize是关键,它决定了地图在不同分辨率手机上是否变形。参数上,Math.min取宽高比例的较小值,保证地图不会超出屏幕。onDraw每帧重画全部元素,推箱子元素少,这样写性能没有压力,也避免了局部刷新带来的状态不同步。
2.3 地图数据怎么表示,才能既好改又好扩展
推箱子的地图本质是一个二维网格,每个格子有固定类型。常见做法是用字符数组表示,可读性最好,改关卡时直接改字符串就行:
// LevelData.java public class LevelData { // # 墙 . 目标点 $ 箱子 * 箱子在目标点 @ 玩家 + 玩家在目标点 空格 地板 private static final String[] DEFAULT = { "#######", "# #", "# .$@ #", "# #", "#######" }; private char[][] grid; private int rows, cols; public void loadDefault() { rows = DEFAULT.length; cols = DEFAULT[0].length(); grid = new char[rows][cols]; for (int r = 0; r < rows; r++) { grid[r] = DEFAULT[r].toCharArray(); } } public int getRows() { return rows; } public int getCols() { return cols; } public char get(int r, int c) { return grid[r][c]; } public void set(int r, int c, char ch) { grid[r][c] = ch; } }逻辑说明:用字符表示地图,好处是关卡数据可以直接写在代码里,也可以后续从res/raw或 assets 读取文本文件。参数上,#是墙、.是目标点、$是箱子、@是玩家,这套符号是推箱子领域的通用约定,网上大量关卡资源都按这个格式给,方便你直接导入。注意玩家和箱子站在目标点上时要区分状态,否则移动后无法还原目标点,这是新手最容易忽略的地方。
3. 把移动、碰撞和通关判定写对:推箱子逻辑的四个核心函数
3.1 玩家移动与推箱子的判定顺序
推箱子的规则看着简单,写起来最容易乱的是判定顺序。正确顺序是:先算出玩家想去的目标格,判断是不是墙;如果是箱子,再判断箱子后面那一格能不能放;两步都通过才真正移动。顺序错了就会出现「穿墙推箱」或者「箱子叠箱子」的玄学 bug。
// GameView.java 中的移动方法 private void movePlayer(int dr, int dc) { int pr = level.getPlayerRow(); int pc = level.getPlayerCol(); int nr = pr + dr; // 玩家目标行 int nc = pc + dc; // 玩家目标列 char target = level.get(nr, nc); if (target == '#') return; // 撞墙,直接返回 if (target == '$' || target == '*') { // 前面是箱子 int br = nr + dr; // 箱子后面那一格 int bc = nc + dc; char beyond = level.get(br, bc); if (beyond == '#' || beyond == '$' || beyond == '*') { return; // 箱子后面是墙或另一个箱子,推不动 } // 先处理箱子移动,再处理玩家移动 moveBox(nr, nc, br, bc); } movePlayerTo(pr, pc, nr, nc); }逻辑说明:dr、dc是方向增量,上移是(-1,0),下移(1,0),左移(0,-1),右移(0,1)。参数上,判断箱子后面那格时,#、$、*都算阻挡,只有地板或目标点才能推。注意这里先移动箱子再移动玩家,顺序反了会导致玩家和箱子位置重叠。
3.2 箱子移动时怎么保留目标点状态
箱子推到目标点上、又从目标点推走,状态必须能还原。常见做法是移动箱子时判断落点是不是目标点,是就写*,不是就写$;同时把箱子原来的位置还原成地板或目标点。
private void moveBox(int fromR, int fromC, int toR, int toC) { // 还原箱子原位置:如果原来是 * 说明底下是目标点 char origin = level.get(fromR, fromC); level.set(fromR, fromC, origin == '*' ? '.' : ' '); // 设置箱子新位置 char dest = level.get(toR, toC); level.set(toR, toC, dest == '.' ? '*' : '$'); }逻辑说明:origin == '*'表示箱子原来压在目标点上,移走后要还原成.,否则目标点会消失。dest == '.'表示箱子推到目标点上,写成*。这两个判断是推箱子状态管理的核心,漏掉任何一个都会出现「目标点丢失」或「箱子永远不算到位」的问题。
3.3 通关判定:所有箱子都在目标点上
通关条件就是地图里不再有单独的$,所有箱子都是*。遍历一遍即可,不需要额外维护计数器,简单可靠。
public boolean isWin() { for (int r = 0; r < rows; r++) { for (int c = 0; c < cols; c++) { if (grid[r][c] == '$') return false; // 还有箱子没到位 } } return true; }逻辑说明:只要发现一个$就返回 false,全部遍历完没有$才算赢。参数上没有额外配置,注意地图里如果本来就没有箱子,会直接判定通关,所以关卡数据要保证至少有一个箱子。
3.4 输入控制:按键和手势怎么接
Android 上没有物理方向键,常见做法是两种:一是用四个按钮,二是在 GameView 里监听滑动手势。按钮方案对新手最友好,也方便测试。
// MainActivity.java 中绑定按钮 Button up = findViewById(R.id.btn_up); up.setOnClickListener(v -> gameView.movePlayer(-1, 0)); // 其余三个方向同理逻辑说明:把movePlayer设为 public,按钮点击直接调用。参数上,方向增量固定为四组。手势方案则是在onTouchEvent里判断ACTION_UP时起点和终点的差值,超过阈值才算滑动,避免误触。两种方案可以共存,但要注意按钮点击后调用invalidate()触发重绘,否则画面不会更新。
4. 避坑与排查:Android Studio 跑推箱子最常见的五个翻车点
4.1 现象:Gradle 同步失败,满屏红字
原因:Android Studio 版本、Gradle 插件版本、JDK 版本三者不匹配。新版 Android Studio 默认用较新的 Gradle 和 JDK,而老教程里的工程配置往往对不上。
解决:先看gradle-wrapper.properties里的 distributionUrl 和build.gradle里的插件版本,按官方对应关系调整。JDK 建议用 Android Studio 自带的,不要手动指向系统里另一个版本。改完点 Sync Now,不要直接 Run。
4.2 现象:模拟器启动后黑屏或闪退
原因:多半是 Activity 没在 Manifest 里注册,或者布局文件里引用了不存在的控件 id。
解决:检查AndroidManifest.xml中 MainActivity 的声明,检查activity_main.xml里每个 id 是否和 Java 里findViewById一致。闪退时看 Logcat 的红色堆栈,第一行通常就是原因。
4.3 现象:地图显示不全或严重变形
原因:cellSize计算时没有考虑地图行列比例,或者用了固定像素值。
解决:回到onSizeChanged,确保用Math.min(w / cols, h / rows),并且绘制时以左上角为原点、按cellSize逐格画。不要用w / rows这种行列写反的写法。
4.4 现象:箱子推到目标点上不计入通关
原因:目标点状态被覆盖,或者通关判定只检查了*而地图里箱子写成了$。
解决:检查moveBox里对origin和dest的判断,确认目标点用.表示、箱子在目标点上用*表示。通关判定遍历时只找$,逻辑是对的,问题通常出在状态没写对。
4.5 现象:连续快速点击按钮,玩家瞬移或穿墙
原因:按钮点击事件在极短时间内多次触发,而移动逻辑没有做输入节流。
解决:在movePlayer开头加一个简单的时间戳判断,两次移动间隔小于 100 毫秒就忽略;或者用setEnabled(false)在动画期间禁用按钮。推箱子是回合制,不需要高频输入,节流不会影响手感。
5. 进阶:把内置关卡换成外部文件,并做一套可验证的关卡加载流程
5.1 为什么要把关卡从代码里挪出去
内置字符串数组适合演示,但关卡一多,代码会变得很长,改地图还要重新编译。更实用的做法是把关卡放到res/raw或 assets 里,每关一个文本文件,程序启动时读取。这样加关卡不用动 Java 代码,也方便别人给你投稿地图。
5.2 从 res/raw 读取关卡文本
// LevelLoader.java public static List<char[][]> loadLevels(Context context) { List<char[][]> levels = new ArrayList<>(); // 假设 raw 目录下有 level1.txt、level2.txt int[] ids = {R.raw.level1, R.raw.level2}; for (int id : ids) { InputStream is = context.getResources().openRawResource(id); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); List<String> lines = new ArrayList<>(); String line; try { while ((line = reader.readLine()) != null) { if (!line.isEmpty()) lines.add(line); // 跳过空行 } reader.close(); } catch (IOException e) { e.printStackTrace(); } char[][] grid = new char[lines.size()][]; for (int i = 0; i < lines.size(); i++) { grid[i] = lines.get(i).toCharArray(); } levels.add(grid); } return levels; }逻辑说明:openRawResource读取原始文本,逐行转成字符数组。参数上,关卡文件里不要留空行,否则行列会对不齐;每行长度要一致,短的行用空格补齐。读取失败时打印堆栈,方便定位是文件缺失还是格式错误。
5.3 用一套最小验证流程确认关卡可用
加载完关卡后,不要直接进游戏,先做三步验证:第一,打印每关的行数和列数,确认没有 0 行或长度不一致;第二,统计每关中$和*的总数,确认箱子数量大于 0;第三,确认有且只有一个@或+,也就是玩家唯一。这三步能挡掉绝大多数「地图能加载但玩不了」的问题。
| 验证项 | 期望结果 | 不通过时的处理 |
|---|---|---|
| 行数/列数 | 均大于 0 且每行等长 | 检查文本文件空行和补空格 |
| 箱子总数 | $+*≥ 1 | 地图里补箱子 |
| 玩家数量 | @++= 1 | 删掉多余玩家标记 |
5.4 我自己的习惯
我现在拿到任何一份小游戏源码,第一件事不是看玩法,而是先把工程跑起来、把地图换成自己写的一关,再故意写错一个字符看它怎么报错。能复现错误,才算真正看懂这套代码。推箱子这个方向投入不大,但能把 Android 项目结构、自定义 View、状态管理和文件读取串一遍,对后面做更复杂的 Java 小游戏是很好的底子。希望帮到你。
本文还有配套的精品资源,点击获取