12 transition
zhbaor edited this page 2024-12-09 21:17:23 +08:00

动画处理与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:
        ???

但是,对于最后一种情况,小华暂时没有什么思路。

图像识别的另一个作用是处理异常情况。如果没有识别到已知元素,一般认为有两种情况:

  1. 游戏处于脚本未适配的新页面,在这种情况下,只能通过退出游戏重新登录的方式,回到熟悉的界面重新操作;
  2. 游戏正在播放动画,导致没有匹配到已知元素,这时需要等待;如果很长时间都没有播完动画,说明游戏卡死了,需要重启恢复。

对于第二种情况的处理也能够覆盖第一种情况。因此,哪怕我们暂时没有完全覆盖游戏内的所有的场景,只要支持识别足够多的元素,在脚本没有写错的情况下,就可以认为“什么都没识别到”是“遇到了动画”。

于是,小华使用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