ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android开心消消乐代码实例:8×8棋盘核心逻辑与多线程实现

Android开心消消乐代码实例:8×8棋盘核心逻辑与多线程实现 简介这份PDF文档面向具备一定Java与Android基础的开发者系统讲解开心消消乐游戏的完整实现思路帮助读者理解从布局搭建到消除判定的核心逻辑。内容围绕8x8按钮矩阵展开涵盖TableLayout与LinearLayout组合布局、按钮三种状态的selector定义、点击事件中相邻方块的交换判断以及用二维mark数组记录可消去位置并处理下落更新的算法细节同时涉及地图是否仍存在可行解的检测思路。资源包共1个PDF文件大小约378KB轻量便于随时查阅适合作为课程设计或练手项目的参考材料。目前已有3220人学习下载读者可从中获得完整的代码组织方式、消除与更新顺序的处理经验以及十字架、T字型重叠消除等边界情况的排错思路对理解Android游戏开发的基本流程具有实际参考价值。1. 从一份 8×8 的 Android 开心消消乐代码说起它到底能跑出什么很多人第一次搜「Android开心消消乐代码实例」心里想的其实是同一件事有没有一份能直接跑起来、逻辑完整、又不至于复杂到看不懂的消消乐源码。这份实例正好卡在这个位置上——它用最朴素的 Java Android SDK 写了一个 8×8 的消消乐 demo没有引入任何第三方引擎也没有花哨的动画框架核心就是二维数组、按钮点击、线程消息这几样东西。作者自己说得很直白没系统学过 Java 面向对象Android 也是从零搭环境开始的最后砍掉了 UI、关卡、数据库和联网只留下「消方块」这一件事。但恰恰因为砍得干净它反而适合拿来拆解消消乐最核心的三块骨头布局怎么动态生成、消除判定怎么做、消除后的掉落和补充怎么用多线程驱动。如果你正在找一份能读懂、能改、能当二次开发起点的 Android 开心消消乐代码这份实例的参考价值不在「完整」而在「骨架清楚」。它适合刚接触 Android、想用一个真实小项目把 Activity、Handler、Thread、TableLayout 串起来的人也适合已经会写业务代码、但没写过游戏循环、想看看棋盘类逻辑怎么落地的人。2. 布局与按钮状态为什么 64 个按钮要用代码生成而不是 XML2.1 动态生成 8×8 按钮网格的取舍这份代码最容易被忽略、但最值得先讲清楚的地方是它没有把 64 个 ImageButton 写进 XML。原因很实际64 个按钮如果手写进布局文件复制粘贴本身就是灾难改一个尺寸要改 64 处。作者的做法是在onCreate里用TableLayoutTableRow双层循环动态创建每个按钮挂一个point对象作为 tag把坐标和图片 id 一起存进去。这样点击事件里拿到v.getTag()就能直接知道「我点的是第几行第几列、当前是什么图案」。// 动态构建 8x8 棋盘外层行、内层列每个按钮绑定 point 作为 tag LinearLayout vlayout (LinearLayout) findViewById(R.id.vlayout); TableLayout tlayout new TableLayout(this); TableRow row[] new TableRow[8]; for (int i 0; i 8; i) { row[i] new TableRow(this); row[i].setGravity(Gravity.CENTER); for (int j 0; j 8; j) { btn[8 * i j] new ImageButton(this); // 38x38 是当时按图片素材定的固定像素实际项目建议用 dp btn[8 * i j].setLayoutParams(new TableRow.LayoutParams(38, 38)); initBtn(i, j); // 随机样式 写入 map 绑定 tag btn[8 * i j].setOnClickListener(listener); row[i].addView(btn[8 * i j]); } tlayout.addView(row[i]); }逻辑上分三步先建行容器再建按钮并初始化最后把按钮塞进行、把行塞进表。initBtn(i, j)里做了三件事——调getStyle()随机取 1~7 的图案、把图案编号写进map[i][j]、把point(i, j, button, id)设成 tag。参数上要注意TableRow.LayoutParams(38, 38)用的是像素而不是 dp在不同密度屏幕上会缩放不一致这是这份 demo 的典型历史写法移植时建议换成 dp 或TypedValue换算。map是纯逻辑棋盘btn是视图层两者靠point里的坐标对齐这个「逻辑与视图分离」的思路是后面所有消除判定的基础。2.2 selector 三态与随机图案的绑定方式按钮的视觉状态没有用代码切换而是交给 drawable 下的 selector。每个图案对应一个btn?.xml里面按state_pressed、state_focused声明不同图片。这样按下时系统自动换图代码里不用管。!-- drawable/btn1.xml按下态、焦点态、普通态三张图 -- selector xmlns:androidhttp://schemas.android.com/apk/res/android item android:drawabledrawable/a1_2 android:state_pressedtrue/ item android:drawabledrawable/a1 android:state_focusedfalse android:state_pressedfalse/ item android:drawabledrawable/a1_1 android:state_focusedtrue/ item android:drawabledrawable/a1 android:state_focusedfalse/ /selectorgetStyle()用Math.random()取 1~7返回对应的R.drawable.btn?同时把num和id存下来。这里有个细节num是逻辑图案编号写进mapid是资源 id写进point两者必须同步更新否则会出现「逻辑上是图案 3、显示的是图案 5」的错位。常见做法是把图案编号和资源 id 做成一张映射表避免 switch 里手写七行。焦点态在这份代码里其实没用到但保留在 selector 里不影响运行属于「有现成图就顺手写上」的处理。3. 消除判定与 mark 数组横竖扫描、十字重叠和更新顺序3.1 用 mark 记录「消去后变成什么」而不是布尔值消除判定的核心是find()先逐行扫描再逐列扫描把能消的方块标记进mark[8][8]。一开始作者把mark设计成布尔量只记「能不能消」后来发现更新阶段还需要知道「消掉之后这个位置应该被上面第几个方块补上」于是把mark改成了整数语义横向三连标记为 1纵向三连标记为 nn 是这一列连续相同的个数。这样更新时只要遍历mark按值决定「从上一行搬」还是「从上面第 n 行搬」。// 横向扫描连续 3 个及以上相同则标记 for (int i 0; i 8; i) { int count 1; for (int j 0; j 7; j) { if (map[i][j] map[i][j 1]) { count; if (count 3) { // 刚好凑满三个回填前两个 flag true; mark[i][j - 1] 1; mark[i][j] 1; mark[i][j 1] 1; score 15; } else if (count 3) { // 超过三个继续往后标 mark[i][j 1] 1; score 5; } } else { count 1; // 断了就重置计数 } } }纵向扫描逻辑类似但标记值不同mark[i-1][j] 3、mark[i][j] 3、mark[i1][j] 3表示这一列要整体下落。参数上count是连续计数器flag是「本轮是否有消除」的返回值score顺手累加。复杂度是 O(2×n²)作者明确说不优化因为动画需要在这里停顿快反而不好。这个取舍很真实棋盘类小游戏在 8×8 规模下性能从来不是瓶颈可读性和可调试性才是。3.2 十字与 T 型重叠时为什么必须先横后竖find()里有一个非常关键的顺序约定先扫横行再扫竖列。原因是十字或 T 型消除时同一个方块可能同时属于横向三连和纵向三连mark会被写两次。更新阶段以纵向为准因为掉落是按列发生的所以必须让竖列扫描后执行用它的值覆盖横向的值。如果反过来横向的1会盖掉纵向的3掉落逻辑就会错乱。// 更新阶段必须从上往下扫描 for (int i 0; i 8; i) { for (int j 0; j 8; j) { if (mark[i][j] 1) { // 横向消除整列上移一格 for (int k i; k 0; k--) { updateBtn(k, j, k - 1, j); } updateBtn(0, j); // 顶部补新块 } else if (mark[i][j] 3) { // 纵向消除从上面第 n 行搬 if (i - mark[i][j] 0) { updateBtn(i, j, i - mark[i][j], j); updateBtn(i - mark[i][j], j); } else { updateBtn(i, j); } } else if (mark[i][j] 2) { // 特殊标记单独补块 updateBtn(i, j); } } }第二个顺序约定是更新时必须从上往下。如果从下往上下面方块的更新会先破坏上面还没处理的数据而mark里记的索引还是旧的就会消错位置。反过来上面先更新不会影响下面的原始数据。这两条顺序规则是这份代码里最容易被改错的地方也是「血泪经验」级别的坑逻辑本身不难难的是记住「谁覆盖谁、谁先谁后」。3.3 判断地图是否还有解check 与 check(i,j) 的分工每轮消除后要判断棋盘上还有没有可行解没有就重开地图。这里有两个同名不同参的函数check(i, j)判断「某个方块所在的行列是否已经形成三连」check()则遍历所有相邻交换交换后分别对两个方块调check(i,j)再换回来。最坏复杂度是 2×(n²)×2×8对 8×8 完全够用。// 遍历所有相邻对试交换后看是否产生三连 private boolean check() { for (int i 0; i 8; i) { for (int j 0; j 7; j) { swapMap(i, j, i, j 1); if (check(i, j)) { swapMap(i, j, i, j 1); return true; } if (check(i, j 1)) { swapMap(i, j, i, j 1); return true; } swapMap(i, j, i, j 1); // 没解就换回来 } } // 纵向同理略 return false; }check(i,j)内部先向上数、再向下数、再向左、向右任一方向凑满 3 就返回 true。参数i、j是被检查方块的坐标边界判断用i1、i18这类写法防止越界。这个函数是「无解检测」的全部没有它棋盘消到死局时玩家会卡住所以它是消消乐里不能省的一块。4. 多线程与 Handler为什么 UI 更新必须回到主线程4.1 run() 里只算不画靠消息驱动主线程作者踩过的一个大坑是一开始把「消去—更新」的逐步画面写在主线程里结果 Android 把所有计算跑完才刷新 UI中间那些「慢慢消失」的代码完全没效果。后来改成ThreadHandlerrun()里只做计算和Thread.sleep每一步通过mHandler.sendEmptyMessage(what)通知主线程去改 UI。Override public void run() { if (find()) { // 有可消除 flag false; int n 10; alpha 255; while (n-- ! 0) { // 分 10 帧做淡出 wait(30); // 每帧停 30ms mHandler.sendEmptyMessage(0); // 通知主线程降 alpha } wait(100); mHandler.sendEmptyMessage(1); // 通知主线程更新棋盘 } else if (flag true) { // 玩家交换无效换回来 swapMap(p1.x, p1.y, p2.x, p2.y); wait(300); mHandler.sendEmptyMessage(2); } else if (flag false) { // 消完后无解重开地图 p1 new point(-2, -2); p2 new point(-2, -2); if (check() false) { mHandler.sendEmptyMessage(3); } } }wait(int)是对Thread.sleep的封装参数单位毫秒。alpha从 255 每次减 2510 次到 5配合hideBtn()做出淡出。flag用来区分「玩家交换无效」和「消除后无解」两种 else 分支这是整个状态机里最容易混的地方。run()里绝对不能碰 UI这是 Android 的硬规则所以所有setBackgroundDrawable、setText都放在Handler.handleMessage里。4.2 Handler 的四个消息分支与线程顺序陷阱mHandler用msg.what分四路0 降 alpha、1 更新棋盘并重启线程、2 换回无效交换、3 重开地图。其中 case 1 里thread.start()是关键——更新完棋盘后掉落下来的方块可能又凑成新的可消除组合所以要再跑一轮run()直到find()返回 false。public Handler mHandler new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case 0: hideBtn(); break; case 1: updateState(); text.setText(分数 score); thread.start(); // 继续下一轮消除 break; case 2: swapImage(); // 无效交换换回 p1 new point(-2, -2); p2 new point(-2, -2); break; case 3: Toast.makeText(MainActivity.this, 已自动生成新地图, Toast.LENGTH_SHORT).show(); for (int i 0; i 8; i) for (int j 0; j 8; j) initBtn(i, j); while (find()) updateState(); break; } super.handleMessage(msg); } };作者调试最久的一个问题是线程执行顺序主线程还没算完次线程已经拿旧数据开始新计算了。解决办法是等主线程处理完再回调次线程也就是把thread.start()放在 case 1 里而不是在run()末尾直接再调。这个「谁先谁后」的坑在多线程游戏循环里非常典型常见做法是用Handler的post串行化或者干脆用单线程消息队列驱动整个游戏循环避免共享map被并发读写。5. 避坑与排查这份消消乐代码最容易翻车的五个地方5.1 现象点击相邻按钮后图案闪一下又弹回原因flag状态没重置或者swapImage()和swapMap()只执行了一个导致逻辑棋盘和视图不一致。解决交换时swapMap和swapImage必须成对调用run()里判定无效后要同时把map和视图换回来并重置p1、p2为(-2,-2)防止下次点击误用旧坐标。5.2 现象消除后上面的方块没掉下来或者掉错位置原因mark的赋值顺序反了先竖后横导致横向的 1 覆盖了纵向的 3或者更新时从下往上扫描破坏了上面的原始数据。解决find()里严格先横后竖updateState()里严格从上往下这两条顺序不能动。5.3 现象棋盘消到某个局面后卡死没有任何可消组合原因check()没被调用或者check()里的交换没有正确换回导致棋盘状态被污染。解决每轮updateState()之后必须调check()返回 false 就走 case 3 重开地图check()里每次swapMap后无论结果如何都要换回来。5.4 现象按钮淡出动画不生效图案直接消失原因hideBtn()在主线程之外被调用或者alpha没有在每轮开始时重置为 255。解决hideBtn()只能通过Handler在主线程执行run()里每轮开头把alpha 255updateState()里把所有按钮 alpha 恢复 255。5.5 现象分数不更新或重复累加原因score在find()里累加但text.setText只在 case 1 执行如果某轮没有走 case 1分数就不同步。解决把分数刷新统一放在updateState()之后或者每次find()返回 true 后都刷新一次避免依赖单一消息分支。6. 进阶改造把这份 demo 变成能继续写的项目这份代码砍掉了 UI、动画、关卡、数据库和联网但骨架是完整的二次开发可以从几个具体点切入。第一把map和btn的耦合再拆一层现在point同时存坐标、视图和资源 id改图案时要同步改三处容易错。常见做法是引入一个Tile类只管逻辑视图层用RecyclerView或自绘View渲染逻辑和渲染彻底分离后面加动画和关卡才不会互相牵制。第二把ThreadHandler换成HandlerThread或Choreographer驱动的固定帧循环run()里的wait(30)是硬编码帧率改成按时间戳计算 delta 后动画在不同设备上速度才一致。第三check()目前是暴力遍历所有相邻交换8×8 够用但如果棋盘扩到 10×10 以上可以只检查「交换后可能形成三连」的局部区域把复杂度从 O(n²×8) 降到 O(n²)。第四mark的整数语义可以扩展成枚举把「横向消除」「纵向消除」「特殊块」分开后面加爆炸块、彩虹块时不用再猜数字含义。// 用枚举替代 mark 的魔法数字后续扩展特殊块更清晰 enum MarkType { NONE, HORIZONTAL, VERTICAL, SPECIAL } private MarkType[][] mark new MarkType[8][8]; // 初始化时全部置 NONE for (int i 0; i 8; i) for (int j 0; j 8; j) mark[i][j] MarkType.NONE;验证改造是否成功最直接的办法是固定随机种子跑一批棋盘统计「平均多少步进入无解」和「单次消除耗时」和原版对比。我自己的习惯是每次动find()或updateState()之前先把map打印成 8×8 文本快照改完再打一次肉眼比对差异比断点调试快得多。从那以后我每次改消除逻辑都强制先跑一遍「横三、竖三、十字、T 型」四个固定棋盘的快照对比确认mark和更新结果一致再往下写。希望帮到你。本文还有配套的精品资源点击获取
返回列表