谁小时候不想要一个游戏修改器?我把电脑 Skill 做成了 Android 小助手

小时候玩游戏,我最想要的东西不是攻略,而是一个属于自己的"修改器":不用背路线,不怕点错,还能自动完成那些重复又费眼睛的操作。
长大后我真的做了一个。不过它并不是直接修改金币、生命或者服务器数据,而是一个运行在我自己手机上的自动操作小助手:它会读取当前进度、理解游戏画面、判断下一步、操作手机,并在每一步之后检查结果。做着做着,我发现真正有意思的并不是"自动通关",而是如何把一个只能在电脑上运行的实验脚本,一点点做成可以安装到 Android 手机上的完整 App。
这篇文章记录的就是这条路线:先用电脑连接手机,把能力做成 Skill;再解决识别、误点、连续运行和通知;最后把 Python 核心搬进 Android App。
本文只讨论我在自己设备、单机游戏和学习环境中的自动化实验,不涉及服务器数据、付费系统或绕过安全机制。
一、先定目标:我要的不是"连点器"
一开始,我给项目定了六个很朴素的目标:
- 电脑能够连接并操作我的 Android 手机。
- 程序知道当前是哪一关、还剩多少内容。
- 它能判断下一步,而不是固定坐标乱点。
- 看不清时先旋转,确认后再点击。
- 一旦出现误点、生命减少或状态异常,立即停止并留下记录。
- 每关完成后自动进入下一关,并给我发通知。
这六条里,最重要的是第五条。自动化程序快并不难,难的是让它知道什么时候不应该继续。
开始前的准备
如果你也想沿着这条路线做一个自己的小助手,建议先准备:
- 一台自己的 Android 测试手机和一根可靠的数据线;
- Android Platform Tools,也就是
adb; - Python 3.11 运行电脑端工具;
- NumPy、SciPy 和 OpenCV 处理计算与画面;
- 做手机版时再准备 Android Studio、JDK 17、Python 3.10 和 Shizuku。
不要一上来就做完整 App。先在电脑上跑通"截图、判断一次、点击一次、验证一次",成功率和调试效率都会高很多。

二、第一阶段:用电脑连接手机
1. 先打通 ADB
电脑和 Android 手机之间最常见的调试通道是 ADB。手机打开开发者选项和 USB 调试后,先确认设备已授权:
bash
adb devices -l
接着测试三个最基础的动作:
bash
# 截取手机画面
adb exec-out screencap -p > screen.png
# 点击屏幕坐标
adb shell input tap 600 1300
# 模拟滑动
adb shell input swipe 800 1300 400 1300 1200
做到这里,电脑已经有了"眼睛"和"手"。但这仍然只是一个高级连点器。要让它自己完成任务,还需要让程序知道当前状态、哪里可以操作,以及操作后是否成功。
2. 把流程做成 Skill
我没有把所有命令堆进一个巨大的脚本,而是把操作规范和工具封装成了一个 Skill。它大致分成四层:
text
arrow-pro-3d-auto-solver/
├── SKILL.md # 操作规则和安全边界
├── scripts/
│ ├── session_state.py # 读取当前进度
│ ├── surface_solver.py # 计算当前可执行动作
│ ├── live_solver.py # 看画面、旋转、点击、验证
│ └── catalog_audit.py # 批量检查关卡数据
└── assets/ # 随游戏版本提取的关卡和视角资料
SKILL.md 很重要。它不仅告诉 AI "怎么做",还写清楚"什么情况下必须停":画面有弹窗时不点、姿态识别不可靠时不点、生命减少时不点、状态对不上时不点。
三、正确答案不是看截图猜出来的
我的实现并不是让模型看一张截图,然后凭感觉猜一根箭头。程序会先读取游戏本地的关卡资料和当前进度,得到一份确定的结构;再计算目前哪些目标可以安全处理。完成一个动作后,它会根据最新进度重新计算。
真正点击前,还要做一次"坐标翻译":关卡资料里的位置是三维坐标,而手机只能点击二维屏幕。程序会先识别物体当前朝向,把三维位置投影到手机画面,再检查这个点是不是落在真实可见的路径上、是否离旁边的目标足够远。
下面这张图是调试画面。绿色圆圈是优先点击位置,橙色圆圈是其他候选位置。红色路径也会被识别,不会因为颜色不同而漏掉。

