本文首发于:行者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】,与行者一块儿讨论吧!