Android连连看源码实战:从跑通到拆解路径判定与死局检测
简介这是一份面向Android初学者与游戏开发入门者的连连看小游戏完整源码帮助读者通过一个可运行的项目理解移动端小游戏从界面绘制到逻辑判断的实现路径。压缩包共233个文件约6.26MB涵盖Java源码、XML布局、PNG图片素材、OGG音效、Gradle构建脚本及APK成品等其中Java与XML负责界面与业务逻辑PNG与OGG提供图标和音效资源Gradle相关文件支撑工程编译结构完整便于直接导入Android Studio运行调试。源码围绕GameActivity初始化、自定义GameView绘制、findMatch消除算法、onTouchEvent触摸交互以及计分、音效、存档等辅助模块展开读者可据此掌握View绘制、事件分发与基础算法设计。目前已有3235人学习下载适合作为Android游戏开发的练手实例帮助在真实工程中积累UI设计与逻辑拆分的经验。1. 从一份连连看源码说起为什么它值得你花一个周末跑通如果你手里正躺着一份 Android 小游戏连连看源码却迟迟没打开那这篇笔记就是写给你的。连连看看着简单——点两个相同图案路径拐弯不超过两次就消掉——但真把它拆开你会发现它是一块被低估的练手料自定义 View 的绘制与触摸、二维网格的数据结构、路径判定算法、关卡生成与死局检测、音效与动画状态机几乎把 Android 小游戏该有的骨架都串了一遍。它不像大型 RPG 那样一上来就劝退也不像纯 UI Demo 那样学不到东西。适合谁适合已经会写 Activity、能看懂基本 Canvas 绘制但没独立做过完整小游戏的人也适合想拿一个现成工程改玩法、换素材、加关卡的老手。接下来我不讲空话直接按「先跑起来、再拆算法、最后改出你自己的版本」这条线走把源码里最容易翻车的地方一个个点出来。2. 把工程跑起来环境、目录与第一次编译2.1 先确认这份源码属于哪种工程形态拿到一份 Android 小游戏连连看源码第一件事不是急着点运行而是判断它的工程形态。常见有三种纯 Java 自定义 View 的单 Module 工程、Java/Kotlin 混合带简单 MVP 的工程、以及基于某个轻量游戏框架比如自己封装的 SurfaceView 循环的工程。判断方法很直接——看app/src/main/java下的包结构如果只有一个view包加一个MainActivity基本就是第一种如果出现presenter、contract这类目录就是第二种。形态不同后面改玩法的入口位置完全不一样。我一般会先扫一眼build.gradle里的minSdkVersion和targetSdkVersion这两个值决定了你要不要处理权限、后台限制这些额外麻烦。2.2 用 Android Studio 导入并锁定依赖版本导入本身没难度坑在依赖版本。老工程常见的翻车点是compileSdkVersion太低或者用了已经被移除的 support 库。下面是我处理这类老工程的常规操作先看清单再动手// app/build.gradle 里重点看这几行 android { compileSdkVersion 33 // 老工程常是 28 甚至更低建议抬到 33 defaultConfig { minSdkVersion 21 // 低于 21 会缺很多 API不建议再降 targetSdkVersion 33 } } dependencies { // 如果看到 com.android.support:appcompat-v7说明是旧 support 库 // 要么整体迁移到 androidx要么把 compileSdk 保持在 28 附近先跑通 implementation androidx.appcompat:appcompat:1.6.1 }逻辑说明compileSdkVersion决定你能调用哪些 APIminSdkVersion决定能装到多老的机器上。参数怎么改——如果源码里全是android.support.v7.app.AppCompatActivity你有两条路一是用 Android Studio 的「Refactor → Migrate to AndroidX」一键迁移二是把compileSdkVersion压回 28 先跑通再慢慢迁。我一般选前者因为后者迟早要还债。迁移后如果报Duplicate class多半是某个第三方库还在引 support 库用./gradlew app:dependencies查冲突来源。2.3 第一次运行要盯的三个信号编译通过不等于跑得起来。第一次运行我只看三件事Logcat 里有没有Resources$NotFoundException素材没打进包、有没有NullPointerException指向BitmapFactory图片路径写错、以及游戏区域是不是一片黑自定义 View 没触发onDraw。这三个信号基本覆盖了 80% 的首次运行失败。如果黑屏但没崩先检查onDraw里有没有在super.onDraw(canvas)之前就 return或者invalidate()压根没被调用。跑通之后别急着改代码先玩两局感受一下消除动画、连击判定、死局重排这些交互后面拆代码时你才知道每段逻辑对应什么手感。3. 拆开核心网格数据结构与路径判定算法3.1 二维数组怎么存棋盘边界为什么要多留一圈连连看的棋盘本质是一个二维数组但直接用一个N×M的数组会给自己挖坑。原因是路径判定允许连线走到棋盘外面绕行所以常见做法是把逻辑棋盘扩一圈变成(N2)×(M2)最外圈永远存 0空。这样判定时不用到处写边界判断代码干净很多。下面是我常用的结构public class Board { public static final int EMPTY 0; private int rows, cols; private int[][] grid; // 实际尺寸 (rows2) x (cols2) public Board(int rows, int cols) { this.rows rows; this.cols cols; // 多留一圈边界索引 0 和 rows1 / cols1 永远为空 grid new int[rows 2][cols 2]; } public int get(int r, int c) { return grid[r][c]; // r、c 直接就是扩展后的坐标 } public void set(int r, int c, int value) { grid[r][c] value; } public boolean isEmpty(int r, int c) { return grid[r][c] EMPTY; } }逻辑说明grid的物理尺寸比逻辑棋盘大 2玩家看到的第 1 行第 1 列其实对应grid[1][1]。参数说明——rows、cols是玩家可见的棋盘行列数改难度时只动这两个值边界自动跟着扩。这样做的代价是每次坐标转换要记得偏移好处是路径判定里可以放心地往四个方向探到边界外不用写一堆if (r 0 r rows)。3.2 两个图案能不能消三种连接情况的判定顺序路径判定的核心是两个相同图案之间能否用一条不超过两次拐弯的线连起来且线上经过的格子全为空。实现上分三种情况依次判断——直线连通、一次拐弯、两次拐弯。顺序不能乱因为直线是拐弯的特例先判简单的情况能提前返回省算力。下面是一个可复现的判定骨架// 判断 (r1,c1) 和 (r2,c2) 能否消除 public boolean canLink(int r1, int c1, int r2, int c2) { if (r1 r2 c1 c2) return false; // 同一个点 if (grid[r1][c1] ! grid[r2][c2]) return false; // 图案不同 // 1. 同行或同列中间全空 if (checkLine(r1, c1, r2, c2)) return true; // 2. 一次拐弯两个候选拐点 if (isEmpty(r1, c2) checkLine(r1, c1, r1, c2) checkLine(r1, c2, r2, c2)) return true; if (isEmpty(r2, c1) checkLine(r1, c1, r2, c1) checkLine(r2, c1, r2, c2)) return true; // 3. 两次拐弯沿一个点的行/列扫描 return checkTwoTurn(r1, c1, r2, c2); } // 同行或同列之间是否全空不含两端 private boolean checkLine(int r1, int c1, int r2, int c2) { if (r1 r2) { int min Math.min(c1, c2), max Math.max(c1, c2); for (int c min 1; c max; c) if (!isEmpty(r1, c)) return false; return true; } if (c1 c2) { int min Math.min(r1, r2), max Math.max(r1, r2); for (int r min 1; r max; r) if (!isEmpty(r, c1)) return false; return true; } return false; }逻辑说明checkLine只检查两端之间不含端点是否全空因为端点本身是有图案的。一次拐弯的两个候选拐点分别是(r1,c2)和(r2,c1)两个都试。两次拐弯的checkTwoTurn思路是从第一个点沿行和列向外扫描每遇到一个空格就试着用它作为中间拐点再对第二个点做同样的扫描看两条「一次拐弯」路径能否接上。参数说明——所有坐标都是扩展后的坐标isEmpty对边界外永远返回 true这正是多留一圈的价值。3.3 死局检测与重排什么时候该触发棋盘上没有任何一对图案能消除就是死局。不处理的话玩家会卡死体验直接崩。常见做法是每次消除后跑一遍全盘扫描用上面的canLink两两配对只要找到一对就返回「还有解」。全盘扫描的复杂度是 O(n²) 乘上路径判定棋盘不大时完全够用。如果扫完没找到就触发重排——把剩余图案随机打乱重新填回空格再扫一次直到有解为止。这里有个坑重排后必须重新检测否则可能连续死局。我一般会加一个重试上限比如 20 次超过就强制按「保证有解」的算法生成避免极端情况下卡在循环里。4. 避坑与排查连连看源码里最容易翻车的五件事4.1 消除后图案没消失或者消失错位现象点两个相同图案判定通过了但画面上的图案没变或者消掉的是旁边那个。原因通常是数据层和视图层用了两套坐标。数据层是扩展后的(rows2)×(cols2)视图层画的时候如果忘了减 1 偏移就会整体错一格。解决在 View 里维护一个统一的坐标转换方法比如screenX (c - 1) * cellWidth所有绘制和触摸都走它别在两处各写一遍。4.2 触摸点算出来的格子总是偏一点现象手指点在图案上判定却落在隔壁格子。原因是触摸坐标是像素格子是逻辑索引中间少了除法和取整。解决int c (int)(touchX / cellWidth) 1;注意加回边界偏移。如果格子之间有间距cellWidth要算上间距否则越往右偏得越多。这个坑我踩过不止一次血泪经验就是——把间距单独存一个变量别混进格子宽里。4.3 重排之后出现「假死局」现象明明重排了玩家还是点不动。原因是重排只打乱了图案位置但没重新检测可解性或者检测用的还是旧棋盘。解决重排函数里先收集所有非空图案洗牌填回原位置然后立刻调用死局检测检测不通过就再洗一次。注意洗牌要用Collections.shuffle而不是自己写随机交换后者分布不均匀容易反复洗出同一局面。4.4 连续快速点击导致重复消除现象手速快的时候同一对图案被消了两次分数多加。原因是触摸事件和消除逻辑之间没有状态锁。解决在消除开始时置一个isAnimating标志动画结束前忽略新的点击。或者更简单——消除后立刻把两个格子的数据置空第二次点击时canLink自然返回 false。我一般两个都做双保险。4.5 素材图片过大导致低端机卡顿现象中低端机上滑动明显掉帧Logcat 里 GC 频繁。原因是每张图案都按原图加载内存里堆了几十张高分辨率 Bitmap。解决加载时用BitmapFactory.Options的inSampleSize做降采样按格子实际显示尺寸的 2 倍来算就行。另外把图案做成一张图集Sprite Sheet用Bitmap.createBitmap裁切比几十个独立文件省内存也省 IO。5. 改出你自己的版本从换皮到加玩法的进阶路径跑通、拆完、避过坑之后这份源码真正的价值才刚开始。最省力的进阶是换皮——把图案换成你自己的素材改一下配色和音效就是一个能拿出手的小 Demo。再往上一步是加玩法比如限时模式、连击加分、道具提示、重排、炸弹。加道具的入口就在消除逻辑那一层提示就是遍历棋盘找一对可消的并高亮重排直接复用死局检测里的重排函数炸弹则是把某个格子周围一圈清空注意清空后要重新检测死局。再进阶一点可以改关卡生成算法。现在的源码多半是随机填图案保证每种图案数量是偶数。你可以改成按难度曲线生成——前几关图案种类少、棋盘小后面逐渐加大。这里有个验证方法写一个离线脚本用同样的生成算法跑一万次统计每次生成后「初始可消对数」的分布如果某类参数下经常出现 0 对说明生成太死得调。这个脚本不用跑在 Android 上纯 Java 控制台就能验证省得每次改完都装一遍 APK。最后说一个我自己的习惯每次改完核心算法先别急着看画面写几个单元测试把canLink的三种情况各覆盖一遍尤其是边界绕行和两次拐弯的极端坐标。连连看的玄学就在路径判定上肉眼看着能连、代码说不能连十有八九是边界那一圈没处理好。把测试跑绿了再上真机能省下大量「盯着屏幕怀疑人生」的时间。这套流程走下来你手里就不只是一份源码而是一个能持续改、持续验证的小游戏框架了。希望帮到你。本文还有配套的精品资源点击获取