【解决方案之道(三)】资源导入与审查
一、需求
在系列文章《【解决方案之道(二)】场景资源管理》中,我们讲解了资源从导入到运用的整个项目过程。
其中我们设计出了一种确保资源有效性的方案,该方案的目的就是确保项目每一个资源都是通过自动审查与人工审查两个步骤,同时为之后的资源使用作出准备。
如果你对当前文章的需求有疑问的,可以在系列文章《解决方案之道(二)场景资源管理》中看到相关的设计思想。
二、设计原理
在核心文章中,我们设计出了这样的流程
这是一个最初的设计思想,如果我们要把它系统化,还需要更多的内容,在本章节,我们就会对它进行更加细致的考虑。
作业流程
一个系统是分为多个子系统的,要明白我们需要什么就需要把整个业务流程想出来。
在以往的开发过程中,我们大部分时候只考虑到技术上的需求,所以在资源导入这一块并没有太多的功能需要去开发。现在,有了解决方案的思想方式后,我们就需要把人为因素,项目效率与品质因素都考虑进来。
于是就有了下面这样的作业流程:
1. 资源发起人:
a) 简介:一般来说,就是策划,他们会对某种资源提出了需求。
b) 职能:
i. 发起(2)
ii. 在资源表加入新的资源配置。
2. 任务提交过程:
a) 简介:资源发起人对资源有了确定需求后,就向设计人员提出了设计任务。
3. 设计出任务:
a) 简介:设计人员将任务设计完成后,导入资源导项目工程内,并自行对其进行自动校验。
b) 职能:
i. 使用自动检测工具检测提交的资源。
ii. 将资源状态设计为“提交”。
4. 通知任务已经提交:
a) 简介:告知资源发起人,任务已经被提交到了工程中。
b) 职能:
i. 任务发起人人工审查资源,并设置资源状态为通过或不通过。
系统需求
有了上面的流程后,我们就可以整理出一些资源导入相关的需求。
在这张思维图中,红色的部分是我们这一篇文章不去执行的,而绿色的部分,是资源导入与审查流程中所必须的,所以,它们就是我们的系统需求。
有了大体的系统需求后,接下来我们就可以用程序的思维来设计我们的一个作业流程,也可以说是工作的操作过程。
首先,我们需要选择审查方式,也就是刚才所说的两种:“自动审查”与“人工审查”。
如果是自动审查,我们就需要提取出需要审查的资源,然后根据资源类型,对每一种类型进行特定的审查。几乎所有的资源我们都要检查路径,名字。而特殊的资源,比如音频格式的资源,就可能需要检查赫兹大小。又比如说模型资源,就需要检查顶点数量等等。每一种资源的各自的检测参数,是每个项目自己所指定的,可以由其他表来维护。在本章中,我们就不进行过多的扩展。
自动审查完毕后,没有通过的资源就会被设置为未通过状态,如果不是资源设计师本人检测的,那么就需要通知资源设计师重新修改这个资源。
之后就是人工审查,我们工具会提取出每一个提交状态的资源,根据资源类型不同,它将会以各自的方式展现,以便于审查人员的检测。
我们举个例子:
1. 音频文件
a) 2D播放,直接播放,审查人员听就好。
b) 3D播放,环绕播放,远近播放,设计师需要听是否满足3D音效的需求。
2. 模型文件
a) 循环3D展示,设计师检查外形是否满足需求,且是否有贴图,透面等错误。
3. 动画文件
a) 与模型文件绑定,会有对应的模型文件来播放该动画。
4. 其他…
还有更多的文件需要去设计它们的检测方式,这里就只写出几个例子。这样的例子,就展示人工审查的一过程,简单的说,就是让审查人员可以方便的,快速的,去坚持资源是否达到设计需求。
有了这样的过程,我们就可以开始我们的设计过程了。
三、系统设计
因为这是【解决方案之道】系列文章的第一篇技术点,所以我们需要更多的准备工作,用来加速后期的开发速度。这些准备工作都是常规的Unity3d使用的一切小技巧,或小工具,我们会不断去实现,以及维护它们,让它们越来越好用。
首先,我们需要一个伪静态的管理基础类。
伪静态管理类
目的:
由于静态类在u3d中不方便排错,当性能出现问题时,我们难以定位问题所在,所以使用继承于MonoBehaviour的伪静态类作为管理类的基础是项目最基础的要求。
代码:
//===============================================================================
// ProjectName : DJManagerBase.cs
// Class Description : 游戏内管理器的基础类
//Author : John
// CreateTime : 2016年7月26日星期二农历六月廿三
//===============================================================================
// Copyright © John 2016 . Allrights reserved.
//===============================================================================
using UnityEngine;
public class DJManagerBase : MonoBehaviour
where T : DJManagerBase
{
///
/// 管理器的公共实体父节点
///
private static GameObject ManagerRoot;
///
/// 根节点的名称
///
private const string RootName = "9_ManagerRoot";
///
/// 管理器的静态对象
///
private static T mInstance;
///
/// 管理器的名字(打日志用的)
///
private string className;
///
/// 是否调试状态
/// 这个值非常的有必要,在后续开发中我们将它的威力展现出来
///
public static bool IsDebug = true;
///
/// 得到静态对象
///
///
public static T Getinstance()
{
if (mInstance != null)
{
return mInstance;
}
//创建并返回一个对象
return CreateInstance();
}
///
/// 创建一个对象
///
///
private static T CreateInstance()
{
//判断是否有根节点
if (ManagerRoot == null)
ManagerRoot = GameObject.Find(RootName);
if (ManagerRoot == null)
CreateRoot();
//得到管理器名字
string name= typeof(T).Name;
//找到老对象干掉,编译模式下
#if UNITY_EDITOR
var old = GameObject.Find(name);
if (old != null)
DestroyImmediate(old);
#endif
//创建对象
GameObject manager = new GameObject(name);
manager.transform.parent = ManagerRoot.transform;
//添加脚本
mInstance = manager.AddComponent();
//执行初始化函数(虽然有Start可以调用,但手动调用堆栈信息较为清晰)
mInstance.Init();
return mInstance;
}
///
/// 创建一个根节点
///
///节点对象
private static void CreateRoot()
{
ManagerRoot = new GameObject();
ManagerRoot.name = RootName;
if (Application.isPlaying)
DontDestroyOnLoad(ManagerRoot);
}
///
/// 初始化
///
public virtual void Init()
{
}
///
/// 该系统的常规日志
///
///
public void Log(string _context)
{
if (IsDebug)
Debug.Log(string.Format(":{0}",_context));
}
///
/// 该系统的错误日志
///
///
public void LogEror(string _context)
{
Debug.LogError(string.Format(":{0}", _context));
}
}
在管理器中,我不仅实现了伪静态,还实现了一个细节的小功能,就是Debug状态,当为Debug状态时,就打印该管理器有关的日志。这样就做到了一个简单的日志输出的模块化。
通常来说,我们会把Unity的Debug工具封装一次,以避免在游戏运行时影响性能,因为我们第一篇文章内容过多,就不进行这样的封装。
管理器最主要的功能就是实现了单例,我还做了一些细节上的处理,将它们放到了同一个目录下,并且,给这个目录编号为9,这样在游戏内,如果所有的实例化对象都有一个编号,它就能总在数字编号的最后面。
好的,有了管理器的基础类后,我们就可以编写第一个功能模块。
资源表
目的:
在我们的作业流程中,所有的步骤依赖于一张资源表,所以我们需要先把资源表定义出来。
代码:
首先,我们写一个资源枚举类,表明我们所需要的资源,这里我就先随便写几个。
///
/// 资源类型
///
[Serializable]
public enum AssetType
{
///
/// 未赋值
///
None = 0,
///
/// 音频资源
///
Audio = 1,
///
/// 角色资源
///
Charactor = 2,
///
/// 动画资源
///
Animation = 3,
///
/// 场景资源
///
Secene = 4,
///
///Unity本身的表文件
///
UnityTable = 5
}
然后是资源表的定义类,里面包含了资源类型与资源模板。每份资源都有属于自己的类型,而每种资源类型都有它自己的目录、后缀,所以我们把资源类型,单独拿出来定义。
///
/// 资源配置类
///
[Serializable]
public class AssetsConfig
{
///
/// 资源类型
///
public AssetType type;
///
/// 资源路径
///
public string path;
///
/// 后缀
/// (无法通过类型校验的文件用后缀来校验)
/// (理论上,所有的资源都可以通过类型校验)
///
public string suffix;
}
这是资源类型的,有了资源类型后,我们就可以定义资源的模板。里面它还包含了资源的状态枚举。
///
/// 资源状态
///
public enum AssetState
{
///
/// 设计中
///
Designing = 1,
///
/// 已提交
///
Submit = 2,
///
/// 审核通过
///
Approved = 3,
///
/// 错误
///
Error
}
///
/// 资源表模板
///
[Serializable]
public class DJAssetDataModel
{
///
/// 资源编号
///
public int id;
///
/// 资源名字
///
public string name;
///
/// 资源类型
///
public AssetType type;
///
/// 说明
///
public string des;
///
/// 资源的md5验证码
///
public string md5;
///
/// 资源状态
///
public AssetState state;
}
最后,“资源类型”+“资源模板”就可以帮助我们通过一条配置信息,找到它所处的位置。下面是完整的定义代码。
////===============================================================================
// ProjectName : DJAssetsEditorDefine.cs
// Class Description : 资源管理器的编辑器定义类
//Author : John
// CreateTime : 2016年8月14日星期日农历七月十二
//===============================================================================
// Copyright © John 2016 . Allrights reserved.
//===============================================================================
using UnityEngine;
using System.Collections.Generic;
using DJAssetsDefine;
namespace DJAssetsEditorDefine
{
///
/// 资源检测类型
///
public enum CheckType
{
///
/// 未选择
///
None,
///
/// 自动审查所以资源
///
AutoCheck,
///
/// 人工审查某类资源
///
TypeCheck
}
///
/// 手动检查的检查过程
///
public enum TypeCheckState
{
///
/// 检查过程
///
Proc,
///
/// 打印结果
///
PrintResult,
}
///
/// 资源检测的数据类
///
public class CheckEditorData
{
///
/// 审查方式
///
public CheckType mCheckType = CheckType.None;
///
/// 审查资源类型.(只有再审查方式为类型审查时候才有用)
///
public AssetType mAssetType = AssetType.None;
#region 自动检测用的变量
///
/// 当前检索到了哪个资源
///
public int CheckingIndex;
///
/// 检测中资源的key列表
///
public List CheckingkeyList = new List();
#endregion
#region 手动检测用的变量
///
/// 手动检查状态
///
public TypeCheckState mTypeCheckState;
///
/// 所有已经提交的资源列表
///
public List<DJAssetDataModel> SubmitAssetsList;
///
/// 通过列表
///
public List<DJAssetDataModel> PassList = new List<DJAssetDataModel>();
///
/// 拒绝列表
///
public List<DJAssetDataModel> RefuseList = new List<DJAssetDataModel>();
#endregion
}
}
接下来,我们还需要把这种类型转变为Unity中的配置表,以便于存储。
////===============================================================================
// ProjectName : DJResourceTable.cs
// Class Description : 资源表的定义文件
//Author : John
// CreateTime : 2016年8月6日星期六农历七月初四
//===============================================================================
// Copyright © John 2016 . Allrights reserved.
//===============================================================================
using System.Collections.Generic;
using DJAssetsDefine;
public class DJResourceTable : DJTableBase
{
public List<DJAssetDataModel> Datas;
public override void Init()
{
Datas = new List<DJAssetDataModel>();
}
}
上面这是资源模板表,下面是资源类型表。
////===============================================================================
// ProjectName : DJAssetsDefine.cs
// Class Description : 资源管理器的定义文件
//Author : John
// CreateTime : 2016年7月26日星期二农历六月廿三
//===============================================================================
// Copyright © John 2016 . Allrights reserved.
// ===============================================================================
using System.Collections.Generic;
using DJAssetsDefine;
public class DJAssetTypeTable : DJTableBase
{
public List<AssetsConfig> Datas;
public override void Init()
{
base.Init();
Datas = new List<AssetsConfig>();
}
}
最后,我们还需要使用下面的函数将这两个表生成出来。
///
/// 资源表路径
///
private const string path = "/DJAsset/Table/Dev";
///
/// 创建表
///
///
public void CreateTable()
where T : DJTableBase
{
T sd = ScriptableObject.CreateInstance();
sd.Init();
//检测是否有path目录,
Debug.Log(Application.dataPath);
Directory.CreateDirectory(Application.dataPath + path);
//这里采用+连接字符串因为需要path目录
string p = "Assets/" + path + "/" + typeof(T).Name + ".asset";
EditorUtility.SetDirty(sd);
AssetDatabase.CreateAsset(sd, p);
}
现在,工程里这两张表已经被生成到了指定的目录中,同样因为本章才开始资源导入,我们先不做资源打包这一块,所以我们将表放入到常规的资源目录中。
同时,我们为两张表,手动填入简单的数据,方便我们后续开发。
好啦,表的准备工作搞定。
资源数据管理器
目的:
有了表,我们就需要读表,以及存储数据,有一个数据管理器,可以帮助我们方便的调用数据。当然,最后我们需要把表与数据管理写成通用的方法来管理。
代码:
//// ===============================================================================
// ProjectName : DJAssetsDataManager.cs
// Class Description : 资源数据管理器
//Author : John
// CreateTime : 2016年7月26日星期二农历六月廿三
// ===============================================================================
// Copyright © John 2016 . Allrights reserved.
//===============================================================================
using System.Collections.Generic;
using DJAssetsDefine;
using UnityEngine;
public class DJAssetsDataManager : DJManagerBase<DJAssetsDataManager>
{
///
/// 资源字典
///
public Dictionary<int, DJAssetDataModel> AssetsDict = new Dictionary<int, DJAssetDataModel>();
///
/// 资源类型字典
///
public Dictionary<AssetType, AssetsConfig> AssetTypeDict = new Dictionary<AssetType, AssetsConfig>();
///
/// 所有资源的起点,所以硬编码
///
private const string TablePath_Resource = "DJAsset/Table/Dev/DJResourceTable";
///
/// 资源类型
///
private const string TablePath_AssetType = "DJAsset/Table/Dev/DJAssetTypeTable";
///
/// 资源表
///
private DJResourceTable assetsTable;
///
/// 加载资源
///
public void LoadData()
{
var typeTable = Resources.Load<DJAssetTypeTable>(TablePath_AssetType);
if (typeTable == null)
{
Debug.LogError("没有读取到表:" + TablePath_AssetType);
return;
}
if (typeTable.Datas.Count == 0)
{
Debug.LogError("表数据为0." + TablePath_AssetType);
return;
}
assetsTable = Resources.Load<DJResourceTable>(TablePath_Resource);
if (assetsTable == null)
{
Debug.LogError("没有读取到表:" + TablePath_Resource);
return;
}
if (assetsTable.Datas.Count == 0)
{
Debug.LogError("表数据为0." + TablePath_Resource);
return;
}
//将表里的内容存入字典
fillTypeDict(0,typeTable.Datas);
//然后是资源表
fillAssetsDict(0,assetsTable.Datas);
}
///
/// 填充类型资源表
///
///索引
///数据队列
private void fillTypeDict(int _index, List<AssetsConfig> _datas)
{
if (_index >= _datas.Count)
return;
var _data = _datas[_index];
if (AssetTypeDict.ContainsKey(_data.type) == true)
{
//如果有资源了,报错
Debug.LogError("资源类型表中有2条记录指向同一种资源:" + _data.type);
return;
}
AssetTypeDict.Add(_data.type, _data);
if (IsDebug)//再判断一次是避免非调试状态下拼接字符串
{
Log("加载了资源类型表,index:" + _index + "类型" + _data.type);
}
fillTypeDict(_index + 1, _datas);
}
///
/// 填充资源表
///
///索引
///数据队列
private void fillAssetsDict(int _index, List<DJAssetDataModel> _datas)
{
if (_index >= _datas.Count)
return;
var _data = _datas[_index];
if (AssetsDict.ContainsKey(_data.id) == true)
{
//如果有资源了,报错
Debug.LogError("资源表中有2条记录指向同一种资源:" + _data.type);
return;
}
AssetsDict.Add(_data.id, _data);
if (IsDebug)//再判断一次是避免非调试状态下拼接字符串
{
Log("加载了资源表,index:" + _index + "类型" + _data.type + "名称" + _data.name);
}
fillAssetsDict(_index + 1, _datas);
}
///
/// 得到该参数1状态的所有资源配置
///
///资源状态
public List<DJAssetDataModel> GetAssetsByState(AssetState _state)
{
List<DJAssetDataModel> aseetsList = new List<DJAssetDataModel>();
foreach (var _tempConfig in AssetsDict.Values)
{
if (_tempConfig.state== _state)
aseetsList.Add(_tempConfig);
}
return aseetsList;
}
#if UNITY_EDITOR
///
/// 改变资源状态,只有在编辑器状态才可以使用
///
///
///
public void SetAssetState(int _id, AssetState _state)
{
if (assetsTable == null)
return;
AssetsDict[_id].state = _state;
//for(int i = 0; i < assetsTable.Datas.Count; i++)
//{
// if(assetsTable.Datas[i].id == _id)
// {
// assetsTable.Datas[i].state= _state;
// break;
// }
//}
UnityEditor.EditorUtility.SetDirty(assetsTable);
}
#endif
}
继承于刚才实现的管理器基类,可以很方便的让我们在游戏内调用它。代码内注释已经较为齐全,这里就不多讲。接下来,是资源表。
基于Editor的审查框架
设计思想:
在项目中,资源是通用,类型是分开的,基于这一点,我将每份资源看做一个单元,然后每一种类型的资源共用同一种单元。也就是,每种类型我们开发一个资源检查脚本。
脚本包含功能:
1. 该类资源加载
2. 该类资源自动审查逻辑
3. 该类资源人工审查逻辑(包含人工操作)
代码:
有了上面按类型划分的脚本单元需求后,我们就设计出这样的单元基类。
////===============================================================================
// Project Name : DJAssetsCheckBase.cs
// Class Description : 资源监测的基本类
//Author : John
// CreateTime : 2016年7月26日星期二农历六月廿三
//===============================================================================
// Copyright © John 2016 . Allrights reserved.
//===============================================================================
using UnityEngine;
using DJAssetsDefine;
using System.Collections.Generic;
public class DJAssetsCheckBase
{
///
/// 编辑器下的信息
///
public virtual List EditorInfos { get; set; }
///
/// 物品id
///
public int id;
///
/// 我的资源
///
protected virtual Object mAsset { get; set; }
///
/// 加载资源
///
///
public virtual bool LoadAsset(int _id)
{ return true; }
///
/// 加载资源
///
///
///
public virtual bool LoadAsset(DJAssetDataModel _model)
{ return true; }
///
/// 检测资源
///
///
public virtual bool CheckAsset()
{
return true;
}
///
/// 打印资源信息
///
public virtual void PrintInfo()
{
}
///
/// 绘制操作界面的GUI
///
public virtual void DrawEditorTestGUI() { }
///
/// 退出,用来清理检测时创建的临时资源
///
public virtual void Quit() { }
///
/// 初始化,用来准备检测资源用的环境
///
public virtual void Init() { }
}
里面主要包含了初始化,自动检查,人工检查,以及加载几个虚函数。有了这样的基类后,我们就可以方便的写出不同类型的检查脚本。这里先不急着写,我们先稍微讲解一下Unity中Editor的绘制。
在本篇文章中,我们使用最简单的基于Inspector窗口的一个自定义编辑器。也就是这个窗口。要使用这个窗口,我们就需要定义一个可以挂在窗口上的常规类
,和一个负责编辑状态下绘制的Editor的绘制类
。
因为Editor类不参与项目打包,所以我们需要把它放到Editor目录下,就可以构成一组窗口关系,最终实现出自定义的编辑状态下的工具界面。
这里只是提一下,如果你会了,直接看代码就好,如果读者不太明白编辑器的编写,可以暂停了去看一下相关文章,试一试,这里就不过多阐述。
接下来,是编辑器的开发,我们需要定义出该界面需要的数据。
检测类型枚举
///
/// 资源检测类型
///
public enum CheckType
{
///
/// 未选择
///
None,
///
/// 自动审查所以资源
///
AutoCheck,
///
/// 人工审查某类资源
///
TypeCheck
}
资源检测的数据类
///
/// 资源检测的数据类
///
public class CheckEditorData
{
///
/// 审查方式
///
public CheckType mCheckType = CheckType.None;
#region 自动检测用的变量
///
/// 当前检索到了哪个资源
///
public int CheckingIndex;
///
/// 检测中资源的key列表
///
public List CheckingkeyList = new List();
#endregion
}
这些是自动检查所需要的一些数据,我们将它放到编辑器类的可挂在脚本中,这样做的好处是当我们鼠标在不同的gameobject对象间切换时,界面数据不会丢失。
//===============================================================================
// Project Name : DJAssetsCheckTools.cs
// Class Description : 资源检测工具
//Author : John
// CreateTime : 2016年7月26日星期二农历六月廿三
//===============================================================================
// Copyright © John 2016 . Allrights reserved.
//===============================================================================
using UnityEngine;
using DJAssetsEditorDefine;
public class DJAssetsCheckTools : MonoBehaviour
{
public CheckEditorData Data = new CheckEditorData();
}
然后,我们就需要开始编辑它对应的绘制类型。主要是的绘制过程在OnInspectorGUI()函数中完成,但也需要初始化过程。
编辑器的初始化
[CustomEditor(typeof(DJAssetsCheckTools))]
public class DJAssetsCheckToolsEditor : Editor
{
///
/// 界面数据
///
CheckEditorData Data;
///
/// 是否初始化
///
private bool isInit = false;
///
/// 资源检测类型,
///
private CheckType tempCheckType = CheckType.None;
void Init()
{
//编辑器数据
Data = (target as DJAssetsCheckTools).Data;
//初始化表
DJAssetsDataManager.Getinstance().LoadData();
isInit = true;
}
public override void OnInspectorGUI()
{
if (isInit == false) Init();
if (Data == null)
Init();
}
}
初始化以后,编辑器也就有数据对象,资源表与类型表也都被加载到了内存当中,供给我们使用。
审查界面的跳转
在我们的作业流程中,审查人员可以选择自动审查与人工审查,这意味着有两种审查方式我们提供给审查人员,那么编辑器的初始界面,就会是一个选择页面,让审查人员选择自己将要进行的审查方式。
//选择检测类型的首页面绘制
if (Data.mCheckType == CheckType.None)
{
TopTips = "请选择检查类型";
var checkType = EditorGUILayout.EnumPopup("检测类型:", tempCheckType);
tempCheckType= (CheckType)checkType;
if (tempCheckType!= Data.mCheckType)
{
if (GUILayout.Button("开始检测"))
{
ChangeState(tempCheckType);
}
}
return;
}
这是在OnInspectorGUI()函数里的代码,调用了下面这些审查类型切换的代码
#region 审查类型切换
void ChangeState(CheckType _type)
{
switch (_type)
{
case CheckType.None:
ToNoneSate();
break;
case CheckType.TypeCheck:
ToTypeCheckState();
break;
case CheckType.AutoCheck:
ToAutoCheckState();
break;
}
Data.mCheckType= _type;
tempCheckType = Data.mCheckType;
}
void ToNoneSate()
{
}
void ToTypeCheckState()
{
//得到所有已经提交资源的列表
Data.SubmitAssetsList = DJAssetsDataManager.Getinstance().GetAssetsByState(AssetState.Submit);
//清除索引
Data.CheckingIndex = 0;
//初始化状态
Data.mTypeCheckState= TypeCheckState.Proc;
}
///
/// 得改成异步的啊
///
void ToAutoCheckState()
{
//锁定编辑窗口
//代码偷自:http://www.xuanyusong.com/archives/3796
var type = typeof(EditorWindow).Assembly.GetType("UnityEditor.InspectorWindow");
var window = EditorWindow.GetWindow(type);
MethodInfo info = type.GetMethod("FlipLocked", BindingFlags.NonPublic | BindingFlags.Instance);
info.Invoke(window, null);
var allAssets = DJAssetsDataManager.Getinstance().AssetsDict;
Data.CheckingIndex = 0;
Data.CheckingkeyList.Clear();
ErrorList.Clear();
foreach (var _key in allAssets.Keys)
{
Data.CheckingkeyList.Add(_key);
}
}
#endregion
在编辑类的定义中,我们定义审查状态,而上面的代码,就是帮助我们在不同的审查状态间切换。其中包含了人工审查与自动审查,并且为了避免使用的人在审查过程中乱切换Gameobject,这里还使用了黑科技,锁定了Inspector窗口。
当代码完成后,界面效果会是这样的。当我们选择一个类型后,就会出现新的按钮
当点击开始检查后,我们的审查流程,就会进入到自动审查里。
自动审查
#region 自动审查
void OnAutoCheckGUI()
{
EditorGUILayout.LabelField("检测结束前,请不要取消锁定");
if (Data.CheckingIndex < Data.CheckingkeyList.Count)
{
TopTips = "自动检测进度:(" + Data.CheckingIndex + "/" + Data.CheckingkeyList.Count + ")";
//循环所有资源,并进行检查
var allAssets = DJAssetsDataManager.Getinstance().AssetsDict;
var _config =allAssets[Data.CheckingkeyList[Data.CheckingIndex]];
//设计中的资源就不检测了
if (_config.state == DJAssetsDefine.AssetState.Designing)
{
Data.CheckingIndex += 1;
return;
}
var testUnit = DJAssetsCheckFactory.GetCheckUnit(_config.type);
//得到对应的检测脚本
if (testUnit == null)
{
//这里就不写出所有类型的测试脚本了
Data.CheckingIndex += 1;
return;
}
if (testUnit.LoadAsset(_config.id) == false ||
testUnit.CheckAsset() == false)
{
ErrorList.Add(testUnit);
Data.CheckingIndex += 1;
return;
}
Data.CheckingIndex += 1;
return;
}
if (ErrorList.Count > 0)
{
EditorGUILayout.BeginScrollView(new Vector2(300, 1200));
//执行到这里检测已经完毕了,就打印结果
for (int i = 0; i < ErrorList.Count; i++)
{
var _data = ErrorList[i];
for (int j = 0; j < _data.EditorInfos.Count; j++)
{
EditorGUILayout.LabelField(_data.EditorInfos[j]);
}
EditorGUILayout.Space();
}
EditorGUILayout.EndScrollView();
TopTips = "共有" + ErrorList.Count + "份资源没有通过自动检查.注意,当退出检测模式时,未通过的资源会被设置为错误状态。";
}
else
{
TopTips = "所有资源通过检查";
}
if (ErrorList.Count > 0)
{
//方便将错误列表转发给各个负责人
if (GUILayout.Button("将错误列表复制到剪切板中"))
{
string errorListStr = "";
for (int i = 0; i < ErrorList.Count; i++)
{
var _config = DJAssetsDataManager.Getinstance().AssetsDict[ErrorList[i].id];
errorListStr+= _config.id + " " + _config.type + "" + _config.name + "n";
}
var te = new TextEditor();
te.text= errorListStr;
te.OnFocus();
te.Copy();
}
}
if (GUILayout.Button("退出检查模式"))
{
for (int i = 0; i < ErrorList.Count; i++)
{
var _data = ErrorList[i];
DJAssetsDataManager.Getinstance().SetAssetState(_data.id, AssetState.Error);
}
ChangeState(CheckType.None);
}
}
#endregion
这里代码比较多,需要细看,主要思想就是每一个绘制帧去加载一份资源,然后通过该资源的测试类去自动检测该资源是否能够通过审查。按照我们设计的,如果通过审查,就略过,但没有通过审查,我们就需要打印出来。这里还提供了小工具,可以把错误的资源列表复制到剪切板中,以便于审查人员得到这些信息并保存。
其中,在每一绘制帧去加载资源,是为了保证编辑器不会卡死,能够顺利的显示进度。而TopTips,就是一个顶端的信息字符串,会被一直打印,通过改变它可以告诉用户一些信息。
这就是结果的图,因为这里还没有讲解音频类型的资源脚本,所以先不讲解测试过程。我们继续看人工审查的部分。
人工审查
在框架中,人工审查部分就会相对简单,它只用负责好资源的上下切换,具体资源是如何被测试的,就需要通过具体的类型单元测试脚本去完成绘制。
我直接给出整个Editor绘制脚本的代码,以便于我们理解。首先,是完整的定义类。
////===============================================================================
// ProjectName : DJAssetsEditorDefine.cs
// Class Description : 资源管理器的编辑器定义类
//Author : John
// CreateTime : 2016年8月14日星期日农历七月十二
//===============================================================================
// Copyright © John 2016 . Allrights reserved.
// ===============================================================================
using UnityEngine;
using System.Collections.Generic;
using DJAssetsDefine;
namespace DJAssetsEditorDefine
{
///
/// 资源检测类型
///
public enum CheckType
{
///
/// 未选择
///
None,
///
/// 自动审查所以资源
///
AutoCheck,
///
/// 人工审查某类资源
///
TypeCheck
}
///
/// 手动检查的检查过程
///
public enum TypeCheckState
{
///
/// 检查过程
///
Proc,
///
/// 打印结果
///
PrintResult,
}
///
/// 资源检测的数据类
///
public class CheckEditorData
{
///
/// 审查方式
///
public CheckType mCheckType = CheckType.None;
///
/// 审查资源类型.(只有再审查方式为类型审查时候才有用)
///
public AssetType mAssetType = AssetType.None;
#region 自动检测用的变量
///
/// 当前检索到了哪个资源
///
public int CheckingIndex;
///
/// 检测中资源的key列表
///
public List CheckingkeyList = new List();
#endregion
#region 手动检测用的变量
///
/// 手动检查状态
///
public TypeCheckState mTypeCheckState;
///
/// 所有已经提交的资源列表
///
public List<DJAssetDataModel> SubmitAssetsList;
///
/// 通过列表
///
public List<DJAssetDataModel> PassList = new List<DJAssetDataModel>();
///
/// 拒绝列表
///
public List<DJAssetDataModel> RefuseList = new List<DJAssetDataModel>();
#endregion
}
}
然后,是完整的Editor类,它是本文中最长的代码了。
//// ===============================================================================
// ProjectName : DJAssetsCheckToolsEditor.cs
// Class Description : 审查工具的Editor绘制类
//Author : John
// CreateTime : 2016年8月14日星期日农历七月十二
//===============================================================================
// Copyright © John 2016 . Allrights reserved.
//===============================================================================
using UnityEngine;
using System.Collections;
using UnityEditor;
using DJAssetsEditorDefine;
using System.Collections.Generic;
using DJAssetsDefine;
using System.Reflection;
[CustomEditor(typeof(DJAssetsCheckTools))]
public class DJAssetsCheckToolsEditor : Editor
{
///
/// 界面数据
///
CheckEditorData Data;
///
/// 顶端文字
///
string TopTips = "";
///
/// 错误的资源列表
///
private List<DJAssetsCheckBase> ErrorList = new List<DJAssetsCheckBase>();
///
/// 是否初始化
///
private bool isInit = false;
///
/// 资源检测类型,
///
private CheckType tempCheckType = CheckType.None;
#region 人工检测用到的变量
///
/// 当前的检测单元
///
private DJAssetsCheckBase currentCheckUnit;
///
/// 当前配置
///
public DJAssetDataModel currentConfig
{
get { return Data.SubmitAssetsList[Data.CheckingIndex]; }
}
#endregion
void Init()
{
//编辑器数据
Data = (target as DJAssetsCheckTools).Data;
//初始化表
DJAssetsDataManager.Getinstance().LoadData();
isInit = true;
}
public override void OnInspectorGUI()
{
if (isInit == false) Init();
if (Data == null)
Init();
EditorGUILayout.LabelField("Tips:" + TopTips);
//switch语法看起来标准,但个人觉得代码阅读性不如下面没有注释的
//switch (Data.mCheckType)
//{
// case CheckType.None:
// TopTips= "请选择检查类型";
// varcheckType = EditorGUILayout.EnumPopup("检测类型:", tempCheckType);
// tempCheckType= (CheckType)checkType;
// if(tempCheckType != Data.mCheckType)
// {
// if(GUILayout.Button("开始检测"))
// {
// ChangeState(tempCheckType);
// }
// return;
// }
// break;
// case CheckType.AutoCheck:
// OnAutoCheckGUI();
// break;
// case CheckType.TypeCheck:
// if(Data.mTypeCheckState == TypeCheckState.Proc)
// OnManualCheckGUI_Proc();
// else
// OnManualCheckGUI_Result();
// break;
//}
//选择检测类型的首页面绘制
if (Data.mCheckType == CheckType.None)
{
TopTips = "请选择检查类型";
var checkType = EditorGUILayout.EnumPopup("检测类型:", tempCheckType);
tempCheckType= (CheckType)checkType;
if (tempCheckType!= Data.mCheckType)
{
if (GUILayout.Button("开始检测"))
{
ChangeState(tempCheckType);
}
}
return;
}
//自动检测页面
if (Data.mCheckType == CheckType.AutoCheck)
{
OnAutoCheckGUI();
return;
}
//人工检测页面
if (Data.mCheckType == CheckType.TypeCheck)
{
if (Data.SubmitAssetsList.Count == 0)
{
EditorGUILayout.LabelField("没有状态为Submit的资源需要人工检测");
if (GUILayout.Button("返回"))
{
ToNoneSate();
}
return;
}
//人工检测过程有2个界面,区分开来绘制代码比较清晰
if (Data.mTypeCheckState == TypeCheckState.Proc)
OnManualCheckGUI_Proc();
else
OnManualCheckGUI_Result();
return;
}
}
#region 审查类型切换
void ChangeState(CheckType _type)
{
switch (_type)
{
case CheckType.None:
ToNoneSate();
break;
case CheckType.TypeCheck:
ToTypeCheckState();
break;
case CheckType.AutoCheck:
ToAutoCheckState();
break;
}
Data.mCheckType = _type;
tempCheckType = Data.mCheckType;
}
void ToNoneSate()
{
}
void ToTypeCheckState()
{
//得到所有已经提交资源的列表
Data.SubmitAssetsList = DJAssetsDataManager.Getinstance().GetAssetsByState(AssetState.Submit);
//清除索引
Data.CheckingIndex = 0;
//初始化状态
Data.mTypeCheckState= TypeCheckState.Proc;
}
///
/// 得改成异步的啊
///
void ToAutoCheckState()
{
//锁定编辑窗口
//代码偷自:http://www.xuanyusong.com/archives/3796
var type = typeof(EditorWindow).Assembly.GetType("UnityEditor.InspectorWindow");
var window = EditorWindow.GetWindow(type);
MethodInfo info = type.GetMethod("FlipLocked", BindingFlags.NonPublic | BindingFlags.Instance);
info.Invoke(window, null);
var allAssets = DJAssetsDataManager.Getinstance().AssetsDict;
Data.CheckingIndex = 0;
Data.CheckingkeyList.Clear();
ErrorList.Clear();
foreach (var _key in allAssets.Keys)
{
Data.CheckingkeyList.Add(_key);
}
}
#endregion
#region 手动审查
///
/// 人工检测结果页面
///
void OnManualCheckGUI_Result()
{
//打印成功检测的清单
EditorGUILayout.LabelField("检测成功的资源清单,共" + Data.PassList.Count + "个");
for (int i = 0; i < Data.PassList.Count; i++)
{
var _config = Data.PassList[i];
EditorGUILayout.LabelField(i + ": " +_config.id + " " + _config.type + "" + _config.name);
}
EditorGUILayout.Space();
//打印检测失败的清单
EditorGUILayout.LabelField("检测失败的资源清单,共" + Data.RefuseList.Count + "个");
for (int j = 0; j < Data.RefuseList.Count; j++)
{
var _config = Data.RefuseList[j];
EditorGUILayout.LabelField(j + ": " +_config.id + " " + _config.type + "" + _config.name);
}
//功能
if (GUILayout.Button("复制失败清单"))
{
string errorListStr = "";
for (int i = 0; i < Data.RefuseList.Count; i++)
{
var _config = Data.RefuseList[i];
errorListStr+= _config.id + " " + _config.type + "" + _config.name + "n";
}
var te = new TextEditor();
te.text= errorListStr;
te.OnFocus();
te.Copy();
}
if (GUILayout.Button("保存审查结果并退出"))
{
if (currentCheckUnit != null)
currentCheckUnit.Quit();
//保存没有通过的
for (int j = 0; j < Data.RefuseList.Count; j++)
{
var _config = Data.RefuseList[j];
DJAssetsDataManager.Getinstance().SetAssetState(_config.id, AssetState.Designing);
}
//保存通过审核的
for (int i = 0; i < Data.PassList.Count; i++)
{
var _config = Data.PassList[i];
DJAssetsDataManager.Getinstance().SetAssetState(_config.id, AssetState.Approved);
}
//退回普通模式中
ChangeState(CheckType.None);
}
}
///
/// 人工检测过程页面
///
void OnManualCheckGUI_Proc()
{
//得到检测单元
if (currentCheckUnit== null)
{
currentCheckUnit= DJAssetsCheckFactory.GetCheckUnit(currentConfig.type);
currentCheckUnit.Init();
currentCheckUnit.LoadAsset(currentConfig.id);//如果加载失败,应该有一些提示,但是这里就先不提示了
}
EditorGUILayout.Space();
currentCheckUnit.DrawEditorTestGUI();
EditorGUILayout.Space();
EditorGUILayout.BeginHorizontal();
if (Data.CheckingIndex > 0 && GUILayout.Button("上一个")) ToLast();
if (Data.CheckingIndex < Data.SubmitAssetsList.Count - 1 && GUILayout.Button("下一个")) ToNext();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
if (GUILayout.Button("通过检测"))
{
TopTips = "操作:通过检查";
//加入成功队列
if (Data.PassList.Contains(currentConfig) == false)
Data.PassList.Add(currentConfig);
if (Data.RefuseList.Contains(currentConfig) == true)
Data.RefuseList.Remove(currentConfig);
ToNext();
}
if (GUILayout.Button("没通过"))
{
TopTips = "操作:没有通过检查";
//加入失败队列
if (Data.RefuseList.Contains(currentConfig) == false)
Data.RefuseList.Add(currentConfig);
if (Data.PassList.Contains(currentConfig) == true)
Data.PassList.Remove(currentConfig);
ToNext();
}
EditorGUILayout.Space();
if (GUILayout.Button("结束检查"))
{
Data.mTypeCheckState= TypeCheckState.PrintResult;
}
}
///
/// 到上一个
///
private void ToLast()
{
if (Data.CheckingIndex == 0)
{
TopTips = "当前是第一个资源";
return;
}
Data.CheckingIndex--;
currentCheckUnit.Quit();
currentCheckUnit = DJAssetsCheckFactory.GetCheckUnit(currentConfig.type);
}
///
/// 到下一个
///
private void ToNext()
{
if (Data.CheckingIndex == Data.SubmitAssetsList.Count - 1)
{
TopTips = "已经是最后一个资源了";
return;
}
Data.CheckingIndex++;
currentCheckUnit.Quit();
currentCheckUnit = DJAssetsCheckFactory.GetCheckUnit(currentConfig.type);
}
#endregion
#region 自动审查
void OnAutoCheckGUI()
{
EditorGUILayout.LabelField("检测结束前,请不要取消锁定");
if (Data.CheckingIndex < Data.CheckingkeyList.Count)
{
TopTips = "自动检测进度:(" + Data.CheckingIndex + "/" + Data.CheckingkeyList.Count + ")";
//循环所有资源,并进行检查
var allAssets = DJAssetsDataManager.Getinstance().AssetsDict;
var _config =allAssets[Data.CheckingkeyList[Data.CheckingIndex]];
//设计中的资源就不检测了
if (_config.state == DJAssetsDefine.AssetState.Designing)
{
Data.CheckingIndex += 1;
return;
}
var testUnit = DJAssetsCheckFactory.GetCheckUnit(_config.type);
//得到对应的检测脚本
if (testUnit == null)
{
//这里就不写出所有类型的测试脚本了
Data.CheckingIndex += 1;
return;
}
if (testUnit.LoadAsset(_config.id) == false ||
testUnit.CheckAsset() == false)
{
ErrorList.Add(testUnit);
Data.CheckingIndex += 1;
return;
}
Data.CheckingIndex += 1;
return;
}
if (ErrorList.Count > 0)
{
EditorGUILayout.BeginScrollView(new Vector2(300, 1200));
//执行到这里检测已经完毕了,就打印结果
for (int i = 0; i < ErrorList.Count; i++)
{
var _data = ErrorList[i];
for (int j = 0; j < _data.EditorInfos.Count; j++)
{
EditorGUILayout.LabelField(_data.EditorInfos[j]);
}
EditorGUILayout.Space();
}
EditorGUILayout.EndScrollView();
TopTips = "共有" + ErrorList.Count + "份资源没有通过自动检查.注意,当退出检测模式时,未通过的资源会被设置为错误状态。";
}
else
{
TopTips = "所有资源通过检查";
}
if (ErrorList.Count > 0)
{
//方便将错误列表转发给各个负责人
if (GUILayout.Button("将错误列表复制到剪切板中"))
{
string errorListStr = "";
for (int i = 0; i < ErrorList.Count; i++)
{
var _config = DJAssetsDataManager.Getinstance().AssetsDict[ErrorList[i].id];
errorListStr+= _config.id + " " + _config.type + "" + _config.name + "n";
}
var te = new TextEditor();
te.text= errorListStr;
te.OnFocus();
te.Copy();
}
}
if (GUILayout.Button("退出检查模式"))
{
for (int i = 0; i < ErrorList.Count; i++)
{
var _data = ErrorList[i];
DJAssetsDataManager.Getinstance().SetAssetState(_data.id, AssetState.Error);
}
ChangeState(CheckType.None);
}
}
#endregion
}
最终实现出的效果,会是下面这个样子。
或者
被涂掉的部分,是类型文件所绘制的,所以先涂掉,而红色框内的部分,就是当前的绘制脚本所绘制的,如果有2个资源需要审核,它还会有上一个与下一个按钮。当我们都审核完毕后,还会又总结页面。
同样,我们可以复制失败的清单,也可以将审核成功的资源状态设置为审核通过。
这就是我们的一个审核工具的框架,最后,我们再实现一个简单的音频资源的检测脚本,来帮助我们测试这个框架。
音频测试单元
目的:
音频测试单元继承于我们的测试单元基类,自动检测里检测赫兹之类的参数,而人工检测的代码里,则需要绘制它自己的操作界面,并实现检测逻辑。
代码:
////===============================================================================
// ProjectName : DJAssetCheckUnit_Audio.cs
// Class Description : 资源检测单元_音频
// Author : John
// CreateTime : 2016年8月14日星期日农历七月十二
//===============================================================================
// Copyright © John 2016 . Allrights reserved.
//===============================================================================
using UnityEngine;
using DJAssetsDefine;
using System.Collections.Generic;
public class DJAssetCheckUnit_Audiu : DJAssetsCheckBase
{
///
/// 存储:我的资源
///
private AudioClip myAsset;
///
/// 存储:编辑器信息
///
private List myEditorInfos = new List();
///
/// 接口:编辑器信息
///
public override List EditorInfos
{
get
{
return myEditorInfos;
}
set
{
myEditorInfos= value;
}
}
#region 非接口声明
AudioSource audioSource;
#endregion
///
/// 自动检测
///
///
public override bool CheckAsset()
{
bool isPass = true;
//音频赫兹
if (myAsset.frequency> 44100)
{
myEditorInfos.Add("[Error]赫兹超标,当前赫兹:" + myAsset.frequency);
isPass= false;
}
//加载类型
//音频质量
//压缩格式
//音频长度
//等等
if (isPass== true)
{
myEditorInfos.Add("自动审核通过");
}
return isPass;
}
///
/// 初始化
///
public override void Init()
{
if (audioSource == null)
{
var obj = new GameObject();
audioSource= obj.AddComponent<AudioSource>();
obj.AddComponent<AudioListener>();
}
else
{
if (audioSource.isPlaying== true)
audioSource.Stop();
}
}
///
/// 退出
///
public override void Quit()
{
if (audioSource != null)
GameObject.DestroyImmediate(audioSource.gameObject);
}
///
/// 绘制人工测试界面
///
public override void DrawEditorTestGUI()
{
if (GUILayout.Button("播放2d"))
{
if (audioSource.isPlaying == true)
audioSource.Stop();
audioSource.clip= myAsset;
audioSource.Play();
}
if (GUILayout.Button("播放3d"))
{
//1. 区分AudioListener 与 AudioSource
//2. 让AudioSource 围绕着 AudioListener,从近到远,从远到近播放
}
if (GUILayout.Button("停止播放"))
{
if (audioSource.isPlaying == true)
audioSource.Stop();
}
}
public override bool LoadAsset(DJAssetDataModel _model)
{
//懒得实现了
return true;
}
public override bool LoadAsset(int _id)
{
id = _id;
myEditorInfos.Add("资源id:" + _id + " 名称:" + DJAssetsDataManager.Getinstance().AssetsDict[_id].name);
myAsset = DJAssetsManager.Getinstance().Load<AudioClip>(_id);
bool isPass = true;
if (myAsset == null)
{
myEditorInfos.Add("[Error]路径失效或类型错误");
isPass= false;
}
return isPass;
}
public override void PrintInfo()
{
}
}
有了这样的代码后,刚才的人工审核界面,就会多出了三个按钮。
以上部分,就是我们资源审查工具的核心部分的代码。
有了这样的工具后,就可以保证我们导入的每一份资源,都被进行有效的检查,同时在扩展以后还可以保证无效的资源被检测出来。
四、测试与使用
接下来,我们就需要测试我们的小工具。
初始化资源表
假设,我发起了2份资源,“干物妹”与“干物妹1”。其中,干物妹1文件并不存在,但我依然把它设置为提交状态,创造一个“错误”的范例。
自动审查
创建一个场景,添加一个空的对象,并给它挂在脚本
然后选择自动审查
点击开始检测
点击将错误列表复制到剪切板中,测试复制功能,得到下面的结果:
3 Audio 干物妹 1
再点击退出检查模式,并查看资源表。
不存在的资源“干物妹1”已经被设置为Error,这样我们就知道了这个资源有问题。
人工审查
之后,我们再选择人工审查。
因为它是背景音乐,所以我们在测试界面中点击播放2d。(3D也没实现)
我们会在耳机中听到这个音乐被播放出来,所以,我们就点击通过检查,会得到下面的返回信息。
最后,我们再通过结束检查,保存检查状态。
查看资源表,发现“干物妹”资源已经成为了过审状态。
当然,我们还应该检查没通过状态的情况。这里我就不做示范了。
五、结束语
资源导入与审查的流程,是一个项目规范化的起点,所以【解决方案之道】系列文章的第一篇技术点文章,就落到了这里。
因为是第一次写这样的技术文,会有比较乱的地方,提供代码,仅仅为了保证我们想出的解决方案是可行的。当然,它也就可以被直接使用于扩展。其中代码也尽量保证100%的注释覆盖,让大家能轻松的明白每一句代码的含义。
Bug在所难免,主要还是解决方案的设计思想。而后续的文章,我也会继续维护这个工程。文章最初会发表在Gad.qq.com,在原文出处也会有打包的工程源文件。大家可以下载直接查看和使用。