基于深度强化学习的局内战斗自动化测试探索

游戏项目研发时,期望搭建自动化测试平台,发现局内bug,避免重复劳动、提高测试效率以及避免人为的操作错误。其中环境要求使用项目需要使用Airtest、poco对接强化学习的服务器,实现Airtest将状态信息发送给服务器,服务器返回下一步的决策。

1. 前期准备工作

了解Airtest、poco、强化学习agent的决策方式。

1.1 Airtest介绍

Airtest基于图像识别的自动化测试框架。这个框架核心不在实现方式和技术上,而是理念!这个框架的理念借用是MIT(麻省理工)研究院的成果 Sikuli ,他们构思了一种全新的UI测试模式,基于图像识别控件而不是具体内存里的控件对象。

Airtest特点

① 支持基于图像识别的可程式化测试工具
② 跨平台
③ 生成测试报告
④ 支持poco等SDK内嵌,提高UI识别精度

Airtest界面(包含点击、滑动、判断、截屏等接口)

Airtest局限性

然而在实际工程中,图像不会一成不变,我们需要捕获项目的动态节点,针对动态节点进行点击、移动等操作(比如商店买旗子的位置节点)
我们需要另一个工具Poco。

1.2 Poco介绍

目前Poco只支持原生Android和ios的接口调用,其他平台均需要接入对应平台的sdk

Poco获取UI树的方式

从根节点开始向下遍历子节点

在unity项目中,需要在unity中安装Poco的SDK

Poco调用方法

Poco调用举例

poco = UnityPoco()
poco('btn_start').click()

① Airtest通过接口调用unity中的pocosdk,SDK对整个ui树进行遍历,将dump后的json信息传回Airtest。

② Airtest在得到的UI树中找到‘btn_start’的元素位置信息,通过adb进行点击操作。

1.3 强化学习的简述

Environment 通常利用马尔可夫过程来描述,Agent 通过采取某种 Policy 来产生Action,和 Environment 交互,产生一个 Reward。之后 Agent 根据 Reward 来调整优化当前的 Policy。

用上图更形象的解释,state是环境的一个状态,observation是Agent观察到的环境状态,这里observation和state是一个意思。首先Agent观察到环境的一个状态,比如是一杯水,然后Agent采取了一个行为,这个行为是Agent把杯子中的水给打碎了,这样环境的的状态已经发生了变化,然后系统会对这个行为打一个分数,来告诉Agent这样的行为是否正确,然后根据新变化的环境状态,Agent再采取进一步的行为。Agent所追求的目标就是让Reward尽量的大。

2. 项目执行过程:

2.1 背景

通过将训练好的一个Agent部署到服务器上,其他人通过访问服务器,流程如下:

① 测试端收集信息->测试端将信息转成约定好的state格式->测试端将state发给服务器->服务器返回一个Agent的决策->测试端收到信息执行决策->

② 测试端收集信息(新一轮循环的开始)...

在这个过程中,测试端收集信息耗时最为严重,针对项目需求决定对其部分进行优化。

2.2 具体问题

① poco首次调用dump接口时会启动大量mincap等诸多可执行文件,导致7秒左右的延迟。

② Airtest操作遇到的延迟过于严重,导致每回合可操作时间30秒内,只能进行4-5个动作。

然而本地训练的agent后期每回合操作数能达到16个左右,导致后期agent动作不能完全在客户端上做完。

2.3 解决方案

① 提前加载poco的click事件

② 定位到dump耗时严重,决定从sdk的接口出发,减少dump出的json文件大小。

a. 在unity的接口中,加入tagfilter、blacklist、propertylist参数,来控制json的文件大小。
其中tagfilter用于针对指定tag的unitygameobject的筛选,可以去除除UI和Default以外的所有物体。
blacklist用于针对unitygameobject名字的筛选,能提高dump效率50%
propertylist用于减少单个unitygameobject的参数写入,默认单个物体有10多个参数,筛选后可以省下6个左右的参数。可提高dump效率33%

b. 在python的接口使用对应接口参数

该方法完美解决了操作延迟的问题,目前客户端单回合30秒可以完成20个左右的动作。

2.4 具体步骤:

  • layerfilter 在项目中,有13个layer。只对tag为UI和Default的UGO进行递归写入子节点信息,剔除掉场景、特效等层级,可以大幅减少开销。
  • namefilter 并非所有UI节点信息都是自动化测试需要获得的必要数据。所以在递归查询子节点时,遇到写入黑名单的UGO的名字时,可以减少约50%的时间开销。

