本文首發於:行者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界面(包含點擊、滑動、判斷、截屏等接口)服務器
(3)Airtest效果演示框架
以上演示的是Airtest與直接經過圖像識別對界面產生交互。如若須要與界面指定元素交互,是須要Poco提供的方法對界面上的元素進行操做工具
(4)Airtest侷限性學習
然而在實際工程中,圖像不會一成不變,咱們須要捕獲項目的動態節點,針對動態節點進行點擊、移動等操做(好比商店買旗子的位置節點)測試
咱們須要另外一個工具Poco。
1.2 Poco介紹
目前Poco只支持原生Android和ios的接口調用,其餘平臺均須要接入對應平臺的sdk
(1)Poco獲取UI樹的方式
從根節點開始向下遍歷子節點
在unity項目中,須要在unity中安裝Poco的SDK
(2)Poco調用方法
(3)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部署到服務器上,其餘人經過訪問服務器,流程以下:
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進行遞歸寫入子節點信息,剔除掉場景、特效等層級,能夠大幅減小開銷。
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】,與行者一塊兒討論吧!