基於深度強化學習的局內戰鬥自動化測試探索

本文首發於:行者AInode

遊戲項目研發時,須要搭建一個自動化測試的平臺,以指望場內戰鬥使用自動化來測試,發現局內bug,避免重複勞動、提升測試效率以及避免人爲的操做錯誤。其中環境要求使用項目須要使用Airtest、poco對接強化學習的服務器,實現Airtest將狀態信息發送給服務器,服務器返回下一步的決策。python

1. 前期準備工做

瞭解Airtest、poco、強化學習agent的決策方式。ios

1.1 Airtest介紹

Airtest基於圖像識別的自動化測試框架。這個框架核心不在實現方式和技術上,而是理念!這個框架的理念借用是MIT(麻省理工)研究院的成果 Sikuli ,他們構思了一種全新的UI測試模式,基於圖像識別控件而不是具體內存裏的控件對象。json

(1)Airtest特色c#

  • 支持基於圖像識別的可程式化測試工具
  • 跨平臺
  • 生成測試報告
  • 支持poco等SDK內嵌,提升UI識別精度

(2)Airtest界面(包含點擊、滑動、判斷、截屏等接口)服務器

wCZSYR.md

(3)Airtest效果演示框架

BAliqO.gif

以上演示的是Airtest與直接經過圖像識別對界面產生交互。如若須要與界面指定元素交互,是須要Poco提供的方法對界面上的元素進行操做工具

(4)Airtest侷限性學習

然而在實際工程中,圖像不會一成不變,咱們須要捕獲項目的動態節點,針對動態節點進行點擊、移動等操做(好比商店買旗子的位置節點)測試

咱們須要另外一個工具Poco。

wCZNhn.png

1.2 Poco介紹

目前Poco只支持原生Android和ios的接口調用,其餘平臺均須要接入對應平臺的sdk

wCFEjg.png

(1)Poco獲取UI樹的方式

從根節點開始向下遍歷子節點

在unity項目中,須要在unity中安裝Poco的SDK

(2)Poco調用方法

wCFaU1.png

(3)Poco調用舉例

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

Airtest經過接口調用unity中的pocosdk,SDK對整個ui樹進行遍歷,將dump後的json信息傳回Airtest。

Airtest在獲得的UI樹中找到‘btn_start’的元素位置信息,經過adb進行點擊操做。

1.3 強化學習的簡述

wCF6DH.png

Environment 一般利用馬爾可夫過程來描述,Agent 經過採起某種 Policy 來產生Action,和 Environment 交互,產生一個 Reward。以後 Agent 根據 Reward 來調整優化當前的 Policy。

wMejYj.jpg

用上圖更形象的解釋,state是環境的一個狀態,observation是Agent觀察到的環境狀態,這裏observation和state是一個意思。首先Agent觀察到環境的一個狀態,好比是一杯水,而後Agent採起了一個行爲,這個行爲是Agent把杯子中的水給打碎了,這樣環境的的狀態已經發生了變化,而後系統會對這個行爲打一個分數,來告訴Agent這樣的行爲是否正確,而後根據新變化的環境狀態,Agent再採起進一步的行爲。Agent所追求的目標就是讓Reward儘可能的大。

2. 項目執行過程:

2.1 背景

經過將訓練好的一個Agent部署到服務器上,其餘人經過訪問服務器,流程以下:

a. 測試端收集信息->測試端將信息轉成約定好的state格式->測試端將state發給服務器->服務器返回一個Agent的決策->測試端收到信息執行決策->

b. 測試端收集信息(新一輪循環的開始)...

在這個過程當中,測試端收集信息耗時最爲嚴重,針對項目需求決定對其部分進行優化。

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進行遞歸寫入子節點信息,剔除掉場景、特效等層級,能夠大幅減小開銷。

wCFvGV.md.png

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;
  }

PS:更多技術乾貨,快關注【公衆號 | xingzhe_ai】,與行者一塊兒討論吧!

相關文章
相關標籤/搜索