ios手机开发安卓脚本开发软件[按键精灵教程]从「找最小值」到「Boss 最近刷新」再到「屏幕范围极值」

先回答一个高频问题:安卓和 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 手机开发里绝大多数"看起来要写一大坨"的需求,回头看都是同一个循环。

相关推荐
开开心心就好1 小时前
安卓PDF处理工具,免费无广告零弹窗
android·服务器·开发语言·算法·pdf·ocr
墨狂之逸才1 小时前
扫码枪响了,应用却没收到:一次 Android USB 线材问题排查
android
alexhilton13 小时前
Room Fts4:全文搜索替代 SQL LIKE
android·kotlin·android jetpack
hai_android18 小时前
把 Android App 打包成系统应用(System App)
android·kotlin·android jetpack
JMchen19 小时前
【Jetpack 内核实战 01】开篇:Jetpack 组件全家桶核心逻辑与专栏完整规划
android·kotlin·android jetpack
骑着蜗牛撵大象32721 小时前
Linux 进程间通信之管道:从 pipe() 的内核语义到 FIFO 落地排错
android·java·linux·系统编程·fifo·管道·进程间通信
众壹新能源科技21 小时前
逆变器自带无功不够用,加装5MVar SVG后功率因数从0.85回到0.99
运维·人工智能·自动化
hunterandroid21 小时前
Retrofit 网络请求的异常处理与重试策略实战
android
sejinan21 小时前
极致轻量与秒级起播:NativeWasmTv 开源硬核技术解析与升级版体验
android·开源·webview·tv