先回答一个高频问题:安卓和 iOS 能用同一套代码吗
写安卓脚本开发教程的人常被问这个问题,做 iOS 脚本开发的人更常问------因为网上能搜到的例子,大多是在安卓上写的,换个平台就不知道能不能用。
能,但要分清"语法层"和"环境层"。
语法层:完全同一份代码。 按键精灵移动版在安卓和 iOS 上用的都是 MQ(Q语法) ,没有"安卓版语法"和"iOS 版语法"的区别。Dim、{null}、Len()、For Each k, v、Function ... End Function、Exit Function,两端一模一样。所以本文十三章的代码,你在 iOS 手机上开发时可以直接抄,一行不用改。
环境层:三件事必须重新确认。
|---------|----------------------------------------------------------------------|
| 事项 | 说明 |
| 引擎版本 | iOS 有新版引擎和旧版引擎两条路,最低系统版本要求不同(新版引擎起点高)。动手前先去看对应命令文档里的"支持环境"表,别看别人的教程猜 |
| 坐标系与分辨率 | 安卓机的分辨率五花八门,iPhone 还有像素倍率。同一台游戏界面,坐标和安卓完全对不上,区域扫描前必须按自己的设备重新标定 |
| 部署方式 | iOS 端的安装与授权方式跟安卓完全不同(越狱 / 免越狱两条路,授权不互通),这块跟本文语法无关,按官网当期说明为准 |
结论 :算法和写法不用改,坐标和像素参数一定要重测。本文第九、十二章会反复强调这一点------iOS 手机开发里那些"代码跑通了但结果不对"的情况,根源多半是坐标没重标,而不是语法写错。
一、为什么"取极值"值得单独写一篇
脚本里凡是带"最"字的需求,本质都是取极值:
- 背包里最少的那个材料 ID
- 邮件列表里最新的一封
- 地图上最近的怪
- Boss 列表里最早刷新的那一只
- 血条最短的那个敌人
- 截屏区域里最靠上的那个色点(判断箭头指向)
这些需求长得完全不一样,但底层是同一个循环 。学会这一个循环,上面六个场景你只需要换 If 里的比较对象。
本文从最裸的 10 行代码开始,一路推到封装函数、多字段极值、二维范围极值,最后把踩过的坑一次性摊开。不管是安卓脚本开发教程里那些传了多年的老例子,还是 iOS 端新引擎上的写法,底下的循环都是同一个。
二、极值的通用模型:擂台法
极值问题的标准解法叫打擂台,三个要素缺一不可:
|------|-----------|-----------------------------|
| 要素 | 作用 | 代码里的角色 |
| 擂主 | 存放"当前最优值" | 一个变量(或一对变量) |
| 挑战者 | 待比较的新数据 | 数组/表里的元素 |
| 打擂规则 | 谁更优谁上位 | If 挑战者 > 擂主 Then 擂主 = 挑战者 |
流程就三句话:
1. 立一个擂主(给初始值)
2. 所有数据挨个上台挑战(遍历)
3. 打完之后台上站着的就是极值(输出)
只要遍历是"每个元素都过一遍且只过一遍",结果就一定对。这是它比排序可靠的地方------排序要 O(n log n),擂台只要 O(n),而且不需要额外内存。
三、最小可用版:10 行代码找最小值
拿一组数字找最小值,完整的可运行代码:
// 功能:在一维数组中找最小值
// 说明:Q语法数组下标从 1 开始,Len() 返回元素个数
Dim arr = {12, 5, 8, 4, 3, 66, 7, 8}
Dim num = 999999 // 擂主初始值
Dim i
For i = 1 To Len(arr)
TracePrint "第" & i & "个挑战者:" & arr[i]
If arr[i] < num Then
num = arr[i]
TracePrint " → 擂主换人,现在是 " & num
End If
Next
TracePrint "最终最小值:" & num
拆开看这四步:
第 1 步 · 准备原始数据
Dim arr = {12, 5, 8, 4, 3, 66, 7, 8}
{} 在 MQ 里是表(table),带下标访问时当数组用。注意两个容易翻车的点:
- 下标从 1 开始,不是 0
- 长度用
Len(arr),不是arr.length,也不是UBound(UBound能用,但对表不如Len稳)
第 2 步 · 立擂主
Dim num = 999999
这里给了一个"非常大"的数。为什么?因为我们要找最小 值,只要初始值比所有真实数据都大,它就不可能被当成答案留下来。这叫哨兵值(sentinel)。
反过来找最大值,初始值就该给一个"非常小"的数(比如 -999999)。
第 3 步 · 遍历
For i = 1 To Len(arr)
For i = 1 To Len(arr) 是 MQ 里遍历数组的标准写法。不要写 To Len(arr) - 1,那是 C 系语言 0 下标留下的肌肉记忆,在 MQ 里会漏掉最后一个元素。
第 4 步 · 打擂
If arr[i] < num Then
num = arr[i]
End If
就这三行。< 换成 > 就是最大值。
这十行也是最好的第一个练手例子:你在找安卓脚本开发教程里的入门练习,或者刚上手 iOS 脚本开发想验证环境是否正常,都可以直接跑它 。因为它只用到了 Dim、Len、For、If 和 TracePrint,不依赖任何插件、不碰屏幕坐标,跑出来就是 "最终最小值:3"------环境对不对,一眼就知道。
四、擂台初始值怎么选:三种策略
哨兵法好用,但有个前提:你得知道数据的取值范围。刷新时间是秒,给 999999 够用;但如果是毫秒时间戳(13 位),999999 就比所有数据都小了,结果直接错。
三种策略,按推荐度排:
策略 A:拿第一个元素当擂主(最推荐)
If Len(arr) = 0 Then
TracePrint "数组为空,没有极值"
Else
Dim num = arr[1]
Dim i
For i = 2 To Len(arr)
If arr[i] < num Then
num = arr[i]
End If
Next
TracePrint "最小值:" & num
End If
优点:跟数据量级完全解耦 ,多大都不怕;循环还少跑一次(从 2 开始)。 代价:必须先判空,否则 arr[1] 取到 null,下一步比较直接报运行时错误。
这是工程代码里的标准写法。下面所有封装函数都用它。
策略 B:哨兵法(数据量级已知时最省事)
Dim num = 999999
优点:不用判空,空数组也能跑出一个"看起来对"的结果。 缺点:量级估错就静默出错,最难查。
策略 C:用 null 当"还没人上台"
Dim num = null
For i = 1 To Len(arr)
If num = null Then
num = arr[i]
Else
If arr[i] < num Then
num = arr[i]
End If
End If
Next
优点:语义最干净,第一个元素自动上位。 缺点:每个元素多一次判空,代码也更啰嗦。
结论:日常写脚本用 A,图快用 B,写给别人用的库用 A。
五、既要知道极值,还要知道"是谁"
实际需求里,"最小值是 3"没有意义,有意义的是"最小值 3 对应的那个道具叫什么"。
那就再立一个"跟班变量",跟着擂主一起换:
// 功能:找出最小值的下标(下标即身份)
Dim arr = {12, 5, 8, 4, 3, 66, 7, 8}
Dim minVal = arr[1]
Dim minIdx = 1
Dim i
For i = 2 To Len(arr)
If arr[i] < minVal Then
minVal = arr[i]
minIdx = i // 关键:值换,身份也换
End If
Next
TracePrint "最小值是 " & minVal & ",在数组第 " & minIdx & " 位"
这里是最容易写错的地方 :很多人只更新了 minVal,忘了更新 minIdx,最后拿到的是"最小值 + 第一个元素的下标",跑起来不报错,结果错得悄无声息。
记住一句话:擂主换人时,跟班必须一起换。
六、实战一:Boss 刷新时间最近的那一只
原始需求:有一张 {Boss名: 刷新秒数} 的表,要找出刷新时间最短的那只 Boss 和它的时间。
这是我在安卓脚本开发和 iOS 脚本开发里见到复用率最高的一个需求原型------换成副本冷却、技能 CD、任务倒计时,逻辑一个字都不用改。所以这一节我给了两个版本:先给能用的,再拆不能用的。
6.1 先说结论版(一趟遍历,推荐)
// 功能:在 {Boss名: 刷新秒数} 表中找出刷新最快的一只
// 返回:{刷新秒数, Boss名}
Dim BossTimeTable = {"怪名称1": 150, "怪名称2": 300, "怪名称3": 1150, "怪名称4": 350}
Function 最快刷新(BossTimeTable)
Dim k, v, bestName, bestTime, isFirst
isFirst = True
For Each k, v In BossTimeTable
If isFirst Then
bestName = k
bestTime = v
isFirst = False
Else
If v < bestTime Then
bestName = k
bestTime = v
End If
End If
Next
最快刷新 = {bestTime, bestName}
End Function
Dim ret = 最快刷新(BossTimeTable)
TracePrint "最近的 Boss:" & ret[2] & ",还有 " & ret[1] & " 秒刷新"
TracePrint Encode.TableToJson(ret)
输出:
最近的 Boss:怪名称1,还有 150 秒刷新
[150,"怪名称1"]
这段代码做到了一件很关键的事:不拆分数据。
哈希表的 For Each k, v 天然能同时拿到键和值,所以"名称"和"时间"从头到尾没有分开过,不存在"两个平行数组下标对不上"的风险。
6.2 网上常见的"分离数组 + 替换法"为什么坑
原始教程里有另一种写法,思路是:先把哈希表拆成两个平行数组,再用"替换法"把小的往前挤。能跑,但有三个坑:
// ⚠️ 这段是问题代码,仅用于说明坑在哪
Dim BossTime = {null}
Dim BossName = {null}
For Each k, v In BossTimeTable
BossTime[Len(BossTime)] = v // 坑 1
BossName[Len(BossName)] = k
Next
For i = 1 To UBound(BossTime) // 坑 2
If BossTime[i] < BossTime[i + 1] Then
BossTime[i + 1] = BossTime[i] // 坑 3
BossName[i + 1] = BossName[i]
End If
Next
坑 1:索引从 0 开始写了。 Len({null}) 返回 0(官方文档里判空就是用 If Len(t) = 0),所以第一次写入落在 0 号位 。于是数组变成了 [0]=第1个, [1]=第2个... 这种"缺一位"的状态,Len() 的计数和 For i = 1 To ... 的遍历全部错位,第一个 Boss 事实上被丢掉了。 正确写法只有一处差别:
BossTime[Len(BossTime) + 1] = v
坑 2:i + 1 越界。 For i = 1 To UBound(...),最后一次 i 已经等于上界,BossTime[i + 1] 就取到了表外,拿到 null 再去比较,运行时直接报错。要用相邻比较,循环上界必须减 1。
坑 3:替换法污染原数据。 它把结果往回写进同一个数组,跑完之后原始刷新时间已经被覆盖,想再取"第二快"就得重新建数据。而且为了走这条路,你还得额外维护两个平行数组,一旦中间 Next 顺序有任何变化,名称和时间立刻错配。
所以我的建议是 :取极值就用单一擂主变量,不要复用数据容器。容器是只读的,擂主是唯一的写入点,这样代码可预测得多。
七、实战二:一次封装,Max/Min 通吃
MQ 没有内置 Min() / Max(),也不支持把函数当参数传(没有函数指针),所以想让最大值和最小值共用一套逻辑,只能用布尔开关:
// 功能:通用擂台函数
// 参数:list - 一维数组
// isMax - True 取最大值,False 取最小值
// 返回:极值;空数组返回 null
Function 擂台(list, isMax)
Dim i
If Len(list) = 0 Then
擂台 = null
Exit Function
End If
擂台 = list[1]
For i = 2 To Len(list)
If isMax Then
If list[i] > 擂台 Then
擂台 = list[i]
End If
Else
If list[i] < 擂台 Then
擂台 = list[i]
End If
End If
Next
End Function
// ------ 用法 ------
Dim arr = {12, 5, 8, 4, 3, 66, 7, 8}
TracePrint "最小值:" & 擂台(arr, False)
TracePrint "最大值:" & 擂台(arr, True)
TracePrint "空数组:" & 擂台({null}, True) // ❌ 拼接 null 会报错
// ✅ 调用方必须自己判一次 null
Dim e = 擂台({null}, True)
If e = null Then
TracePrint "空数组:没有极值"
Else
TracePrint "空数组:" & e
End If
几个细节值得说:
Exit Function之后不再执行,擂台 = null必须先赋值------MQ 的返回值靠给函数名赋值 ,不是return。
{null}是 MQ 里"空表"的写法,官方文档明确要求空表必须写成{null},{}不在语法范围内。
- 空数组返回
null而不是 0,是为了让调用方能区分"没有数据"和"极值就是 0"。
- 但
null一旦参与字符串拼接或数值比较就会报错,所以调用方也得判一次 。上面那段If e = null不是啰嗦,是必须的。
同样的思路可以做成"取最小值对应的对象":
// 功能:在一组表中按指定字段取最小值的那个表
// 参数:list - 表数组,每个元素是 {"name":..., "value":...} 结构
// field - 用于比较的字段名
Function 取最小项(list, field)
Dim i
If Len(list) = 0 Then
取最小项 = null
Exit Function
End If
取最小项 = list[1]
For i = 2 To Len(list)
If list[i][field] < 取最小项[field] Then
取最小项 = list[i]
End If
Next
End Function
Dim items = { _
{"name": "红药水", "cnt": 3}, _
{"name": "蓝药水", "cnt": 12}, _
{"name": "回城卷", "cnt": 0} _
}
Dim r = 取最小项(items, "cnt")
TracePrint "最少的道具:" & r["name"] & ",数量 " & r["cnt"]
// 输出:最少的道具:回城卷,数量 0
这个函数几乎是万能的 :换 field 就能换业务含义,换比较符号就能换最大最小。我在自己的脚本库里放了三年没改过。
八、实战三:多字段极值(主键 + 次键)
真实需求经常是"血量最低优先,血量一样就挑最近的"。这就是字典序比较:先比主键,主键相等才比次键。
最常见的错误 是把两个条件用 And 串起来:
// ❌ 错误示范
If list[i]["hp"] < best["hp"] And list[i]["dis"] < best["dis"] Then
best = list[i]
End If
这段的逻辑是"血量更低并且距离也更近才算更优"。但现实中新目标往往血量更低、距离却更远------按这个规则它永远上不了台,最后选出个四不像。
正确写法是分级比较:
// 功能:血量优先、距离次之的目标筛选
Dim enemies = { _
{"name": "小怪A", "hp": 3200, "dis": 120}, _
{"name": "小怪B", "hp": 1800, "dis": 300}, _
{"name": "精英C", "hp": 9000, "dis": 80}, _
{"name": "小怪D", "hp": 1800, "dis": 60} _
}
Function 最优先目标(list)
Dim i, isBetter
If Len(list) = 0 Then
最优先目标 = null
Exit Function
End If
最优先目标 = list[1]
For i = 2 To Len(list)
isBetter = False
If list[i]["hp"] < 最优先目标["hp"] Then
isBetter = True
Else
If list[i]["hp"] = 最优先目标["hp"] Then
If list[i]["dis"] < 最优先目标["dis"] Then
isBetter = True
End If
End If
End If
If isBetter Then
最优先目标 = list[i]
End If
Next
End Function
Dim t = 最优先目标(enemies)
TracePrint "优先攻击:" & t["name"] & "(血 " & t["hp"] & ",距 " & t["dis"] & ")"
// 输出:优先攻击:小怪D(血 1800,距 60)
注意 小怪D 在数组里排第 4,但它血量同为 1800 且距离更近,所以正确地上位了。而 精英C 距离最近(80),血量却最高,按"血量优先"的规则不该被选中------如果你用 And 串条件,它可能就赢了。
如果优先级超过两级 ,别写三层嵌套 If,改成加权打分:
Function 打分(e)
// 血量权重 1,距离权重 5,数值越小越优先
打分 = e["hp"] + e["dis"] * 5
End Function
然后对 打分() 的结果取最小值,一行搞定,还能随时调权重。
九、实战四:屏幕范围极值(最上/最下/最左/最右)
这是"极值"最有意思的用法:不是取某个数,而是取二维区域里的边界点。
典型场景:识别一个可以 360° 旋转的箭头指向哪个方向。箭头本身是类三角形,只要拿到它所在矩形区域的四个极点(最上、最下、最左、最右),就能反推出三个角点,进而算出指向。
9.1 为什么要截屏缓存
逐像素取色如果每次都实时截屏,速度会慢到不可用。KeepCapture 把画面缓存一次,之后 GetPixelColor 从缓存读,速度快一个量级。用完必须 ReleaseCapture,否则缓存会一直占着,后续截图命令可能拿到旧画面。
9.2 做 iOS 手机开发前,先把屏幕尺寸打出来
这一节是专门给 iPhone 上做范围扫描的人提个醒,安卓开发者可以略过。
安卓机 1080×2400 就是 1080×2400,坐标写死也能跑;iPhone 有像素倍率,你在截图工具里量到的坐标,和脚本里 GetPixelColor(x, y) 认的坐标,很可能不是同一套。照搬安卓坐标的结果就是:代码不报错,扫描区域永远扫不到东西------最难查的那种 bug。
所以落代码之前,先跑这两行,把真实尺寸打出来:
// 先确认脚本眼里的屏幕有多大,再决定扫描区域
TracePrint "屏幕宽:" & GetScreenX()
TracePrint "屏幕高:" & GetScreenY()
拿这个值和你的截图分辨率对一下,比例不对就说明存在倍率差,区域坐标要按比例缩。这一步只花十秒,能省掉半小时瞎找------我在 iOS 脚本开发的答疑里遇到的"扫不到颜色",基本都卡在这里。
9.3 四极点扫描
// 功能:在矩形区域内找出目标色的最上/最下/最左/最右四个极限点
// 参数:x1,y1,x2,y2 - 扫描区域;tr,tg,tb - 目标色的 RGB 分量;sim - 单通道容差
// 返回:{最上点, 最左点, 最下点, 最右点},每个点为 {x, y};未命中返回 null
Function 范围四极点(x1, y1, x2, y2, tr, tg, tb, sim)
Dim x, y, c, r, g, b
Dim topX, topY, botX, botY, lftX, lftY, rgtX, rgtY, found
found = False
topX = -1
topY = -1
botX = -1
botY = -1
lftX = -1
lftY = -1
rgtX = -1
rgtY = -1
KeepCapture
For y = y1 To y2
For x = x1 To x2
c = GetPixelColor(x, y)
ColorToRGB c, r, g, b
// 用 RGB 三通道分差做"近似色"判断,比精确比对实用得多
If Abs(r - tr) <= sim Then
If Abs(g - tg) <= sim Then
If Abs(b - tb) <= sim Then
If found = False Then
// 行优先扫描,第一个命中点天然就是"最上"点
topX = x
topY = y
lftX = x
lftY = y
found = True
End If
If y > botY Then
botX = x
botY = y
End If
If x < lftX Then
lftX = x
lftY = y
End If
If x > rgtX Then
rgtX = x
rgtY = y
End If
End If
End If
End If
Next
Next
ReleaseCapture
If found Then
范围四极点 = {{topX, topY}, {lftX, lftY}, {botX, botY}, {rgtX, rgtY}}
Else
范围四极点 = null
End If
End Function
两个容易看漏的细节:
① 八个坐标变量全部初始化为
-1。 少初始化一个(比如只给topX赋了值,忘了botY),第一次比较y > botY就会拿数字跟null比,直接报运行时错误。② 传进来的是拆好的 RGB 分量,不是颜色字符串。 像素自身的颜色当然要逐点拆(
ColorToRGB c, r, g, b),但目标色只需要拆一次。如果把目标色也写进内层循环里反复拆,等于每个像素多拆一遍,白烧一半性能------所以进函数前先用下面这个小工具拆好:
// 把颜色字符串拆成 RGB 三元组,只在循环外调用一次
Function 拆色(color)
Dim r, g, b
ColorToRGB color, r, g, b
拆色 = {r, g, b}
End Function
调用时:
Dim tc = 拆色("00FF00")
Dim pts = 范围四极点(430, 642, 465, 672, tc[1], tc[2], tc[3], 30)
别小看这一次 :一个 36×31 的箭头区域有 1116 个像素,把目标色拆色写进内层循环,就是 1116 次多余的 ColorToRGB 调用;区域放大到 200×200 就变成 4 万次。拆一次、传进去,是范围扫描的标准做法。
9.4 一个关键优化:最上点不用比较
原始教程的做法是每个命中像素都比 4 次。但因为是外层 y 升序、内层 x 升序 扫描,第一个命中的像素,其 y 一定是全局最小------它天生就是最上点。
所以:
- 最上点 :
found = False时第一次赋值,之后永不更新
- 最下点 :
y > botY才更新
- 最左点 :
x < lftX才更新
- 最右点 :
x > rgtX才更新
省掉的那个比较,在 1080P 全屏扫描里能省出上百万次判断。
9.5 完整调用示例
Dim tc = 拆色("00FF00")
Dim pts = 范围四极点(430, 642, 465, 672, tc[1], tc[2], tc[3], 30)
If pts = null Then
TracePrint "区域内没找到目标色"
Else
TracePrint "最上点:" & pts[1][1] & "," & pts[1][2]
TracePrint "最左点:" & pts[2][1] & "," & pts[2][2]
TracePrint "最下点:" & pts[3][1] & "," & pts[3][2]
TracePrint "最右点:" & pts[4][1] & "," & pts[4][2]
End If
十、进阶:极点去重与"最短边定尖点"
拿到四个极点还差一步。矩形有四个边界方向,但三角形只有三个 角点------所以四个极点里必然有两个是同一个点(比如"最上"和"最右"落在同一个角上)。
10.1 去重:差值都小于阈值就算同一点
// 功能:坐标去重,两个点 x、y 差值都小于 tol 视为同一点
Function 坐标去重(pts, tol)
Dim i, j, isDup, out
out = {null}
For i = 1 To Len(pts)
isDup = False
For j = 1 To Len(out)
If Abs(pts[i][1] - out[j][1]) < tol Then
If Abs(pts[i][2] - out[j][2]) < tol Then
isDup = True
End If
End If
Next
If isDup = False Then
out[Len(out) + 1] = pts[i]
End If
Next
坐标去重 = out
End Function
注意 out 的初始值是 {null},Len(out) 返回 0,所以第一次追加用 Len(out) + 1 = 1,落在 1 号位------这才是正确姿势(前面坑 1 说的就是漏了这个 +1)。
tol 给 8~12 比较合适,太小去不掉、太大会把真的两个角点误判成一个。
10.2 定尖点:最短边对应的角
三角形去掉一个重复点后正好剩三个角。箭头尖点的特征是------它对面的那条边最短(箭头两翼长、尖口窄)。
用两点距离公式 √((x1-x2)² + (y1-y2)²):
Dim p = 坐标去重(pts, 10)
If Len(p) <> 3 Then
TracePrint "极点数量异常,实际 " & Len(p) & " 个"
Else
Dim d1 = Sqr((p[1][1] - p[2][1]) ^ 2 + (p[1][2] - p[2][2]) ^ 2)
Dim d2 = Sqr((p[1][1] - p[3][1]) ^ 2 + (p[1][2] - p[3][2]) ^ 2)
Dim d3 = Sqr((p[2][1] - p[3][1]) ^ 2 + (p[2][2] - p[3][2]) ^ 2)
If d1 < d2 Then
If d1 < d3 Then
TracePrint "箭头尖点:" & p[3][1] & "," & p[3][2]
End If
End If
If d2 < d1 Then
If d2 < d3 Then
TracePrint "箭头尖点:" & p[2][1] & "," & p[2][2]
End If
End If
If d3 < d1 Then
If d3 < d2 Then
TracePrint "箭头尖点:" & p[1][1] & "," & p[1][2]
End If
End If
End If
逻辑对应关系:d1 是 p1-p2 的距离,其对顶点是 p3;d2 是 p1-p3,对顶点 p2;d3 是 p2-p3,对顶点 p1。最短边 → 对面那个点就是尖点。
拿到尖点坐标后,再和另外两点的中心连一条线,这条线跟水平方向的夹角就是箭头指向的角度。三种状态(正上方/斜上/正下方)就能映射成游戏里的"往前跑/往左上跑"等动作。
十一、踩坑记录(8 个,全是真踩过的)
坑 1:空表写成 {}
Dim t = {} // ❌ 不在官方语法范围内,后续赋值可能异常
Dim t = {null} // ✅ 官方写法
MQ 官方文档写得很明确:定义空表时必须在花括号内写上 null ,否则这个表没有"第一个元素",后续 t[1] = x 的行为就不可预期。别赌它能跑。
顺带一个反直觉的点:Len({null}) 返回的是 0 ,不是 1。所以 {null} 既能当空表声明,也能直接往后追加元素------判空就是 If Len(t) = 0。
坑 2:追加元素忘了 +1
t[Len(t)] = v // ❌ 落在 0 号位,第一个元素被漏掉
t[Len(t) + 1] = v // ✅
这就是第六节那个"替换法"代码丢数据的原因。它不报错,只是结果少一个,最难查。
坑 3:相邻比较时上界没减 1
For i = 1 To Len(arr) // ❌ 最后一次 arr[i+1] 越界取 null
If arr[i] < arr[i + 1] Then
For i = 1 To Len(arr) - 1 // ✅
坑 4:拿字符串跟数字比大小
OCR 识别、界面读文本拿到的都是字符串 。"150" 和 150 直接比大小,MQ 底层是 Lua,会抛 attempt to compare number with string(报错文本沿用 Lua 的消息格式)。
Dim s = "150"
If CInt(s) < 200 Then // ✅ 先转数字
TracePrint "比 200 小"
End If
同类问题还有:字符串参与减法时按键会自动尝试 转数字,能跑通;但比较运算不会自动转,所以减法不报错、比较报错,很容易让人误判。
坑 5:For Each 遍历哈希表,顺序是不固定的
// ❌ 危险假设:以为最后一个就是"最新"的
For Each k, v In hashTable
lastKey = k
Next
TracePrint "最后一个是:" & lastKey // 顺序由哈希决定,跟书写顺序无关
哈希表没有顺序概念。需要顺序就必须用数组,或者遍历时老老实实打擂比较。这条在"取最新一封邮件"这类需求里翻车率极高。
坑 6:空数组没判空,arr[1] 拿到 null
If Len(arr) = 0 Then
TracePrint "没有数据"
Exit Function
End If
null < 数字 同样会抛运行时错误(比较运算拿到 nil)。所有封装函数的第一行都该是这个判断。
坑 7:中文长度和英文不一样
Len() 按字节算。官方文档明确写了:汉字和全角字符占 3 个字符位置 。所以四个汉字返回的是 12,不是 4。如果极值是在处理文本(比如取"最短的物品名"),中文场景下要用 UTF8 插件的 UTF8.Len()。
TracePrint Len("按键精灵") // 12(4 个汉字 × 3 字节)
TracePrint UTF8.Len("按键精灵") // 4
坑 8:浮点数用 = 比较
血量百分比、坐标缩放后的距离,都是浮点。If a = b Then 可能永远不成立。浮点判等必须给容差:
If Abs(a - b) < 0.0001 Then
或者干脆全程用整数(放大 100 倍存),脚本里更省心。
极值这个问题,代码量不超过 10 行,但坑的密度极高:索引起始位、循环边界、类型转换、遍历顺序,每一个都能让结果静默出错。
我的经验是------把"立擂主"和"打擂"这两步分开写,别合并成一个表达式。合并看起来省一行,debug 的时候要多花半小时。
本文所有命令名、参数顺序、数组语义都按 MQ 官方文档核对过,逻辑也逐行核过;但我手上没有你的机型和游戏界面,没做真机实测。范围极值那一段的区域坐标、颜色值、容差都必须按你的实际分辨率重新标定------直接复制粘贴大概率扫不出东西,这不是代码的问题,是坐标不对。
最后回到第零章那个问题:安卓和 iOS 到底要不要分两套写法?
答案是不要。同一套 MQ 语法,安卓脚本开发和 iOS 脚本开发共用;同一段擂台代码,安卓和 iPhone 上都能跑。真正需要你动手的,是每换一台设备就重新标定一次坐标、颜色和容差------这件事没人能替你代劳,也不该指望从教程里抄到一个"通用坐标"。
如果你刚入门,建议按这个顺序练:先跑第三章的最小值(验证环境),再做第六章的 Boss 列表(练擂台),最后啃第九章的范围极值(练坐标标定)。这三关过了,iOS 手机开发里绝大多数"看起来要写一大坨"的需求,回头看都是同一个循环。