整个执行循环可以简化成下面几行伪代码:
python
while level_not_finished:
state = read_current_state()
safe_targets = calculate_safe_targets(state)
if visible_target_exists(safe_targets):
tap_one_target()
verify_progress_and_lives()
else:
rotate_to_next_useful_view()
这里有一个刻意的限制:每次只点击一次,然后验证一次。 速度会慢一点,但不会因为一次判断错误连续点掉几条生命。

四、真正费时间的,是那些"不对劲"
第一版跑起来后,问题很快就出现了。
1. 同样的轮廓,不一定是同一个面
立方体和长方体旋转之后,有些角度的外轮廓非常相似。如果只看外形,程序可能把 A 面误认成 B 面,最后点击到错误位置。
我的修复思路是:不只比外轮廓,还要同时核对整条路径的位置。只有物体轮廓和路径身份都能对上,才允许点击。首次接管一个已经被玩家转过的页面时,先做一次不点击的全局识别,再进入正式执行。
2. 页面上明明有能消除的,为什么不点
"逻辑上能处理"和"画面上能安全点"是两件事。某个目标可能已经可消除,但它在当前角度下太靠边、被遮住,或者与旁边路径距离太近。这时程序宁愿先旋转,也不会用一个不可靠的坐标碰运气。
后续我增加了更全面的候选扫描和有限度的安全降级:只在目标正对镜头、路径像素足够清晰、周围有足够空间时,才允许使用稍宽松的候选点。
3. 空白面不要停留
有些旋转会把一个几乎没有目标的面转到正前方。早期版本仍会花时间做完整识别,看起来就像"对着白墙发呆"。后来我增加了空白面快速判断:有效路径像素很少时,直接短距离旋转到下一面,但最多连续跳过几次,避免因为追求速度而失去方向。
4. 飞走时没有颜色,也要记录
动画过程中,目标偶尔会变淡或者短暂没有颜色。如果程序正好在这个时刻截图,颜色识别就可能不稳定。我的处理不是马上补点,而是等待状态稳定,并以本地进度文件为最终依据。同时把未消除点击、意外消除、生命变化、姿态失败和运行异常写入 issues.jsonl,方便之后复现。

一条异常记录大概长这样:
json
{
"kind": "missed-tap",
"summary": "点击后未确认目标消失",
"level": 68,
"lives_before": 3,
"lives_after": 3,
"action": "stop-and-review"
}
这一步对后续优化帮助很大。没有记录时,我只能说"刚才好像哪里不对";有记录后,我能知道是哪一关、哪个角度、哪次点击、点击前后发生了什么。
五、从单关脚本到连续运行器
单关跑通之后,我又写了一个 continuous_runner.py,让它负责整个生命周期:
- 读取当前关卡和剩余进度;
- 调用单关求解器;
- 识别全屏广告并按返回键;
- 点击通关页面并等待下一关真实加载;
- 保存断点,程序中断后仍能从当前页面继续;
- 把通关结果放进通知队列,失败后自动重试。
通知使用独立线程发送,不会因为网络慢而挡住主流程。我还给消息加了幂等标识,避免程序重试时重复通知。

这时电脑端已经可以连续工作了,但新的问题也很明显:手机必须一直连着电脑。我要出门、合盖或者换电脑时,整套能力就停了。
六、第二阶段:把 Skill 搬进 Android App
我给自己的新目标是:日常使用时只拿手机,不再每次连接电脑。
最省事的方案不是重写整个求解器,而是复用已经验证过的 Python 核心,只替换设备控制层。

1. Android 端的技术选型
手机版使用了这些组件:
- 原生 Android 界面:配置当前显示关卡、启动、暂停和停止;
- 前台 Service:保证长时间运行,并显示持续通知;
- 悬浮窗:在游戏画面上直接控制暂停和停止;
- Shizuku:在手机本机执行截图和触控命令;
- Chaquopy:把 Python、NumPy、SciPy 和 OpenCV 打包进 APK;
- 本地通知与飞书 Webhook:每关完成后报告结果。
Gradle 中最关键的部分是 Python 和 Shizuku:
gradle
plugins {
id "com.android.application"
id "com.chaquo.python"
}
dependencies {
implementation "dev.rikka.shizuku:api:13.1.5"
implementation "dev.rikka.shizuku:provider:13.1.5"
}
chaquopy {
defaultConfig {
version = "3.10"
pip {
install "numpy==1.23.3"
install "scipy==1.8.1"
install "opencv-python==4.5.1.48"
}
}
}
2. 用适配器替换电脑 ADB
电脑版本里的核心代码只认识一个设备接口:截图、点击、滑动和读取文件。手机版实现了同样的接口,但底层改成 Shizuku。
python
class LocalDevice:
def screenshot(self):
return shizuku_shell("screencap -p")
def tap(self, x, y):
shizuku_shell(f"input tap {x} {y}")
def swipe(self, x1, y1, x2, y2, duration):
shizuku_shell(
f"input swipe {x1} {y1} {x2} {y2} {duration}"
)
这样,数值计算、画面识别和安全校验几乎不用重写。变的是"手和眼睛从哪里来",不是"大脑怎么思考"。
3. 手机上的使用界面
App 只保留实际需要的设置:Shizuku 授权、悬浮窗授权、当前关卡、通知配置和三个控制按钮。它不是展示功能的落地页,打开就是工具本身。