主要修改C#的poco中AbstractDumper中的dumpHierarchyImpl接口,具体如图:

private Dictionary<string, object> dumpHierarchyImpl (AbstractNode node, bool onlyVisibleNode, Dictionary<string, object> extrapar)
{
    if (node == null) 
    {
        return null;
    }
    Dictionary<string, object> payload = new Dictionary<string, object>();
    if (extrapar != null && extrapar.ContainsKey("param4") && extrapar["param4"] != null)
    {
        payload = node.enumerateAttrs(extrapar["param4"].ToString());
    }
    else
    {
        payload = node.enumerateAttrs(null);
    }
    Dictionary<string, object> result = new Dictionary<string, object> ();
    string name = (string)node.getAttr ("name");

    result.Add ("name", name);
    result.Add ("payload", payload);
    List<object> children = new List<object>();
    if (extrapar!= null)
    {
        if (extrapar.ContainsKey("param3") && extrapar["param3"] != null)
        {
            requirelayer = extrapar["param3"].ToString().Split('|').ToList();
            string layer = (string)node.getAttr("layer");
            if (!requirelayer.Contains(layer))
            {
                //Debug.LogError("--dumpHierarchyImpl layer is not contains");
                return result;
            }
        }
        if (extrapar.ContainsKey("param2") && extrapar["param2"] != null)
        {
            try
            {
                filterlist.Clear();
                string str = extrapar["param2"].ToString();
                filterlist = str.Split('|').ToList();
            }
            catch
            {
                Debug.LogError("~~~dumpHierarchy Implextrapar param2 error");
            }

            if (filterlist.Contains(name))
            {
                return result;
            }
        }
    }

    foreach (AbstractNode child in node.getChildren()) 
    {
        if (!onlyVisibleNode || (bool)child.getAttr ("visible")) 
        {
         children.Add (dumpHierarchyImpl (child, onlyVisibleNode, extrapar));
        }
    }
    if (children.Count > 0) 
    {
        result.Add ("children", children);
    }
    return result;
  }
  • propertyfilter json默认dump出的一个节点参数包含:

name、payload、type、visible、pos、size、scale、anchorPoint、zOrders、clickable、components、_ilayer、layer、_instanceId等参数。我们剔除掉了
visible|scale|anchorPoint|clickable|components|_ilayer|layer|_instanceId实际上用不上的参数,大大减少的dump出来json的文件大小,可以减少约33%的时间开销。

主要修改C#的poco中UnityNode中的enumerateAttrs、GetPayload接口,具体如下:

  private Dictionary<string, object> GetPayload(string blackList)
  {
        Dictionary<string, object> all =  new Dictionary<string, object>() {
            { "name", gameObject.name },
            { "type", GuessObjectTypeFromComponentNames (components) },
            { "visible", GameObjectVisible (renderer, components) },
            { "pos", GameObjectPosInScreen (objectPos, renderer, rectTransform, rect) },
            { "rawpos", GameObjectVec3Pos (objectRawPos) },
            { "rawrectpos", GameObjectVec3Pos (objectRectRawPos) },
            { "size", GameObjectSizeInScreen (rect, rectTransform) },
            { "scale", new List<float> (){ 1.0f, 1.0f } },
            { "anchorPoint", GameObjectAnchorInScreen (renderer, rect, objectPos) },
            { "zOrders", GameObjectzOrders () },
            { "clickable", GameObjectClickable (components) },
            { "text", GameObjectText () },
            { "components", components },
            { "texture", GetImageSourceTexture () },
            { "tag", GameObjectTag () },
            { "_ilayer", GameObjectLayer() },
            { "layer", GameObjectLayerName() },
            { "_instanceId", gameObject.GetInstanceID () },
        };
        Dictionary<string, object> payload = new Dictionary<string, object>();
        if (!string.IsNullOrEmpty(blackList))
        {
            List<string> black_list = blackList.Split('|').ToList();
            foreach(KeyValuePair<string, object> it in all)
            {
                if(black_list.Contains(it.Key))
                {
                    continue;
                }
                payload.Add(it.Key,it.Value);
            }
        }
        else
        {
            payload = all;
        }

        return payload;
  }

我们是行者AI,我们在“AI+游戏”中不断前行。

如果你也对游戏感兴趣,对AI充满好奇,那就快来加入我们吧~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,612评论 5 471
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,345评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,625评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,022评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,974评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,227评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,688评论 3 392
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,358评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,490评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,402评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,446评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,721评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,802评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,013评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,504评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,080评论 2 341

推荐阅读更多精彩内容