
简介一份关于安卓平台“打地鼠”益智游戏开发的设计报告主要面向移动开发初学者、高校游戏方向学生以及需要完成课程设计或毕业设计的开发者。内容覆盖从开发环境搭建、关键技术分析到需求分析、游戏设计、代码实现与测试验证的完整项目流程重点介绍了安卓SDK、NDK、Java语言、游戏引擎与图形音频库等知识并配有核心代码与测试说明能直观展示一个可运行小游戏背后的设计思路和工程组织方式。整份资源由1个doc文档组成大小约1.73MB包含封面、摘要、目录及从绪论到总结共六章正文结构完整尤其适合对照目录逐步梳理项目写作框架。目前已获得477人学习可作为“打地鼠”类安卓游戏开发的教学参考也能帮助读者快速掌握移动小游戏项目从0到1的落地方法。1. 先别急着写“打地鼠”这个设计报告真正的分水岭在哪“Android打地鼠”这类题目有个迷惑性它看起来太像一道练习题了几张图片、一个点击事件、一个倒计时拼在一起似乎就是全部。但真按“开发设计报告”的格式去交你会发现最容易被追问的是这一句“地鼠是怎么冒出来的”如果答案是“我把ImageView的visible改了一下”整份报告就停留在控件操作层面撑不起“设计”二字。这篇按做项目时的思路来拆先定状态与数据再谈布局最后把命中判定、生成节奏和倒计时连成一条不互相打架的调用链。工具就用android studio自带的环境和一个能开android debug bridge的真机不引入任何第三方库。读完你会得到一个能复现、能答辩、能往上加特效的最小完整实现。2. 坐标映射与状态管理打地鼠的“洞”不只是一个图片2.1 九个洞的数据结构为什么不用二维坐标大部分第一次写打地鼠的人会把棋盘存成一个二维数组再用行列去定位地鼠。这个直觉来自“地图”类游戏但打地鼠不是地图寻路它没有相邻格子之间的移动逻辑所有行为都是“某个洞在地鼠出现/被击中/消失”这三个状态之间跳转。用9个元素的线性数组就够了坐标只用来确定初始位置。我一般会这样定义棋盘数据洞编号行列状态字段说明000moleVisible[0]该洞当前是否有地鼠101moleVisible[1]依次类推202moleVisible[2]九宫格右上310moleVisible[3]中间行左411moleVisible[4]正中央512moleVisible[5]中间行右620moleVisible[6]底部左721moleVisible[7]底部中822moleVisible[8]底部右这张表就是设计报告里“数据结构设计”一节的底稿。真正运行时不关心第4号洞在第1行第1列只关心“4号洞的状态是否可见”。把行、列留在XML布局里把编号留在逻辑层两者通过固定的编号约定对应可以省掉一整层坐标换算代码。2.2 用enabled状态驱动图片切换StateListDrawable的妙用洞的显示与隐藏常见做法有两种切换ImageView的visibility或者切换setImageResource。两种我都用过但写“设计报告”时更推荐第三种用StateListDrawable把“是否有地鼠”映射成View的enabled状态。看一下selector资源的写法存放在res/drawable/mole_selector.xmlselector xmlns:androidhttp://schemas.android.com/apk/res/android !-- enabledtrue 表示地鼠冒头 -- item android:state_enabledtrue bitmap android:srcdrawable/mole_up android:gravityfill_horizontal|fill_vertical / /item !-- 默认状态空洞 -- item android:state_enabledfalse layer-list item android:drawabledrawable/hole_background / /layer-list /item /selector布局里的ImageView这样引用它ImageView android:idid/hole_4 android:layout_width110dp android:layout_height110dp android:scaleTypefitXY android:srcdrawable/mole_selector android:enabledfalse /关键点在于ImageView的enabled属性一旦变化selector会立刻重新匹配并切换图片。逻辑层不需要去调setImageResource只需要维护布尔状态。把moleVisible[4] true和hole4.isEnabled true绑定成同一件事状态源只有一个不会出现“逻辑上有地鼠但图片没换”的时序错位。这里有个很多人踩过的坑enabledfalse会让View不响应点击。放平时这正好是需求——空点打下去本来就不该计分。但如果你希望“打空了也有反馈动画”就不能依赖enabled拦截事件需要改用自定义状态或单独设tag。设计报告里把这个边界写清楚比堆功能更能体现设计意识。2.3 把九宫格铺进android studioGridLayout参数说明棋盘布局用GridLayout就够不必上RecyclerView因为这里没有任何滑动复用的需求。android studio里新建项目后在activity_main.xml中把根布局换成GridLayout并按需调整参数GridLayout android:idid/board android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop24dp android:columnCount3 android:rowCount3 android:alignmentModealignMargins android:useDefaultMarginstrue !-- 这里放9个ImageViewID为hole_0到hole_8 -- /GridLayout几个参数说明columnCount3指定三列rowCount3指定三行两个值必须与代码里的BooleanArray(9)长度对应。alignmentModealignMargins配合useDefaultMarginstrue让子View自动获得统一边距免去手动给每个洞加margin。每个ImageView的宽高不要超过120dp否则三列会超出小屏宽度建议用110dp起步在真机上根据机型微调。运行前记得在android sdk里装好API 29以上的系统镜像项目minSdk设到21左右即可覆盖绝大多数旧设备。到这里界面层的洞全部就绪但它们还不会动。3. 生成节奏与命中判定同一个Handler管住所有定时任务3.1 用Handler而不是TimerLooper时间源统一的关键打地鼠需要一个“生成者”每隔几百毫秒随机选一个洞让地鼠冒头过一会儿再缩回去。Thread.sleep加死循环肯定不行TimerTask也能跑但Timer的调度线程和UI线程的时间源是分开的游戏结束时想一键停干净很麻烦。我只会用一个主线程Handler来跑所有定时任务代码是这样的class GameController(private val board: GridLayout, private val scoreText: TextView) { private val handler Handler(Looper.getMainLooper()) private val moleVisible BooleanArray(9) private val holeViews arrayOfNullsImageView(9) private var running false private var score 0 /** 每只地鼠冒头时长800ms */ private val MOLE_SHOW_MS 800L /** 生成间隔1200ms */ private val SPAWN_INTERVAL_MS 1200L init { for (i in 0..8) { holeViews[i] board.findViewById( resources.getIdentifier(hole_$i, id, board.context.packageName) ) } } fun start() { running true scheduleNextSpawn() } private fun scheduleNextSpawn() { handler.postDelayed(::spawnOne, SPAWN_INTERVAL_MS) } private fun spawnOne() { if (!running) return val index (0..8).random() if (!moleVisible[index]) { moleVisible[index] true holeViews[index]?.isEnabled true // 到点后收回 handler.postDelayed({ moleVisible[index] false holeViews[index]?.isEnabled false }, MOLE_SHOW_MS) } scheduleNextSpawn() } }逻辑代码有三点要展开说明。第一SPAWN_INTERVAL_MS和MOLE_SHOW_MS的配比直接决定游戏手感。1200ms生成一只、800ms后缩回中间有400ms空窗玩家有反应时间但不敢走神。如果两只参数都压到500ms以下游戏会变成纯运气测试且同一洞被连续选中的概率仍然存在体验会很差。这两个值应当作为常量放在顶部而不是散落在代码里——设计报告评审时这是“参数可配置性”的加分项。第二上述实现同一时刻最多只有一只地鼠。这是因为spawnOne里先检查moleVisible[index]选中已冒头的洞就跳过而每次只有收回后才会安排下一次生成。想要“双洞同现”的进阶版本需要把scheduleNextSpawn的间隔缩短并允许选中已冒头的洞时换一个目标同时要处理多个hide回调的清理复杂度会上一个台阶。课程项目把单洞节奏做好已经足够。第三postDelayed里直接写::spawnOne方法引用比postDelayed(Runnable)写匿名内部类更清楚避免在回调里把Handler传得到处都是。如果项目用Kotlin这个写法也是官方推荐风格的简化版。3.2 按下即中onTouchListener与onClick的取舍打地鼠的点击判定有个细节多数新手会忽略onClick事件在手指抬起时才触发。玩家手指按下去的那一瞬间游戏感觉是最跟手的如果必须等抬起才计分快速连打的时候会明显觉得慢半拍。所以物理上的命中判定必须放在按下事件里。具体到代码每个ImageView绑定一个触摸监听private fun bindHitListener(index: Int) { val view holeViews[index] ?: return view.setOnTouchListener { v, _ - val hit v.isEnabled // enabledtrue 表示当前有地鼠 if (hit) { v.isEnabled false moleVisible[index] false score 10 scoreText.text 得分: $score } true } }这段代码的巧妙之处在于状态判断不再依赖moleVisible数组而是直接读v.isEnabled。因为图片切换也是由enabled驱动的enabled就是唯一事实来源。如果打完地鼠后还要做“缩回动画”那就在置为false前启动一个ScaleAnimation结束时再把enabled设回去动画归动画状态归状态互不污染。onTouch返回true表示消费事件这样后续的onClick不会触发也就不会有“按一下得两分”的问题。若你确实需要点击音效或者震动反馈同样在这个监听器里做。3.3 倒计时与android进度条把三条定时任务收进一套循环倒计时如果用CountDownTimer单独写就会和地鼠生成各自为政。后面结束时一边还在postDelayed生成新地鼠游戏就停不干净。所以进度条更新也走同一个Handler用进度条展示剩余时间private val MAX_TIME_MS 30_000L private var remainMs MAX_TIME_MS private fun startCountdown() { handler.post(object : Runnable { override fun run() { if (!running) return remainMs - 1000 // progressBar的max设为100按百分比赋值 progressBar.progress (remainMs * 100 / MAX_TIME_MS).toInt() if (remainMs 0) { handler.postDelayed(this, 1000) } else { endGame() } } }) }android进度条默认是转圈样式这里要用水平进度条在XML里给ProgressBar设置style?android:attr/progressBarStyleHorizontal。上面代码里的MAX_TIME_MS是完整局时长30秒由于整秒递减进度条会有明显的逐格跳动不会出现浮点累积误差。这里把进度条max设为100而不是MAX_TIME_MS是为了让onProgressChanged监听器拿到的是整数百分比界面层不需要再关心原始毫秒值。同样的原则也适用于得分文本UI只读格式化后的字符串不直接读内部计数器。endGame()里只需要一行handler.removeCallbacksAndMessages(null)就能把生成、收回、倒计时三条任务全部清掉。因为三者共用一个Handler这个设计让“清除所有定时任务”从遍历任务列表变成了单点操作。把这一条写进设计报告的性能与生命周期部分很能说明对Android消息机制的理解。4. 防连点、线程泄漏与真机调试把报告收在有工程价值的地方地鼠被打中后同一只的得分结算有窗口期问题。玩家手速快时在一次冒头周期内连续点按理论上可以多次触发加分。解决方法是引入一个短锁private val clickLocked BooleanArray(9) // 命中时 if (clickLocked[index]) returnsetOnTouchListener true clickLocked[index] true view.postDelayed({ clickLocked[index] false }, 150L)150毫秒的锁窗口足以遮断同一次点击的重复事件又不会影响下一只地鼠的生成节奏。比用时间戳差值判断更省事。注意这个lock也要和地鼠收回逻辑分开因为锁的存活时间远短于MOLE_SHOW_MS如果共用一个定时任务地鼠收回去之后锁还没解开就会产生“刚冒头就打不中”的错判。Activity退出时的Handler清理是另一个高频扣分点。检查一下你的代码里是否遗漏了onDestroy中的这一行override fun onDestroy() { super.onDestroy() handler.removeCallbacksAndMessages(null) }如果不做这一步已经post出去的Runnable会持有Controller引用Activity无法被GC回收Logcat里会出现常见的LeakCanary警告。espresso跑完测试再反复进出页面这个泄漏非常容易复现。真机调试阶段建议直接用android debug bridge抓游戏日志而不是只盯着android studio的logcat窗口。在项目里给关键状态流转加上TAG为“MoleGame”的Log然后命令行过滤adb devices adb -s 设备序列号 logcat -s MoleGame:D-s MoleGame:D表示只看MoleGame这个TAG的Debug级别日志其余系统噪音全部滤掉。加-s时不带*:S默认会附带silent级别过滤其它TAG输出干净。用这种方式观察“生成→命中→锁定→收回”的状态序列比打断点更接近真实运行频率。最后说一个关于环境的热知识如果还在纠结android studio怎么设置中文我的建议是直接放弃汉化。界面语言对写代码没有任何帮助真正决定开发效率的是gradle构建速度和真机联调链路。把时间花在配置android sdk平台工具、解决一台实机的驱动识别上收益高得多。设计报告收尾时附上真机型号、android版本号和三条现场日志切片比写“经过测试运行正常”有说服力得多。本文还有配套的精品资源点击获取