点击"启动并闯关"后,App 会把游戏切到前台,前台服务启动 Python 求解器。截图和旋转期间悬浮窗会临时隐藏,避免它被识别成游戏内容;操作完成后再显示出来。

七、真机调试比模拟器诚实得多
APK 能编译,只代表第一步完成。装到我的小米手机后,我又遇到了几个只有真机才会暴露的问题:
- 首次拉起游戏时,系统会弹出"是否允许打开其他应用",固定等待 900 毫秒就开始识别会失败。
- 悬浮窗如果不在截图前隐藏,会挡住游戏内容,改变识别结果。
- 游戏中的剩余数量已经变化,悬浮窗仍显示本关初始值,说明状态回调没有及时更新。
- 游戏必须保持前台。如果直接切到其他 App,自动点击可能落到别的界面,所以后台使用前必须先暂停。
这些问题让我更加确定:自动化产品不能只测试"成功路径"。权限弹窗、动画、网络慢、状态滞后和用户临时切后台,才是决定它是否真的能用的地方。
最终,手机端连续完成了多关测试,生命保持不变,并为每一关生成本地通知。

八、关于"脱离电脑"的真实边界
当前版本在 Shizuku 已运行时,可以只用手机启动、暂停和停止,日常闯关不需要连接电脑。但 Shizuku 不是 root 服务,手机重启后通常需要重新启动。可以通过 Android 无线调试在手机上启动,也可以临时连接电脑执行一次启动命令。
如果要做到重启后也完全不依赖电脑,下一版可以改用无障碍服务负责触控、MediaProjection 负责截图。不过这会增加常驻权限、录屏授权和系统兼容性工作,不能简单地把 Shizuku 删掉就结束。
九、我从这个项目里学到的五件事
- 先做可观察,再做自动化。 没有截图、状态、报告和异常日志,问题只能靠回忆。
- 一次只做一步。 每次动作都验证,远比一次发出几十个点击可靠。
- 识别"我看到了什么",也要识别"我现在在哪"。 姿态和身份不确定时,坐标再精确也没用。
- 跨平台优先做适配层。 把 ADB 换成 Shizuku时,核心算法可以继续复用。
- 用户的一句"这里明明能点",可能就是最有价值的测试用例。 红色目标、空白面和无色动画,都是这样进入优化列表的。
十、你也想从 0 做一个,可以按这个顺序
- 做最小演示: 电脑获取一张手机截图,再执行一次点击。
- 读取真实状态: 不依赖屏幕上的数字,找到稳定的本地进度来源。
- 只完成一个动作: 先证明程序能找到一个安全目标,不要急着连续运行。
- 补上验证和日志: 每次动作后检查结果,把异常保存下来。
- 再做连续运行: 处理过关页面、广告、重试、断点和通知。
- 最后迁移手机: 抽象设备接口,把电脑 ADB 替换成手机本地能力。
- 必须真机回归: 专门测试系统弹窗、切后台、悬浮窗、动画和网络慢。
这条路线看起来比"先画一个漂亮 App"慢,但它能确保每向前一步,都有一个真实可用的结果。
结语
小时候我以为"修改器"的快乐,是把生命改成 999,把所有关卡瞬间解锁。真正自己做了一次之后,我发现更有意思的是另一件事:把一个模糊的念头拆成可以验证的小步骤,再让它从电脑终端里一点点长成手机上的按钮、悬浮窗和通知。
它没有修改游戏世界,却修改了我完成重复工作的方式。
这大概就是长大以后,实现小时候梦想的一种方式。