前言:一个咸鱼玩家的觉醒
事情是这样的。
某天我下载了一款叫《咸鱼之王》的游戏。作为一个放置类游戏,它的核心玩法是——点点点。
- 每日任务要点
- 领奖励要点
- 打副本要点
- 抽卡要点
- 合成要点
- ……
玩了一周后,我的手指已经开始抗议了。作为一个程序员,我陷入了深深的思考:
我玩游戏,还是游戏玩我?
于是,"猫吃鱼"项目诞生了——让脚本替我吃掉这条咸鱼。
第一章:技术选型——为什么是 Auto.js
1.1 安卓自动化方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| Auto.js | 无需 root,基于无障碍服务,JavaScript 语法 | 官方版已下架,需要用开源版 |
| Tasker | 功能强大,生态丰富 | 学习曲线陡峭,自动化能力有限 |
| ADB + Python | 灵活,可电脑控制 | 需要连接电脑,不够便携 |
| 按键精灵 | 简单易用 | 功能有限,很多要付费 |
| Root + 触摸模拟 | 权限最高,啥都能干 | 需要 root,有变砖风险 |
最终选择 Auto.js(准确说是开源版 AutoX.js),原因:
- 无需 root:基于安卓无障碍服务(Accessibility Service)
- JavaScript 语法:前端出身的我表示很亲切
- 开源免费:AutoX.js 是社区维护的开源版本
- 功能全面:控件操作、图色识别、悬浮窗、定时任务一应俱全
1.2 环境搭建
# 1. 下载 AutoX.js
# GitHub: https://github.com/autox-community/AutoX
# 下载 APK 安装到手机
# 2. 开启无障碍服务
# 设置 -> 无障碍 -> AutoX.js -> 开启
# 3. 开启悬浮窗权限
# 设置 -> 应用管理 -> AutoX.js -> 悬浮窗权限
# 4. 关闭电池优化(重要!)
# 设置 -> 电池 -> AutoX.js -> 不优化
# 否则后台运行会被杀掉踩坑提醒:不同手机厂商的设置路径不一样,MIUI、ColorOS、EMUI 各有各的"特色"。有时候需要在"自启动管理"、"后台运行"等多个地方设置。
第二章:无障碍服务——脚本的灵魂
2.1 什么是无障碍服务
无障碍服务(Accessibility Service)本来是安卓为视障人士设计的功能,可以:
- 读取屏幕内容:获取当前界面所有控件的信息
- 模拟用户操作:点击、滑动、输入文字
- 监听系统事件:界面变化、通知到达等
简单说,开启无障碍服务后,你的脚本就像一个"看得见屏幕的机器人",可以知道屏幕上有什么,然后去操作它。
2.2 控件 vs 坐标
Auto.js 有两种主要的操作方式:
方式一:基于控件(推荐)
// 通过文字找到按钮并点击
click("领取奖励");
// 通过 id 找到控件
id("btn_claim").findOne().click();
// 通过描述找到控件
desc("关闭").findOne().click();方式二:基于坐标
// 直接点击屏幕坐标
click(540, 1200);
// 滑动
swipe(500, 1500, 500, 500, 500);对比:
| 特性 | 控件操作 | 坐标操作 |
|---|---|---|
| 适配性 | 好,不同分辨率通用 | 差,换手机就废 |
| 稳定性 | 高,控件存在才操作 | 低,可能点到别的地方 |
| 速度 | 稍慢,需要查找控件 | 快,直接点 |
| 适用场景 | 原生应用、有控件信息的界面 | 游戏画面、Canvas 渲染的界面 |
《咸鱼之王》的尴尬在于——它是个混合应用,部分界面有控件信息,部分界面是纯游戏渲染,需要两种方式结合使用。
2.3 布局分析——找到控件的"身份证"
Auto.js 自带布局分析功能,可以查看当前界面的控件树:
// 方法一:悬浮窗里点"布局分析"
// 方法二:代码打印
auto.waitFor(); // 等待无障碍服务启动
let root = className("android.widget.FrameLayout").findOne();
log(root); // 打印控件树布局分析会显示每个控件的:
text:显示的文字desc:描述(contentDescription)id:资源 IDclassName:控件类型bounds:位置和大小
技巧:优先用 text 和 id,它们最稳定。
第三章:项目架构——"猫吃鱼"的设计
3.1 功能规划
作为一个有追求的脚本,"猫吃鱼"要实现:
猫吃鱼
├── 日常任务
│ ├── 一键领取所有奖励
│ ├── 自动完成日常任务
│ └── 自动观看广告
├── 战斗系统
│ ├── 自动挂机刷副本
│ ├── 自动打竞技场
│ └── 自动参与活动
├── 资源管理
│ ├── 自动合成装备
│ ├── 自动强化英雄
│ └── 自动使用体力
└── 辅助功能
├── 防检测(随机延迟)
├── 异常处理(弹窗、卡顿)
└── 运行日志3.2 代码结构
cat_eat_fish/
├── main.js # 主入口
├── config.js # 配置文件
├── modules/
│ ├── daily.js # 日常任务
│ ├── battle.js # 战斗相关
│ ├── resource.js # 资源管理
│ └── utils.js # 工具函数
├── lib/
│ ├── ui.js # 悬浮窗 UI
│ ├── logger.js # 日志模块
│ └── ocr.js # 文字识别(可选)
└── images/ # 图片资源(用于找图)
├── btn_claim.png
├── icon_close.png
└── ...3.3 配置文件设计
// config.js
module.exports = {
// 游戏包名
packageName: "com.xianyuwang.game",
// 功能开关
features: {
autoDailyTask: true, // 自动日常
autoWatchAd: false, // 自动看广告(费流量)
autoBattle: true, // 自动战斗
autoMerge: true, // 自动合成
},
// 时间配置(毫秒)
timing: {
shortDelay: [300, 600], // 短延迟范围
mediumDelay: [800, 1500], // 中延迟范围
longDelay: [2000, 4000], // 长延迟范围
pageLoadWait: 2000, // 页面加载等待
},
// 战斗配置
battle: {
maxRounds: 100, // 最大战斗轮数
autoRetry: true, // 失败自动重试
},
// 安全配置
safety: {
randomDelay: true, // 随机延迟(防检测)
maxRunTime: 3600000, // 最大运行时间(1小时)
}
};第四章:核心代码实现
4.1 工具函数——脚本的基础设施
// modules/utils.js
/**
* 随机延迟,模拟人类操作
*/
function randomSleep(min, max) {
let delay = Math.floor(Math.random() * (max - min + 1)) + min;
sleep(delay);
return delay;
}
/**
* 安全点击 - 基于文字
* @param {string} text - 要点击的文字
* @param {number} timeout - 超时时间(毫秒)
* @returns {boolean} - 是否点击成功
*/
function safeClickText(text, timeout = 5000) {
let target = textContains(text).findOne(timeout);
if (target) {
let bounds = target.bounds();
// 点击控件中心,加一点随机偏移
let x = bounds.centerX() + random(-5, 5);
let y = bounds.centerY() + random(-5, 5);
click(x, y);
log(`点击成功: ${text}`);
randomSleep(300, 600);
return true;
}
log(`未找到: ${text}`);
return false;
}
/**
* 安全点击 - 基于 ID
*/
function safeClickId(targetId, timeout = 5000) {
let target = id(targetId).findOne(timeout);
if (target) {
target.click();
log(`点击成功: ${targetId}`);
randomSleep(300, 600);
return true;
}
log(`未找到: ${targetId}`);
return false;
}
/**
* 等待页面加载
*/
function waitForPage(identifier, timeout = 10000) {
let startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (text(identifier).exists() || id(identifier).exists()) {
log(`页面已加载: ${identifier}`);
randomSleep(500, 1000);
return true;
}
sleep(500);
}
log(`页面加载超时: ${identifier}`);
return false;
}
/**
* 处理常见弹窗
*/
function handlePopups() {
// 关闭按钮
if (desc("关闭").exists()) {
desc("关闭").findOne().click();
log("关闭弹窗");
return true;
}
// 确定按钮
if (text("确定").exists()) {
click("确定");
log("点击确定");
return true;
}
// 广告关闭
if (id("ad_close").exists()) {
id("ad_close").findOne().click();
log("关闭广告");
return true;
}
return false;
}
/**
* 返回主界面
*/
function goHome() {
let maxTries = 5;
while (maxTries > 0) {
if (text("主城").exists() || id("main_page").exists()) {
log("已在主界面");
return true;
}
back();
randomSleep(800, 1200);
handlePopups();
maxTries--;
}
log("返回主界面失败");
return false;
}
module.exports = {
randomSleep,
safeClickText,
safeClickId,
waitForPage,
handlePopups,
goHome
};4.2 日常任务模块
// modules/daily.js
const utils = require('./utils.js');
const config = require('../config.js');
/**
* 领取所有可领取的奖励
*/
function claimAllRewards() {
log("===== 开始领取奖励 =====");
// 领取邮件奖励
claimMailRewards();
// 领取任务奖励
claimTaskRewards();
// 领取活动奖励
claimEventRewards();
log("===== 奖励领取完成 =====");
}
/**
* 领取邮件奖励
*/
function claimMailRewards() {
log("检查邮件...");
// 点击邮件图标
if (!utils.safeClickText("邮件")) {
log("未找到邮件入口");
return;
}
utils.waitForPage("邮件列表");
// 一键领取
if (text("一键领取").exists()) {
utils.safeClickText("一键领取");
utils.randomSleep(1000, 1500);
utils.handlePopups(); // 处理领取成功弹窗
} else {
log("没有可领取的邮件");
}
utils.goHome();
}
/**
* 领取任务奖励
*/
function claimTaskRewards() {
log("检查任务奖励...");
// 打开任务界面
if (!utils.safeClickText("任务")) {
log("未找到任务入口");
return;
}
utils.waitForPage("每日任务");
// 循环领取所有可领取的奖励
let claimed = 0;
while (true) {
// 查找"领取"按钮
let claimBtn = text("领取").findOne(2000);
if (claimBtn) {
claimBtn.click();
claimed++;
utils.randomSleep(500, 800);
utils.handlePopups();
} else {
break;
}
// 防止死循环
if (claimed > 20) break;
}
log(`共领取 ${claimed} 个任务奖励`);
utils.goHome();
}
/**
* 自动完成简单任务
*/
function autoCompleteTasks() {
log("===== 开始自动任务 =====");
// 每日签到
dailyCheckIn();
// 免费抽卡
freeGacha();
// 领取体力
claimStamina();
// 快速战斗
quickBattle();
log("===== 自动任务完成 =====");
}
/**
* 每日签到
*/
function dailyCheckIn() {
log("执行每日签到...");
if (utils.safeClickText("签到")) {
utils.waitForPage("签到");
// 点击签到按钮
if (text("签到").exists()) {
utils.safeClickText("签到");
utils.randomSleep(1000, 1500);
}
utils.handlePopups();
utils.goHome();
}
}
/**
* 免费抽卡
*/
function freeGacha() {
log("检查免费抽卡...");
if (utils.safeClickText("召唤")) {
utils.waitForPage("召唤");
// 查找免费按钮
if (textContains("免费").exists()) {
utils.safeClickText("免费");
utils.randomSleep(3000, 4000); // 等待抽卡动画
// 跳过动画或关闭结果
click(device.width / 2, device.height / 2);
utils.randomSleep(500, 800);
utils.handlePopups();
} else {
log("今日免费次数已用完");
}
utils.goHome();
}
}
module.exports = {
claimAllRewards,
autoCompleteTasks
};4.3 战斗模块
// modules/battle.js
const utils = require('./utils.js');
const config = require('../config.js');
/**
* 自动刷副本
* @param {string} dungeonName - 副本名称
* @param {number} times - 刷取次数
*/
function autoDungeon(dungeonName, times = 10) {
log(`===== 开始刷副本: ${dungeonName} =====`);
// 进入副本界面
if (!enterDungeon(dungeonName)) {
log("进入副本失败");
return;
}
let successCount = 0;
let failCount = 0;
for (let i = 0; i < times; i++) {
log(`第 ${i + 1}/${times} 次战斗`);
// 检查体力
if (!checkStamina()) {
log("体力不足,停止刷副本");
break;
}
// 开始战斗
if (startBattle()) {
// 等待战斗结束
let result = waitBattleEnd();
if (result === "win") {
successCount++;
log(`战斗胜利!当前 ${successCount} 胜 ${failCount} 负`);
} else if (result === "lose") {
failCount++;
log(`战斗失败... 当前 ${successCount} 胜 ${failCount} 负`);
if (!config.battle.autoRetry) {
log("配置为失败不重试,停止");
break;
}
} else {
log("战斗状态异常");
utils.handlePopups();
}
} else {
log("开始战斗失败");
break;
}
// 战斗间隔
utils.randomSleep(1500, 2500);
}
log(`副本完成: ${successCount} 胜 ${failCount} 负`);
utils.goHome();
}
/**
* 进入副本
*/
function enterDungeon(dungeonName) {
// 打开副本界面
if (!utils.safeClickText("副本") && !utils.safeClickText("冒险")) {
return false;
}
utils.randomSleep(1000, 1500);
// 选择具体副本
if (utils.safeClickText(dungeonName)) {
utils.randomSleep(800, 1200);
return true;
}
// 如果没找到,可能需要滑动查找
for (let i = 0; i < 3; i++) {
swipe(540, 1500, 540, 800, 500);
utils.randomSleep(500, 800);
if (utils.safeClickText(dungeonName)) {
utils.randomSleep(800, 1200);
return true;
}
}
return false;
}
/**
* 开始战斗
*/
function startBattle() {
// 点击挑战按钮
if (utils.safeClickText("挑战") || utils.safeClickText("战斗") || utils.safeClickText("开始")) {
utils.randomSleep(1500, 2000);
return true;
}
return false;
}
/**
* 等待战斗结束
*/
function waitBattleEnd(timeout = 120000) {
let startTime = Date.now();
while (Date.now() - startTime < timeout) {
// 检查胜利
if (textContains("胜利").exists() || textContains("Victory").exists()) {
utils.randomSleep(1000, 1500);
// 点击继续或关闭
utils.safeClickText("继续") || utils.safeClickText("确定") || click(540, 1600);
return "win";
}
// 检查失败
if (textContains("失败").exists() || textContains("Defeat").exists()) {
utils.randomSleep(1000, 1500);
utils.safeClickText("确定") || click(540, 1600);
return "lose";
}
// 处理可能的弹窗
utils.handlePopups();
sleep(1000);
}
log("战斗超时");
return "timeout";
}
/**
* 检查体力是否充足
*/
function checkStamina(minStamina = 10) {
// 这里需要根据游戏实际 UI 来获取体力值
// 方法1:OCR 识别
// 方法2:找特定控件读取
// 方法3:检查"体力不足"提示
// 简化处理:如果没有"体力不足"提示就认为够用
return !textContains("体力不足").exists();
}
/**
* 自动竞技场
*/
function autoArena(times = 5) {
log(`===== 开始竞技场 ${times} 次 =====`);
// 进入竞技场
if (!utils.safeClickText("竞技场") && !utils.safeClickText("PVP")) {
log("未找到竞技场入口");
return;
}
utils.waitForPage("竞技场");
for (let i = 0; i < times; i++) {
log(`竞技场第 ${i + 1}/${times} 场`);
// 选择对手(选第一个,一般最弱)
let opponent = className("android.widget.LinearLayout")
.findOne(3000);
if (opponent) {
opponent.click();
utils.randomSleep(500, 800);
// 点击挑战
if (startBattle()) {
waitBattleEnd(60000); // 竞技场战斗一般较快
}
} else {
log("未找到对手");
break;
}
utils.randomSleep(2000, 3000);
}
utils.goHome();
}
module.exports = {
autoDungeon,
autoArena
};4.4 主程序入口
// main.js
"ui"; // 启用 UI 模式
const config = require('./config.js');
const daily = require('./modules/daily.js');
const battle = require('./modules/battle.js');
const utils = require('./modules/utils.js');
// ==================== UI 界面 ====================
ui.layout(
<vertical padding="16">
<text text="🐱 猫吃鱼 - 咸鱼之王助手" textSize="22sp" textColor="#333333" gravity="center" marginBottom="16"/>
<horizontal>
<checkbox id="cbDaily" text="日常任务" checked="true"/>
<checkbox id="cbBattle" text="自动副本" checked="true" marginLeft="16"/>
</horizontal>
<horizontal marginTop="8">
<checkbox id="cbArena" text="竞技场" checked="false"/>
<checkbox id="cbAd" text="看广告" checked="false" marginLeft="16"/>
</horizontal>
<horizontal marginTop="16">
<text text="副本次数: "/>
<input id="inputTimes" text="10" inputType="number" w="80"/>
</horizontal>
<horizontal marginTop="24" gravity="center">
<button id="btnStart" text="开始运行" w="120" style="Widget.AppCompat.Button.Colored"/>
<button id="btnStop" text="停止" w="100" marginLeft="16"/>
</horizontal>
<text text="运行日志:" marginTop="16"/>
<scroll h="200">
<text id="txtLog" textSize="12sp" textColor="#666666"/>
</scroll>
<text text="提示: 请先打开游戏,再点击开始运行" textSize="12sp" textColor="#999999" marginTop="8"/>
</vertical>
);
// ==================== 全局变量 ====================
let isRunning = false;
let mainThread = null;
// ==================== 日志函数 ====================
function appendLog(msg) {
let time = new Date().toLocaleTimeString();
let logText = `[${time}] ${msg}\n`;
ui.run(() => {
ui.txtLog.setText(ui.txtLog.text() + logText);
});
console.log(msg);
}
// 重写 log 函数
log = appendLog;
// ==================== 事件处理 ====================
ui.btnStart.on("click", () => {
if (isRunning) {
toast("脚本正在运行中");
return;
}
// 检查无障碍服务
if (!auto.service) {
toast("请先开启无障碍服务");
app.startActivity({
action: "android.settings.ACCESSIBILITY_SETTINGS"
});
return;
}
isRunning = true;
ui.btnStart.setText("运行中...");
mainThread = threads.start(function() {
try {
runScript();
} catch (e) {
log("发生错误: " + e);
} finally {
isRunning = false;
ui.run(() => {
ui.btnStart.setText("开始运行");
});
}
});
});
ui.btnStop.on("click", () => {
if (mainThread) {
mainThread.interrupt();
mainThread = null;
}
isRunning = false;
ui.btnStart.setText("开始运行");
log("脚本已停止");
});
// ==================== 主逻辑 ====================
function runScript() {
log("🐱 猫吃鱼启动!");
// 确保在游戏中
let packageName = config.packageName;
if (currentPackage() !== packageName) {
log("正在启动游戏...");
app.launchPackage(packageName);
sleep(8000); // 等待游戏加载
}
// 处理可能的开屏弹窗
utils.randomSleep(2000, 3000);
utils.handlePopups();
// 执行日常任务
if (ui.cbDaily.checked) {
daily.claimAllRewards();
daily.autoCompleteTasks();
}
// 执行自动副本
if (ui.cbBattle.checked) {
let times = parseInt(ui.inputTimes.text()) || 10;
battle.autoDungeon("普通副本", times);
}
// 执行竞技场
if (ui.cbArena.checked) {
battle.autoArena(5);
}
log("🎉 所有任务完成!");
}
// ==================== 脚本退出处理 ====================
events.on("exit", function() {
if (mainThread) {
mainThread.interrupt();
}
log("脚本退出");
});第五章:进阶技巧——让脚本更稳定
5.1 图色识别——当控件失效时
有些游戏界面是纯 Canvas 绘制的,没有控件信息。这时需要用图色识别:
// 找图
let img = images.read("/sdcard/cat_eat_fish/images/btn_claim.png");
let point = findImage(captureScreen(), img, {
threshold: 0.8, // 相似度阈值
region: [0, 0, device.width, device.height] // 搜索区域
});
if (point) {
log(`找到图片: (${point.x}, ${point.y})`);
click(point.x + 10, point.y + 10); // 点击图片中心偏移
}
img.recycle(); // 释放图片资源,很重要!
// 找色
let color = "#FF5722"; // 目标颜色
let p = findColor(captureScreen(), color, {
region: [100, 200, 300, 400],
threshold: 10
});
if (p) {
log(`找到颜色: (${p.x}, ${p.y})`);
}注意:找图前需要获取截图权限:
if (!requestScreenCapture()) {
toast("请授予截图权限");
exit();
}5.2 多分辨率适配
// 获取设备信息
let screenWidth = device.width;
let screenHeight = device.height;
// 设计基准(以 1080x1920 为基准)
const BASE_WIDTH = 1080;
const BASE_HEIGHT = 1920;
// 坐标转换函数
function scaleX(x) {
return Math.floor(x * screenWidth / BASE_WIDTH);
}
function scaleY(y) {
return Math.floor(y * screenHeight / BASE_HEIGHT);
}
// 使用
click(scaleX(540), scaleY(1200));5.3 异常恢复机制
/**
* 带重试的操作包装器
*/
function withRetry(action, maxRetries = 3, delay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try {
if (action()) {
return true;
}
} catch (e) {
log(`操作失败 (${i + 1}/${maxRetries}): ${e}`);
}
sleep(delay);
}
return false;
}
/**
* 全局异常处理
*/
function globalErrorHandler() {
// 检测是否还在游戏中
if (currentPackage() !== config.packageName) {
log("检测到离开游戏,尝试返回");
app.launchPackage(config.packageName);
sleep(5000);
return;
}
// 处理常见弹窗
if (utils.handlePopups()) {
return;
}
// 尝试返回主界面
utils.goHome();
}
// 定时执行异常检测
setInterval(globalErrorHandler, 30000);5.4 防检测措施
游戏可能会检测自动化脚本,一些防护措施:
// 1. 随机延迟(最重要)
function humanLikeDelay() {
// 模拟人类操作的不规则延迟
let base = random(300, 800);
// 偶尔来个长延迟,像是在思考
if (random(1, 10) === 1) {
base += random(1000, 3000);
}
sleep(base);
}
// 2. 随机点击位置
function humanLikeClick(x, y, range = 10) {
let actualX = x + random(-range, range);
let actualY = y + random(-range, range);
click(actualX, actualY);
}
// 3. 随机滑动速度
function humanLikeSwipe(x1, y1, x2, y2) {
let duration = random(400, 800);
swipe(x1, y1, x2, y2, duration);
}
// 4. 设置随机的运行时间,不要 24 小时挂机
function shouldTakeBreak() {
// 每运行 1-2 小时休息一下
// ...
}第六章:踩坑记录——血与泪的教训
6.1 坑 1:无障碍服务被杀
现象:脚本运行一段时间后自动停止
原因:安卓系统的电池优化会杀后台应用
解决:
- 关闭 AutoX.js 的电池优化
- 允许后台运行
- 开启前台服务(悬浮窗)
- 部分手机需要加入白名单
6.2 坑 2:控件找不到
现象:findOne() 返回 null
可能原因:
- 控件还没加载出来 → 加等待
- 控件在 WebView 里 → 用
className("android.webkit.WebView")先找到 WebView - 游戏用 Canvas 绘制 → 改用图色识别
- 控件被遮挡 → 先关闭弹窗
6.3 坑 3:click() 没反应
现象:代码执行了但没效果
可能原因:
- 点击位置不对 → 用布局分析确认
- 需要长按而不是点击 → 用
longClick() - 控件不可点击 → 点击其父控件
- 有透明遮罩层 → 先处理遮罩
// 点击父控件
let target = text("目标").findOne();
if (target && !target.clickable()) {
target.parent().click();
}6.4 坑 4:图片资源管理
现象:运行一段时间后内存爆了
原因:images.read() 和 captureScreen() 创建的图片没有释放
解决:
let img = captureScreen();
// ... 使用图片
img.recycle(); // 用完一定要 recycle!
// 或者用 images.copy() 复制一份
let copy = images.copy(img);
img.recycle();第七章:最终效果与感悟
7.1 成果展示
经过两周的开发迭代,"猫吃鱼"实现了:
- ✅ 自动领取所有奖励(邮件、任务、活动)
- ✅ 自动完成日常任务(签到、免费抽卡)
- ✅ 自动刷副本(支持配置次数)
- ✅ 自动竞技场
- ✅ 异常恢复(弹窗处理、游戏崩溃重启)
- ✅ 运行日志和统计
每天早上起床,打开脚本,等刷完牙洗完脸,日常任务已经做完了。
时间节省:从每天手动操作 30-40 分钟,到现在只需要点一下"开始"。
7.2 一些感悟
- 自动化的本质是理解规律
写脚本的过程,其实是在深入理解游戏机制。哪些操作是固定的?哪些是随机的?什么时候会弹窗?理解了这些,脚本才能稳定运行。 - 程序员思维 vs 产品思维
一开始我想把功能做得很全,后来发现 80% 的收益来自 20% 的功能。与其花大量时间做"完美"的脚本,不如先做好核心功能。 - 边界情况永远比预想的多
网络波动、游戏更新、意外弹窗... 真实环境比测试复杂得多。要有足够的异常处理和日志记录。 - 关于游戏和脚本的思考
用脚本"解放双手"后,我反而开始思考:如果一个游戏需要用脚本才能"好玩",那它真的好玩吗?
附录:常用 API 速查表
控件操作
// 查找控件
text("文字").findOne(timeout)
textContains("部分文字").find()
id("resource_id").findOne()
desc("描述").findOne()
className("android.widget.Button").find()
// 组合查找
text("文字").className("Button").findOne()
// 控件操作
widget.click()
widget.longClick()
widget.setText("文字")
widget.bounds() // 获取位置
widget.parent() // 父控件
widget.children() // 子控件`
### 手势操作
javascript
`click(x, y)
longClick(x, y)
swipe(x1, y1, x2, y2, duration)
gesture(duration, [x1, y1], [x2, y2], ...)
press(x, y, duration)图色操作
requestScreenCapture() // 申请截图权限
captureScreen() // 截图
images.read(path) // 读取图片
findImage(img, template, options) // 找图
findColor(img, color, options) // 找色
images.pixel(img, x, y) // 获取像素颜色
img.recycle() // 释放资源应用操作
app.launchPackage(packageName) // 启动应用
currentPackage() // 当前应用包名
currentActivity() // 当前 Activity
back() // 返回键
home() // Home 键结语
从一个想偷懒的念头,到一个能稳定运行的自动化脚本,"猫吃鱼"项目让我体会到了编程的乐趣——用代码解决实际问题。
当然,这只是自动化入门。更复杂的场景可能需要:
- OCR 文字识别
- 机器学习判断场景
- 云控多设备管理
- ...
但对于一个咸鱼玩家来说,现在的"猫吃鱼"已经足够了。
毕竟,真正的咸鱼,是让脚本替自己咸的。