Table of Contents
动画处理与transition写法
小明下载了明日方舟后,不满于基建的枯燥,开始尝试编写脚本。小明先尝试实现“从首页进入基建,再进控制中枢”的功能。
他最开始是这样写的:
tap((1557, 943)) # 点击首页的终端入口 tap((1296, 228)) # 点击控制中枢
从首页进入基建,需要先经过一段加载动画。上面的代码直接在首页连点了两次,肯定是不行的。
小明决定让脚本在两次点击中间等待一会儿:
tap((1557, 943)) # 点击首页的终端入口 sleep(5) # 等待动画 tap((1296, 228)) # 点击控制中枢
跑通了!小明兴冲冲地把脚本发给他的朋友小华,然而在小华的老爷机上,从首页进终端一般要花6秒。脚本进行第二次点击时,游戏还在播动画,没有进控制中枢。
小明弄明白问题的原因后,决定加大等待的时长,以兼容更多的设备:
tap((1557, 943)) # 点击首页的终端入口 sleep(10) # 等待动画 tap((1296, 228)) # 点击控制中枢
可惜在自己的电脑上,明明已经进了基建,脚本还要等很久。唉,反正是脚本,为了小华,忍了!
编写脚本的目标是适配更多的机器、处理更多的情况。在某台机器上的一次成功运行,不代表在其它机器上能够多次成功运行。
更糟的是,小明发现,脚本在自己的电脑上也偶尔出问题:首页偶尔出现网络连接的动画,如果第一次点击正好遇到网络连接,点击就会无效,后续步骤也会陷入混乱。
愤怒的小明尝试这样写:
for _ in range(100): tap((1557, 943)) # 愤怒地点击首页的终端入口 sleep(10) # 等待动画 for _ in range(100): tap((1296, 228)) # 愤怒地点击控制中枢
然而,小明还是没有摆脱困境:网络连接的动画持续时间不定,遇到倒霉的情况,一次耗时较长的网络连接可能覆盖了100次点击;在网络通畅的情况下,进入基建后点击还可能没有结束,第一步点击的位置在基建里落在第4间宿舍上。
冷静下来之后,小明想:有没有更好的方法呢?
编写脚本时,图像识别最基础的作用是处理动画。
小明学会了用
find("control_central")
识别控制中枢。获得了图像识别之力的小明跃跃欲试,他想:如果没有识别到控制中枢,说明还没进基建,这时应该点击首页的基建入口;识别到控制中枢后,再点击控制中枢的位置。于是他这样写:while not find("control_central"): # 没有识别到控制中枢时 tap((1557, 943)) # 点击首页的终端入口 tap((1296, 228)) # 点击控制中枢
小明试着跑了几次,却发现进入基建后,脚本有时能进入控制中枢,有时却没有认出来控制中枢,仍然点进了第4间宿舍。
写脚本时需要考虑截图的用时。截图的主要用时往往并不是花费在捕获图像上,而是在后续的编码、传输、解码环节上。因此,截图的画面内容更接近截图开始时的内容,而非截图结束时的内容。从时间上来看,当前截图中没有控制中枢,可以理解为“开始截图时游戏画面内没有控制中枢”,而非“截图结束时游戏画面内没有控制中枢”。如果某个元素逐渐出现,截图中有这个元素,说明当前游戏画面中也有这个元素,截图中没有这个元素,不能说明当前画面中没有这个元素;如果某个元素逐渐消失,截图中没有这个元素,说明当前游戏画面中也没有这个元素;截图中有这个元素,不能说明当前画面中没有这个元素。
另外,脚本很难识别动画播放过程中的画面。例如,进入基建时,动画最后一段由暗到亮,画面亮到什么程度时,脚本能够认出这是基建界面?进一步的识别是否考虑了处在动画中的情况?开发者写识别一般对着动画结束后的截图,但这时也不要忘记对于动画的处理。
小明改用
find("index")
识别游戏的首页:while find("index"): # 在游戏首页时 tap((1557, 943)) # 点击首页的终端入口 while find("control_central") # 在基建内 tap((1296, 228)) # 点击控制中枢
小明发现,这段脚本不仅能正确地处理动画,而且如果脚本在基建内而不是首页开始运行,也能自动地进控制中枢。
虽然index
元素逐渐消失,截图中有index
元素,不能说明游戏还在首页,但接下来的动画时间一般很长,多点一下在大多数情况下没有影响。一种写法能够正确处理某处动画,不代表能够正确处理其它地方的动画。
小明对这个问题感到很苦恼。小华把自己在老爷机上手操的经验传授给小明:“其实脚本没必要一直点的:正常人来操作,也不会看到基建入口就一直连点吧?一般是先点一下,假设它成功了,观察一会儿,如果出现了相应的动画,说明点击成功了;如果画面不变,就再点一次。”
这一操作模式在mower-ng框架下称为ctap模式,实质上是对特定的点击施加了节流的限制,用于处理需要避免多余的点击的情况。Ctap假定经过一段时间的等待后,能够观察出先前的操作是否成功,如果不成功则再次点击。
终于,小明写出了稳定可靠的脚本:
while find("index"): # 在游戏首页时 ctap((1557, 943)) # 点击首页的终端入口 while find("control_central"): # 在基建内 tap((1296, 228)) # 点击控制中枢
小华又提出了意见:“从首页进入基建的动画很长,这个过程中你的脚本一直在截图找控制中枢。能不能识别到动画的时候把截图间隔调大一些?”
小明于是尝试这样写:
while find("index"): # 在游戏首页时 ctap((1557, 943)) # 点击首页的终端入口 while find("animation"): # 动画 sleep(3) while find("control_central"): # 在基建内 tap((1296, 228)) # 点击控制中枢
小华又说道:“那么遇到网络连接的时候,能不能也采取类似的措施呢?”小明
笑道:“却不是特地来消遣我!”愁眉苦脸道:“网络连接每一步都有可能遇上,只能挨个检查了。”while True: if find("connecting"): sleep(1) elif find("index"): ctap((1557, 943)) while True: if find("connecting"): sleep(1) elif find("animation"): sleep(3) while True: if find("connecting"): sleep(1) elif find("control_central"): tap((1296, 228))
小华看到小明被折磨得死去活来,睁眼看着小明道:
“洒家特地要消遣你。”“脚本的操作,每一步都是一个循环。直接用一个大的循环代替一个个的小循环如何?”说罢,小华抢过键盘,把代码改成了这个样子:
while True: if find("connecting"): sleep(1) elif find("animation"): sleep(3) elif find("index"): ctap((1557, 943)) elif find("control_central"): tap((1296, 228))
小明不服:你这样也没有退出条件,不是直接死循环了?于是,小华通过
central_button
识别控制中枢房间,在检测到进入控制中枢后退出:while True: if find("connecting"): sleep(1) elif find("animation"): sleep(3) elif find("index"): ctap((1557, 943)) elif find("control_central"): tap((1296, 228)) elif find("central_button"): break else: ???
但是,对于最后一种情况,小华暂时没有什么思路。
图像识别的另一个作用是处理异常情况。如果没有识别到已知元素,一般认为有两种情况:
- 游戏处于脚本未适配的新页面,在这种情况下,只能通过退出游戏重新登录的方式,回到熟悉的界面重新操作;
- 游戏正在播放动画,导致没有匹配到已知元素,这时需要等待;如果很长时间都没有播完动画,说明游戏卡死了,需要重启恢复。
对于第二种情况的处理也能够覆盖第一种情况。因此,哪怕我们暂时没有完全覆盖游戏内的所有的场景,只要支持识别足够多的元素,在脚本没有写错的情况下,就可以认为“什么都没识别到”是“遇到了动画”。
于是,小华使用
waiting_solver
处理连接中、加载中和未知等动画场景:while True: if find("index"): ctap((1557, 943)) elif find("control_central"): tap((1296, 228)) elif find("central_button"): print("已进入控制中枢") break else: if not waiting_solver(): print("动画超时") break