前两天做了游戏(unity项目)中新手引导配置文件检查的功能,配置写在Lua脚本中,询问项目中的前辈后,调用了统一的接口加载游戏中所有Lua文件,获取到LuaTable,对其中按钮路径进行了检查,主要目的是防止prefab更新后未更新配置文件,这会导致新手引导中对应的按钮找不到。反复调试检查后,自信的提交了任务,主程来验收后表示错漏百出,代码格式没遵守规范,循环中重复加载资源,反复调用得到相同结果的函数……
代码风格每个人习惯不一样没有好坏之分就忽略他了,其他通用的记下来自省,也希望能对大家有帮助。
1.函数重复调用
先上优化前代码
Hashtable ReadLuaTableAsHash()
{
Hashtable table = new Hashtable();
// ...具体实现读取LuaTable功能
// ...
return table;
}
bool CheckSingle(string wnd, string btn)
{
bool result = true;
// 具体实现检查路径功能
return result;
}
void Main()
{
Hashtable userGuideConfigTable = ReadLuaTableAsHash();
if(userGuideConfigTable!=null)
{
IEnumerator itr = userGuideConfigTable.Keys.GetEnumerator();
while(itr.MoveNext())
{
// 得到临时的table,其中包含了单个配置信息
Hashtable temp = userGuideConfigTable[itr.Current] as Hashtable;
// 单个配置信息中 interWndName 记录prefab名字
// interBtnPath 记录了prefab中 btn的路径
if (!CheckSingle(temp["interWndName"].ToString(), temp["interBtnPath"].ToString()))
{
Debug.Log(temp["interWndName"] + "中的节点" + temp["interBtnPath"] + " 请修改" + temp["interWndName"] + " 或者配置文件");
}
}
}
}
代码运行起来是完全正确的,程序老鸟和细心的朋友应该已经发现哪里不对了,“[]”只是重载了方法,不管数据结构如何优化都是需要耗费性能的,需要重复使用的数据,只获取一次到变量中,于是做了如下修改。
void Main()
{
Hashtable userGuideConfigTable = ReadLuaTableAsHash();
if (userGuideConfigTable != null)
{
IEnumerator itr = userGuideConfigTable.Keys.GetEnumerator();
while (itr.MoveNext())
{
// 得到临时的table,其中包含了单个配置信息
Hashtable temp = userGuideConfigTable[itr.Current] as Hashtable;
// 单个配置信息中 interWndName 记录prefab名字
// interBtnPath 记录了prefab中 btn的路径
object objInterWndName = temp["interWndName"];
object objInterBtnPath = temp["objInterBtnPath"];
if (objInterWndName == null || objInterBtnPath == null)
{
continue;
}
string strInterWndName = objInterWndName.ToString();
string strInterBtnPath = objInterBtnPath.ToString();
if (!CheckSingle(strInterWndName, strInterBtnPath))
{
Debug.Log(strInterWndName + "中的节点" + strInterBtnPath + " 请修改" + strInterWndName + " 或者配置文件");
}
}
}
}
其实这个问题很基础,偶尔会忘记,但却是代码中常用到的,用得多了对性能影响自然也就大了。
2.字符串解析优化
调用统一接口加载Lua文件的时间有些长,而且对于检查配置这一功能来说,这样的操作有些浪费,所以新的需求是用读文本的形式读取Lua文件。
小科普
所有文件本质上都是二进制文件,一堆0和1,文件本身并没有意义,关键看如何解读。
于是把Lua文件当做纯文本来解析,要解决的问题主要有两个:
1.去除备注以及文本中LuaTable以外的元素
2.将LuaTable解析出来
这其中肯定要识别关键字,例如
public static Hashtable ReadLuaTable(string[] content)
{
Hashtable table = new Hashtable();
#if LOG_PARSE_TIME
long currentTime = System.DateTime.Now.Ticks;
#endif
string strFinal = "";
bool isFunction = false;
for (int i = 0; i < content.Length; i++)
{
string tempStr = content[i];
#region 检查是否为注释行
if (tempStr.Contains("--"))
{
if (tempStr.IndexOf("--") - 1 > 0)
{
tempStr = tempStr.Substring(0, tempStr.IndexOf("--"));
}
else
{
tempStr = "";
}
}
if (tempStr.Replace(" ", "").Replace("\t", "") == "")
{
tempStr = "";
}
#endregion
#region 检查是否为函数,忽略他
if (tempStr.Contains("function"))
{
isFunction = true;
tempStr = "";
}
else if (tempStr.Contains("end"))
{
if (tempStr.IndexOf("end") == 0)
{
isFunction = false;
tempStr = "";
}
}
if (isFunction)
{
tempStr = "";
}
#endregion
content[i] = tempStr;
}
strFinal = string.Join("", content);
dicMatchLength = new Hashtable();
#if LOG_PARSE_TIME
long elapsed = System.DateTime.Now.Ticks - currentTime;
Debug.Log("预处理lua文件消耗时长 : " + elapsed);
currentTime = System.DateTime.Now.Ticks;
#endif
table = ReadLuaTable(strFinal.Replace("\\\\", "\\"), 0) as Hashtable;
#if LOG_PARSE_TIME
elapsed = System.DateTime.Now.Ticks - currentTime;
Debug.Log("解析lua文件消耗时长 : " + elapsed);
#endif
return table;
}
我一开始的做法显得比较笨拙,首先读取文件,采用了File.ReadAllLines()来读取文件,这里会把二进制文件逐行读成string,对每行string查找关键字,以检测是否为注释或函数,这些地方会影响到之后解析LuaTable。解析LuaTable需要逐字解析,所以最后我用string.Join()将数组重新变为单个string,不难察觉,生成这么多临时的string并不合适,这将会占用存储空间,且效率不高。
同时有几个小技巧,可以进一步优化代码。
1.string.IndexOf()与string.Contains()这个函数能够达到相同效果,当string.IndexOf(‘,’)得到的值小于0时则当前string不存在’,’。
2.content.Replace();content.Substring();等函数将会生成临时string以返回结果,尽量少用。
3.当有大量字符串操作时StringBuilder.Append();StringBuilder.Remove();要比直接用几个string相加,或者substring要快得多,StringBuilder已经事先申请好内存,所以在频繁操作的过程中省下了申请内存的时间。
嗯,这几点我当然本来也没注意,又是主程一秒点破,于是之后重构了代码。
public static Hashtable ReadLuaTable2(System.IO.StreamReader file)
{
string totalFile = file.ReadToEnd();
int nFileLength = totalFile.Length, nLineStartAt = 0, nLineEndAt = 0, nCount = 0;
Hashtable table = new Hashtable();
System.Text.StringBuilder strFinal = new System.Text.StringBuilder(1 << 19);
bool isFunction = false;
#if LOG_PARSE_TIME
long currentTime = System.DateTime.Now.Ticks;
#endif
for (int i = 0; i < nFileLength; nLineStartAt = nLineEndAt + 1, i++)
{
//TODO: \r
nLineEndAt = totalFile.IndexOf('\n', nLineStartAt);
if (nLineEndAt < 0)
break;
int nSubStringLength = nCount = nLineEndAt - nLineStartAt;
#region 检查是否为注释行
if (!isFunction)
{
int indexOfComment = totalFile.IndexOf("--", nLineStartAt, nCount);
if (indexOfComment > 0)
{
nSubStringLength = indexOfComment - nLineStartAt;
}
else if (indexOfComment == 0)
{
continue;
}
bool bEmptyLine = true;
for (int j = nLineStartAt, jMax = nLineStartAt + nSubStringLength; j < jMax; j++)
{
char character = totalFile[j];
if (character != ' ' && character != '\t')
{
bEmptyLine = false;
break;
}
}
if (bEmptyLine)
{
continue;
}
}
#endregion
#region 检查是否为函数,忽略他
if (!isFunction)
{
if (totalFile[nLineStartAt] == 'f' && totalFile[nLineStartAt + 1] == 'u' && totalFile[nLineStartAt + 2] == 'n' && totalFile[nLineStartAt + 3] == 'c' &&
totalFile[nLineStartAt + 4] == 't' && totalFile[nLineStartAt + 5] == 'i' && totalFile[nLineStartAt + 6] == 'o' && totalFile[nLineStartAt + 7] == 'n')
{
isFunction = true;
continue;
}
}
else
{
if (totalFile[nLineStartAt] == 'e' && totalFile[nLineStartAt + 1] == 'n' && totalFile[nLineStartAt + 2] == 'd')
{
isFunction = false;
continue;
}
}
if (isFunction)
{
continue;
}
#endregion
strFinal.Append(totalFile, nLineStartAt, nSubStringLength);
}
#if LOG_PARSE_TIME
long elapsed = System.DateTime.Now.Ticks - currentTime;
Debug.Log("预处理lua文件消耗时长 : " + elapsed);
#endif
string strFinalFinal = strFinal.ToString().Replace("\\\\", "\\");
dicMatchLength = new Hashtable();
#if LOG_PARSE_TIME
currentTime = System.DateTime.Now.Ticks;
#endif
table = ReadLuaTable(strFinalFinal, 0) as Hashtable;
#if LOG_PARSE_TIME
elapsed = System.DateTime.Now.Ticks - currentTime;
Debug.Log("解析lua文件消耗时长 : " + elapsed);
#endif
return table;
}
前后对比了时间,后者更稳定且一直略快于前者,第一次快20ms,多运行几次后,前者时间越来越接近后者,应该是unity内部对string的操作进行了什么神秘的优化,但总体来说,后者策略是要优于前者的,当然也视各位的具体情况而定。
3.资源重复加载
动态加载资源,无论是游戏本身还是类似我在写的检查工具都经常用到。
UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(path);
这次我吸取了上次的教训,没有重复调用这个函数,而是赋给了局部变量,之后的检查过程中,调用局部变量无需重新加载。
这么做仍然有可以优化的地方,拿我做的工具举例,每当有prefab修改都会进行一次检查,每次检查都会需要加载许多资源,但其实,常常有一些资源并没有发生改变,这里可以做一个“池”,存放我们需要的资源,避免重复加载的现象。
GameObject GetAssets(string path)
{
if(dicAssets.ContainsKey(path))
{
return dicAssets[path];
}
else
{
GameObject temp = UnityEditor.AssetDatabase.LoadAssetAtPath<GameObject>(path);
dicAssets.Add(path, temp);
return temp;
}
}
封装这么一个方法就可以在第一次检查之后省下加载资源的时间。
好的不喜欢说自己是萌新的萌新分享结束了,有错误的地方大家纠正,想要讨论问题也欢迎发邮件到lguanyuan@qq.com,之前放qq号上去发现这样qq会加了许多平时不联系的人,还是邮箱来得舒服,哈哈。