前言
在项目上线前期,这边根据需求制作了一套QA测试工具。主要分为以下四个模块的测试**图1**
- **数值测试:**主要包括了角色的等级变更、游戏里货币的变更、(目前已制作的)游戏道具的数量变更。这些可能归一为一类测试模型
- **动画测试:**包括角色的控制系统的所有Animation资源的播放状态【目前无测试需求】
- **流程测试:**比如是否需要快速胜利、跳过新手指引、指定比赛胜利类型(胜负、平局)等等一系列流程。
- **自定测试:**笔者目前没有想到的,可能出现的其他需要测试的分类。
工具架构
主菜单顶部横栏
如图1所示,主菜单是横向布局,静态显示的。
using System.Collections.Generic;
using JetBrains.Annotations;
using QAModule;
using UnityEngine;
using UnityEngine.UI;
using TEngine;
namespace GameLogic.UI
{
[Window(UILayer.UI)]
public class QAMainPageUI : UIWindow
{
//缓存池对象
private QAOptionPanel _optionPanelInBuffer;
private List<TestOption> _optionsList;
//菜单选项条目
private Dictionary<TestType, string[]> _menuDictionary;
#region 脚本工具生成的代码
private Image m_imgBg;
private GameObject m_goOptionPanel;
private GameObject m_goTestNameRoot;
private Button m_btnNumericalTest;
private Button m_btnAnimationTest;
private Button m_btnProcessTest;
private Button m_btnBack;
public override void ScriptGenerator()
{
m_imgBg = FindChildComponent<Image>("m_imgBg");
m_goOptionPanel = FindChild("m_goOptionPanel").gameObject;
m_goTestNameRoot = FindChild("m_goTestNameRoot").gameObject;
m_btnBack = FindChildComponent<Button>("m_btnBack");
m_btnNumericalTest = FindChildComponent<Button>("m_goTestNameRoot/m_btnNumericalTest");
m_btnAnimationTest = FindChildComponent<Button>("m_goTestNameRoot/m_btnAnimationTest");
m_btnProcessTest = FindChildComponent<Button>("m_goTestNameRoot/m_btnProcessTest");
m_btnBack.onClick.AddListener(OnClickBackBtn);
m_btnNumericalTest.onClick.AddListener(OnClickNumericalTestBtn);
m_btnAnimationTest.onClick.AddListener(OnClickAnimationTestBtn);
m_btnProcessTest.onClick.AddListener(OnClickProcessTestBtn);
}
#endregion
public override void OnCreate()
{
base.OnCreate();
Initialize();
}
private void Initialize()
{
_optionsList = new List<TestOption>();
_menuDictionary = new Dictionary<TestType, string[]>();
QAInitDataTable dataTable = new QAInitDataTable();
_menuDictionary = dataTable.MenuDictionary;
}
/// <summary>
/// 根据选项展开面板
/// </summary>
/// <param name="index"></param>
private void OpenPanel(TestType type)
{
int index = (int)type;
m_goOptionPanel.SetActive(true);
m_imgBg.enabled = false;
//对应属性高亮
int indexCounts = m_goTestNameRoot.transform.childCount;
List<Transform> childrenTrans= m_goTestNameRoot.transform.GetAllChildren();
for (int i = 0; i < indexCounts; i++)
{
var select = childrenTrans[i].Find("Selected").gameObject;
if (select != null)
{
if (index == i)
{
select.SetActive(true);
}
else
{
if (select.activeInHierarchy)
{
select.SetActive(false);
}
}
}
}
//创建面板
_optionPanelInBuffer ??= CreateWidgetByPath<QAOptionPanel>(m_goOptionPanel.transform, "QAOptionPanel");
//读取缓存池,刷新选项内容
_optionPanelInBuffer.Init(_optionsList,type,_menuDictionary[type]);
}
/// <summary>
/// 数值类型测试
/// </summary>
private void OnClickNumericalTestBtn()
{
OpenPanel(TestType.NumericalType);
}
/// <summary>
/// 动画类型测试
/// </summary>
private void OnClickAnimationTestBtn()
{
OpenPanel(TestType.AnimationType);
}
/// <summary>
/// 流程类型测试
/// </summary>
private void OnClickProcessTestBtn()
{
OpenPanel(TestType.ProcessType);
}
private void OnClickBackBtn()
{
if (m_goOptionPanel.activeInHierarchy)
{
m_goOptionPanel.SetActive(false);
m_imgBg.enabled = true;
}
else
{
GameModule.UI.CloseWindow<QAMainPageUI>();
}
}
}
}
背包面板
点击顶部菜单按钮提示,展开二级选择面板。根据考虑,我选择了类似背包面板的展示模式。
在面板中通过网格布局,创建需要的测试条目。
面板切换时,使用了一个缓存池做优化。
首次创建时选项的预制体加入缓存池,如果切换面板只需更新UI、更换打开的工作流即可。
缓存池
/// <summary>
/// 创建面板里的选项
/// </summary>
/// 根据TestType类型创建条目,每个条目已经绑定了打开的显示逻辑
public void Init(List<TestOption> optionList,TestType type,string[] optionType)
{
int typeCounts = optionType.Length;
int bufferCounts = optionList.Count;
//缓存池中数量小于需创建的数量,重复部分刷新值,多余部分创建并入池子。
if (bufferCounts < typeCounts)
{
for (int index = 0; index < typeCounts; index++)
{
if (index < bufferCounts)
{
if (!optionList[index].gameObject.activeInHierarchy)
{
optionList[index].gameObject.SetActive(true);
}
optionList[index].Initialize(index,type,optionType[index]);
}
else
{
var testOption = CreateWidgetByPath<TestOption>(m_goContent.transform, "TestOption");
testOption.Initialize(index,type,optionType[index]);
optionList.Add(testOption);
}
}
}
//缓存池中数量大于等于需创建的数量,读取池子刷新内容,多余部分隐藏。
else
{
for (int i = 0; i < bufferCounts; i++)
{
if (i < typeCounts)
{
optionList[i].Initialize(i,type,optionType[i]);
if (!optionList[i].gameObject.activeInHierarchy)
{
optionList[i].gameObject.SetActive(true);
}
}
else
{
optionList[i].gameObject.SetActive(false);
}
}
}
}
具体测试面板
点击进入具体测试面板时,对于面板笔者是这么规划的。
数据类
既然测试的大类型分为了四类,那么自然每个类型都应该有不同的初始化数据
图2
在面板中,红框的部分是**派生的预制体持有的,**剩余部分应该是每种类型都应该显示的了,那就是标题
以数值类型测试为例,数据脚本如下
namespace QAModule
{
//基础数据类型存储结构
public class QABaseData
{
public string TestType
{
get => _testType;
set => _testType = value;
}
private string _testType;
}
}
namespace QAModule
{
/// <summary>
/// 数值类型字段存储结构
/// </summary>
public class QANumericalData : QABaseData
{
public string InitDisplayValue
{
get => _initDisplayValue;
set => _initDisplayValue = value;
}
public float IncrementRate
{
get => _incrementRate;
set => _incrementRate = value;
}
public float DecrementRate
{
get => _decrementRate;
set => _decrementRate = value;
}
private string _initDisplayValue;
private float _incrementRate;
private float _decrementRate;
}
}
物体脚本
那么实现的脚本至少有两层
using GameLogic.UI.QAEvent;
using UnityEngine.UI;
using TEngine;
using QAModule;
using UnityEngine;
namespace GameLogic.UI
{
[Window(UILayer.UI)]
public class QAPanelBase<T> :UIWindow where T : QAPanelBase<T>
{
//需要记忆存储的参数
protected static string _testType;
#region 脚本工具生成的代码
protected Text m_textType;
private Button m_btnBack;
public override void ScriptGenerator()
{
m_textType = FindChildComponent<Text>("Title/m_textType");
m_btnBack = FindChildComponent<Button>("m_btnBack");
m_btnBack.onClick.AddListener(OnClickBackBtn);
}
#endregion
public override void RegisterEvent()
{
base.RegisterEvent();
AddUIEvent<QABaseData>(QAEventDefine.StartWorkflow,OnStartWorkflow);
}
protected virtual void InitData(QABaseData data)
{
}
private void CreateWorkflow() //确定工作流,软件模型:瀑布模型
{
ReadDataFromMemory();//1.
AddListener();
InitPanel();
}
protected virtual void ReadDataFromMemory()
{
}
protected virtual void AddListener(){}
protected virtual void InitPanel()
{
}
#region 事件
private void OnStartWorkflow(QABaseData data)
{
InitData(data);
CreateWorkflow();
}
protected virtual void OnClickBackBtn()
{
//打开主界面
Debug.Log("back from base");
GameModule.UI.ShowUI<QAMainPageUI>();
}
#endregion
}
}
using QAModule;
using UnityEngine;
using UnityEngine.UI;
using TEngine;
namespace GameLogic.UI
{
[Window(UILayer.UI)]
public class QAPanelNumerical : QAPanelBase<QAPanelNumerical>
{
protected QANumericalData _numericalData;
//需要记忆存储的参数
protected static float _increment;
protected static float _decrement;
protected static float _incrementRate;
protected static float _decrementRate;
// _increment = _incrementRate * _addSliderValue
private static float _incrementSliderValue;
private static float _decrementSliderValue;
private static string _displayValue;
#region 脚本工具生成的代码
protected GameObject m_goAdd;
protected GameObject m_goMinus;
private Text m_textDisplayType;
private Text m_textDisplayValue;
protected InputField m_inputAddInputField;
private Text m_textIncrement;
protected Slider m_sliderAddValues;
private Button m_btnAddValues;
protected InputField m_inputMinusInputField ;
private Text m_textDecrement;
protected Slider m_sliderMinusValues ;
private Button m_btnMinusValues;
public override void ScriptGenerator()
{
base.ScriptGenerator();
m_goAdd = FindChild("ControlZone/m_goAdd").gameObject;
m_goMinus = FindChild("ControlZone/m_goMinus").gameObject;
m_textDisplayType = FindChildComponent<Text>("DisplayZone/DisplayBg/m_textDisplayType");
m_textDisplayValue = FindChildComponent<Text>("DisplayZone/DisplayBorder/m_textDisplayValue");
m_inputAddInputField = FindChildComponent<InputField>("ControlZone/m_goAdd/m_inputAddInputField");
m_textIncrement = FindChildComponent<Text>("ControlZone/m_goAdd/m_inputAddInputField/m_textIncrement");
m_sliderAddValues = FindChildComponent<Slider>("ControlZone/m_goAdd/m_sliderAddValues");
m_btnAddValues = FindChildComponent<Button>("ControlZone/m_goAdd/m_btnAddValues");
m_inputMinusInputField = FindChildComponent<InputField>("ControlZone/m_goMinus/m_inputMinusInputField ");
m_textDecrement = FindChildComponent<Text>("ControlZone/m_goMinus/m_inputMinusInputField /m_textDecrement");
m_sliderMinusValues = FindChildComponent<Slider>("ControlZone/m_goMinus/m_sliderMinusValues ");
m_btnMinusValues = FindChildComponent<Button>("ControlZone/m_goMinus/m_btnMinusValues");
m_sliderAddValues.onValueChanged.AddListener(OnSliderAddValuesChange);
m_btnAddValues.onClick.AddListener(OnClickAddValuesBtn);
m_sliderMinusValues .onValueChanged.AddListener(OnSliderMinusValuesChange);
m_btnMinusValues.onClick.AddListener(OnClickMinusValuesBtn);
}
#endregion
protected override void InitData(QABaseData data)
{
base.InitData(data);
_numericalData = data as QANumericalData;
m_textType.text = "Test - " +_numericalData?.TestType;
_displayValue = _numericalData?.InitDisplayValue;
if (_numericalData != null) _incrementRate = _numericalData.IncrementRate;
if (_numericalData != null) _decrementRate = _numericalData.DecrementRate;
}
protected override void ReadDataFromMemory()
{
m_sliderAddValues.value = _incrementSliderValue == 0 ? 1 : _incrementSliderValue;
m_sliderMinusValues.value = _decrementSliderValue == 0 ? 1 : _decrementSliderValue;
}
protected override void AddListener()
{
m_inputAddInputField.onValueChanged.AddListener(OnInputAddField);
m_inputMinusInputField.onValueChanged.AddListener(OnInputMinusField);
}
protected override void InitPanel()
{
//是否是初始数据。是,表中获取。否,用上次设过值的
if (_incrementRate == 0 && _decrementRate == 0)
{
if (_numericalData != null)
{
_incrementRate = _numericalData.IncrementRate;
_decrementRate = _numericalData.DecrementRate;
}
}
_increment = _incrementRate * m_sliderAddValues.value;
_decrement = _decrementRate * m_sliderMinusValues.value;
//初始化面板显示
m_textDisplayType.text = "Current" + _testType;
m_textIncrement.text = $"Increment:{_increment}";
m_textDecrement.text = $"Decrement:{_decrement}";
if (_displayValue == null)
{
_displayValue = _numericalData?.InitDisplayValue;
m_textDisplayValue.text = _numericalData?.InitDisplayValue;
}
else
{
m_textDisplayValue.text = _displayValue;
}
}
#region 事件
/// <summary>
/// 设置参数倍率
/// </summary>
/// <param name="value"></param>
private void OnSliderAddValuesChange(float value)
{
_increment = value * _incrementRate;
m_textIncrement.text = $"Increment:{_increment}";
_incrementSliderValue = value;
}
protected virtual void OnClickAddValuesBtn()
{
//Change Panel
float curValue = float.Parse(m_textDisplayValue.text);
curValue += _increment ;
m_textDisplayValue.text = curValue.ToString();
_displayValue = curValue.ToString();
//Test Function
}
private void OnSliderMinusValuesChange(float value)
{
_decrement = value * _decrementRate;
m_textDecrement.text = string.Format("Decrement:{0}", _decrement);
_decrementSliderValue = value;
}
protected virtual void OnClickMinusValuesBtn()
{
//Change Panel表现
float curValue = float.Parse(m_textDisplayValue.text);
curValue -= _decrement ;
m_textDisplayValue.text = curValue.ToString();
_displayValue = curValue.ToString();
//Test Function
//.....
}
/// <summary>
/// 通过输入框自定义参数值
/// </summary>
private void OnInputMinusField(string inputParma)
{
//更新倍率
_decrementRate = float.Parse(inputParma);
//更新面板
m_sliderMinusValues.value = 1;
_decrement = _decrementRate * m_sliderMinusValues.value;
m_textDecrement.text = string.Format("Decrement:{0}", _decrement);
}
private void OnInputAddField(string inputParma)
{
_incrementRate = float.Parse(inputParma);
m_sliderAddValues.value = 1;
_increment = _incrementRate * m_sliderAddValues.value;
m_textIncrement.text = string.Format("Increment:{0}", _increment);
}
#endregion
}
}
数值类型的面板如上图2,为了便于控制一次加减的值,我做了4档可输入计算器。可以根据倍率准确定位数值,做到对大小数值的便捷测试。
// _increment = _incrementRate * _addSliderValue
//实际值 = 倍率 * 滑动条的档数
数据和表现分离
所以,以上两层继承,完成了通过点击面板,完成UI界面数值的更替。
现在,需要把相关更替的数值注入到指定的数据集中【即:做出实际的测试功能】
那么只需要再让具体的XX测试 继承数值测试,然后读取相应字段,重写+ -按钮的回调函数,跟据读取的数值,做相应功能就行了。
using TEngine;
using GameLogic.DKSystem.Soical;
using UnityEngine;
namespace GameLogic.UI
{
[Window(UILayer.UI,"QAPanelNumerical")]
public class QAPanelGold : QAPanelNumerical
{
protected override void OnClickAddValuesBtn()
{
//数据
base.OnClickAddValuesBtn();
var gold = (int)_increment ;
PlayerService.AddGold(gold);
Debug.Log(string.Format("{0} + {1} success.", _testType, gold));
}
protected override void OnClickMinusValuesBtn()
{
base.OnClickMinusValuesBtn();
var gold = (int)_decrement;
PlayerService.AddGold(-gold);
Debug.Log(string.Format("{0} - {1} success.", _testType, gold));
}
protected override void OnClickBackBtn()
{
base.OnClickBackBtn();
GameModule.UI.CloseWindow<QAPanelGold>();
}
}
}
using TEngine;
using GameLogic.DKSystem.Soical;
using QAModule;
using UnityEngine;
namespace GameLogic.UI
{
[Window(UILayer.UI,"QAPanelNumerical")]
public class QAPanelExp : QAPanelNumerical
{
protected override void InitPanel()
{
base.InitPanel();
m_goMinus.SetActive(false);
}
protected override void OnClickAddValuesBtn()
{
//数据
base.OnClickAddValuesBtn();
var exp = (int)_increment ;
PlayerService.AddExp(exp);
Debug.Log(string.Format("{0} + {1} success.", _testType, exp));
}
protected override void OnClickMinusValuesBtn()
{
base.OnClickMinusValuesBtn();
var exp = (int)_decrement;
PlayerService.AddExp(-exp);
Debug.Log(string.Format("{0} - {1} success.", _testType, exp));
}
protected override void OnClickBackBtn()
{
base.OnClickBackBtn();
GameModule.UI.CloseWindow<QAPanelExp>();
}
}
}
using GameLogic.DKSystem;
using TEngine;
using UnityEngine;
namespace GameLogic.UI
{
[Window(UILayer.UI,"QAPanelNumerical")]
public class QAPanelStar : QAPanelNumerical
{
protected override void InitPanel()
{
base.InitPanel();
m_inputAddInputField.gameObject.SetActive(false);
m_inputMinusInputField.gameObject.SetActive(false);
m_sliderAddValues.gameObject.SetActive(false);
m_sliderMinusValues.gameObject.SetActive(false);
}
protected override void OnClickAddValuesBtn()
{
base.OnClickAddValuesBtn();
AnswerRankService.PostSta(new StaData()
{
season_id = WikipediaQuizSystem.Instance.SeasonId,
is_victory = 3
},null);
Debug.Log("增加1个星星");
}
protected override void OnClickMinusValuesBtn()
{
base.OnClickMinusValuesBtn();
AnswerRankService.PostSta(new StaData()
{
season_id = WikipediaQuizSystem.Instance.SeasonId,
is_victory = 1
},null);
Debug.Log("减少1个星星");
}
protected override void OnClickBackBtn()
{
base.OnClickBackBtn();
GameModule.UI.CloseWindow<QAPanelStar>();
}
}
}
面板
using UnityEngine;
using UnityEngine.UI;
using TEngine;
using QAModule;
namespace GameLogic.UI
{
[Window(UILayer.UI)]
public class TestOption : UIWidget
{
private TestType _type;
private int _optionIndex;
private Button m_btnTestOption;
#region 脚本工具生成的代码
private Image m_imgBg;
private Text m_textTestOption;
public override void ScriptGenerator()
{
m_imgBg = FindChildComponent<Image>("m_imgBg");
m_textTestOption = FindChildComponent<Text>("m_textTestOption");
}
#endregion
/// <summary>
/// 根据index查找对应测试的名称类型
/// </summary>
/// <param name="index"></param>
/// <param name="type"></param>
/// <param name="description"></param>
public void Initialize(int index,TestType type,string description)
{
m_btnTestOption = gameObject.GetComponent<Button>();
m_btnTestOption.onClick.AddListener(OnClickTestOptionBtn);
_optionIndex = index;
m_textTestOption.text = description;
m_imgBg.color = Color.cyan;
}
private void InitWorkflow(int index,TestType type)
{
TestProcessManager.Instance.CurTestType = type;
TestProcessManager.Instance.SelectTestProcess(index);
}
#region 事件
private void OnClickTestOptionBtn()
{
InitWorkflow(_optionIndex,_type);
}
#endregion
}
}
打开并创建界面的核心是 创建工作流、先初始化,再创建。方法在工作流管理器调用
单例的工作流管理器
using Aliyun.OSS;
using GameBase;
using UnityEngine;
using UnityEngine.UI;
using TEngine;
using QAModule;
namespace GameLogic.UI
{
/// <summary>
/// 测试项目的种类
/// </summary>
public enum TestType
{
NumericalType = 0, //数值类型测试
AnimationType = 1, //动画类型测试
ProcessType = 2, // 流程类型测试
ElseType = 3 //自定义测试类型
}
public class TestProcessManager : Singleton<TestProcessManager>
{
public TestType CurTestType;
public void SelectTestProcess(int index)
{
switch (CurTestType)
{
case TestType.NumericalType:
NumericalProcessFlow numericalProcessFlow = new NumericalProcessFlow(index);
numericalProcessFlow.CreateTestPanel();
break;
case TestType.AnimationType:
break;
case TestType.ProcessType:
break;
}
}
protected override void Initialize()
{
}
protected override void UnInitialize()
{
}
}
}