版本:0.1
最后修改:2012-06-12
撰写:李现民
涉及内容:unity3d, c#, string
前两日,有同事设计游戏连击特效的功能。因为这个功能在游戏环节中会调用的非常频繁,因此顺手点了开来,发现代码设计尚有优化的余地,于是便有了本次尝试。事后回想开来,发现涉及的东西还颇多,便想着记下来以飨后来者。
这是一份unity3d脚本程序,原始脚本文件MBStarCombo.cs代码如下:
using UnityEngine;
using System;
class MBStarCombo : MonoBehaviour
{
void Start()
{
_combo = transform.Find("Sprite (U-Combo)").gameObject;
_num1 = transform.Find("sprite_num1").gameObject;
_num2 = transform.Find("sprite_num2").gameObject;
_num3 = transform.Find("sprite_num3").gameObject;
_num4 = transform.Find("sprite_num4").gameObject;
Instance = this;
}
public void UpdateComboNum(int curCombo)
{
if(curCombo > 9999)
curCombo = 9999;
if(0 == curCombo)
{
_combo.SetActiveRecursively(false);
_num1.SetActiveRecursively(false);
_num2.SetActiveRecursively(false);
_num3.SetActiveRecursively(false);
_num4.SetActiveRecursively(false);
}
else if(curCombo > 0 && curCombo < 10)
{
_combo.SetActiveRecursively(true);
_num1.SetActiveRecursively(true);
_num2.SetActiveRecursively(false);
_num3.SetActiveRecursively(false);
_num4.SetActiveRecursively(false);
_num1.GetComponent<UISprite>().spriteName = "U-" + curCombo.ToString();
}
else if(curCombo >= 10 && curCombo < 100)
{
_combo.SetActiveRecursively(true);
_num1.SetActiveRecursively(true);
_num2.SetActiveRecursively(true);
_num3.SetActiveRecursively(false);
_num4.SetActiveRecursively(false);
int num1 = curCombo / 10;
int num2 = curCombo % 10;
_num1.GetComponent<UISprite>().spriteName = "U-" + num1.ToString();
_num2.GetComponent<UISprite>().spriteName = "U-" + num2.ToString();
}
else if(curCombo >= 100 && curCombo < 1000)
{
_combo.SetActiveRecursively(true);
_num1.SetActiveRecursively(true);
_num2.SetActiveRecursively(true);
_num3.SetActiveRecursively(true);
_num4.SetActiveRecursively(false);
int num1 = curCombo / 100;
int num2 = (curCombo - num1 * 100) / 10;
int num3 = curCombo % 10;
_num1.GetComponent<UISprite>().spriteName = "U-" + num1.ToString();
_num2.GetComponent<UISprite>().spriteName = "U-" + num2.ToString();
_num3.GetComponent<UISprite>().spriteName = "U-" + num3.ToString();
}
else if(curCombo >= 1000 && curCombo < 10000)
{
_combo.SetActiveRecursively(true);
_num1.SetActiveRecursively(true);
_num2.SetActiveRecursively(true);
_num3.SetActiveRecursively(true);
_num4.SetActiveRecursively(true);
int num1 = curCombo / 1000;
int num2 = (curCombo - num1 * 1000) / 100;
int num3 = (curCombo - num1 * 1000 - num2 * 100) / 10;
int num4 = curCombo % 10;
_num1.GetComponent<UISprite>().spriteName = "U-" + num1.ToString();
_num2.GetComponent<UISprite>().spriteName = "U-" + num2.ToString();
_num3.GetComponent<UISprite>().spriteName = "U-" + num3.ToString();
_num4.GetComponent<UISprite>().spriteName = "U-" + num3.ToString();
}
}
internal static MBStarCombo Instance { get; set; }
private GameObject _combo;
private GameObject _num1;
private GameObject _num2;
private GameObject _num3;
private GameObject _num4;
}
其中,Start()方法只会调用一次,而UpdateComboNum()将在游戏过程中被反复调用,因此后者将是代码优化的重点。我们将新代码保存至MBStarCombo2.cs文件中,同时进行调用,并使用Profiler比较改进的结果。
程序逻辑比较简单清晰:根据当前传入的分数,最大限制到9999,分情况将分数的各个数字取出来,并设置对应UISprite的纹理项为对应的分数数值。代码中用到了NGUI,因此只要简单的按名称设置就可以更改UISprite的纹理。
当前目所能及的地方包括:SetActiveRecursively()与GetComponent()。
首先是SetActiveRecursively(),这是一个开销比较大的调用,它会循环遍历gameObject的所有子对象并设置它们的active状态,看不到源代码实现,但估计是深度优先之类的搜索遍历。其实,在本次代码中,我们只需要简单的设置UISprite组件的enabled属性就可以实现隐藏或显示该组件的功能。
其次是GetComponent()。原始代码中,程序保存了_num1到_num4共4个gameObject的对象引用。程序频繁的调用了GetComponent()方法去获取其UISprite组件,这其实是一个开销比较大的操作。Unity3d开发文档的性能优化指南中有相关介绍,对频繁使用的组件应该缓存其引用。这不仅仅是指GetComponent()调用,包括transform, gameObject等内置变量如果需要频繁使用的话,同样应该通过定义一个私有变量缓存其引用,因为它们本质其实是类似的。
另外,对于if()调用中边界条件的判断,curCombo > 0,curCombo > =10等其实是没有必要的,可以直接去除之。
修改完成后的代码如下:
class MBStarCombo2 : MonoBehaviour
{
void Start()
{
_combo = transform.Find("Sprite (U-Combo)").gameObject.GetComponent<UISprite>();
_num1 = transform.Find("sprite_num1").gameObject.GetComponent<UISprite>();
_num2 = transform.Find("sprite_num2").gameObject.GetComponent<UISprite>();
_num3 = transform.Find("sprite_num3").gameObject.GetComponent<UISprite>();
_num4 = transform.Find("sprite_num4").gameObject.GetComponent<UISprite>();
Instance = this;
}
public void UpdateComboNum(int curCombo)
{
if(curCombo > 9999)
curCombo = 9999;
if(0 == curCombo)
{
_combo.enabled = false;
_num1.enabled = false;
_num2.enabled = false;
_num3.enabled = false;
_num4.enabled = false;
}
else if(curCombo < 10)
{
_combo.enabled = true;
_num1.enabled = true;
_num2.enabled = false;
_num3.enabled = false;
_num4.enabled = false;
_num1.spriteName = "U-" + curCombo.ToString();
}
else if(curCombo < 100)
{
_combo.enabled = true;
_num1.enabled = true;
_num2.enabled = true;
_num3.enabled = false;
_num4.enabled = false;
int num1 = curCombo / 10;
int num2 = curCombo % 10;
_num1.spriteName = "U-" + num1.ToString();
_num2.spriteName = "U-" + num2.ToString();
}
else if(curCombo < 1000)
{
_combo.enabled = true;
_num1.enabled = true;
_num2.enabled = true;
_num3.enabled = true;
_num4.enabled = false;
int num1 = curCombo / 100;
int num2 = (curCombo - num1 * 100) / 10;
int num3 = curCombo % 10;
_num1.spriteName = "U-" + num1.ToString();
_num2.spriteName = "U-" + num2.ToString();
_num3.spriteName = "U-" + num3.ToString();
}
else if(curCombo < 10000)
{
_combo.enabled = true;
_num1.enabled = true;
_num2.enabled = true;
_num3.enabled = true;
_num4.enabled = true;
int num1 = curCombo / 1000;
int num2 = (curCombo - num1 * 1000) / 100;
int num3 = (curCombo - num1 * 1000 - num2 * 100) / 10;
int num4 = curCombo % 10;
_num1.spriteName = "U-" + num1.ToString();
_num2.spriteName = "U-" + num2.ToString();
_num3.spriteName = "U-" + num3.ToString();
_num4.spriteName = "U-" + num3.ToString();
}
}
internal static MBStarCombo2 Instance { get; set; }
private UISprite _combo;
private UISprite _num1;
private UISprite _num2;
private UISprite _num3;
private UISprite _num4;
}
通过Profiler观察,可以发现优化后的代码占用CPU大概少一个百分点,如图:
优化力度没有我想象中的那么大 -______-
进一步观察,我们发现如下代码:
int num1 = curCombo / 1000;
int num2 = (curCombo - num1 * 1000) / 100;
int num3 = (curCombo - num1 * 1000 - num2 * 100) / 10;
int num4 = curCombo % 10;
_num1.spriteName = "U-" + num1.ToString();
_num2.spriteName = "U-" + num2.ToString();
_num3.spriteName = "U-" + num3.ToString();
_num4.spriteName = "U-" + num3.ToString();
其实就是获取了连击分数的各位数值,如果能想办法把它们去掉,至少在代码复杂性上会减少很多。于是,我想到了其实c#的string其实是有索引器的,于是它们可以改写为:
var text = curCombo.ToString();
_num1.spriteName = "U-" + text[0];
_num2.spriteName = "U-" + text[1];
_num3.spriteName = "U-" + text[2];
_num4.spriteName = "U-" + text[3];
进一步的,很容易发现,修正_numX的个数与text的长度其实密切相关,只要将_numX改写为一个数组,可以很容易的统一提取成一个循环,如下:
if (curCombo > 0)
{
var text = curCombo.ToString();
var length = text.Length;
for (int i = 0; i < length; ++i)
{
_nums[i].spriteName = "U-" + text[i];
}
}
这样,MBStarCombo2.cs的所有代码还包括:
class MBStarCombo2 : MonoBehaviour
{
void Start()
{
_combo = transform.Find("Sprite (U-Combo)").gameObject.GetComponent<UISprite>();
_num1 = transform.Find("sprite_num1").gameObject.GetComponent<UISprite>();
_num2 = transform.Find("sprite_num2").gameObject.GetComponent<UISprite>();
_num3 = transform.Find("sprite_num3").gameObject.GetComponent<UISprite>();
_num4 = transform.Find("sprite_num4").gameObject.GetComponent<UISprite>();
_nums = new UISprite[] { _num1, _num2, _num3, _num4};
Instance = this;
}
public void UpdateComboNum(int curCombo)
{
if (curCombo > 9999)
{
curCombo = 9999;
}
if(0 == curCombo)
{
_combo.enabled = false;
_num1.enabled = false;
_num2.enabled = false;
_num3.enabled = false;
_num4.enabled = false;
}
else if(curCombo < 10)
{
_combo.enabled = true;
_num1.enabled = true;
_num2.enabled = false;
_num3.enabled = false;
_num4.enabled = false;
}
else if(curCombo < 100)
{
_combo.enabled = true;
_num1.enabled = true;
_num2.enabled = true;
_num3.enabled = false;
_num4.enabled = false;
}
else if(curCombo < 1000)
{
_combo.enabled = true;
_num1.enabled = true;
_num2.enabled = true;
_num3.enabled = true;
_num4.enabled = false;
}
else if(curCombo < 10000)
{
_combo.enabled = true;
_num1.enabled = true;
_num2.enabled = true;
_num3.enabled = true;
_num4.enabled = true;
}
if (curCombo > 0)
{
var text = curCombo.ToString();
var length = text.Length;
for (int i = 0; i < length; ++i)
{
_nums[i].spriteName = "U-" + text[i];
}
}
}
internal static MBStarCombo2 Instance { get; set; }
private UISprite _combo;
private UISprite _num1;
private UISprite _num2;
private UISprite _num3;
private UISprite _num4;
private UISprite[] _nums;
}
此时,_combo与_numX的设置规律已经比较明显了,可以统一按text的长度修改,再稍微整理一下,去除一些冗余代码可以得到如下结果:
class MBStarCombo2 : MonoBehaviour
{
void Start()
{
_combo = GetSprite("Sprite (U-Combo)");
_nums = new UISprite[] { GetSprite("sprite_num1"), GetSprite("sprite_num2"), GetSprite("sprite_num3"), GetSprite("sprite_num4") };
Instance = this;
}
UISprite GetSprite(string name)
{
return transform.Find(name).gameObject.GetComponent<UISprite>();
}
public void UpdateComboNum(int curCombo)
{
if (curCombo > 9999)
{
curCombo = 9999;
}
if(0 == curCombo)
{
_combo.enabled = false;
Array.Clear(_nums, 0, _nums.Length);
}
else
{
var text = curCombo.ToString();
var length = text.Length;
_combo.enabled = true;
_nums[0].enabled = length >= 1;
_nums[1].enabled = length >= 2;
_nums[2].enabled = length >= 3;
_nums[3].enabled = length >= 4;
for (int i = 0; i < length; ++i)
{
_nums[i].spriteName = "U-" + text[i];
}
}
}
internal static MBStarCombo2 Instance { get; set; }
private UISprite _combo;
private UISprite[] _nums;
}
相对于原始代码,这简洁了不少,并且相对于第一版的优化效率也略有提升:
进一步对Profiler进行观察,可以发现String.Concat()与Int32.ToString()其实占了大头,如果能够把它们优化去掉,那世界一定会变得美好的多。因此考虑设计一个字符数组,直接对字符数组的数据进行修改而不是做Concat()操作:
for (int i = 0; i < length; ++i)
{
_spriteName[2] = text[i];
_nums[i].spriteName = new string(_spriteName);
}
从Profiler的结果来看,新代码的CPU占用率已经不足原始代码的一半:
然而,正如您所看到的那样,最大的优化竟然来自于对字符串操作的优化,这实在另人有些意外。不过仔细想想也有些道理,前一版中的”U-”+num.ToString()至少涉及到至少2次内存分配:一次给num.ToString(),一次给连接后的结果字符串。而使用字符串数组则仅仅用到一次内存copy,显然会快一些。
优化后的通篇代码如下:
class MBStarCombo : MonoBehaviour
{
void Start()
{
_combo = _GetSprite("Sprite (U-Combo)");
_nums = new UISprite[] { _GetSprite("sprite_num1"), _GetSprite("sprite_num2"), _GetSprite("sprite_num3"), _GetSprite("sprite_num4") };
Instance= this;
}
UISprite _GetSprite(string name)
{
return transform.Find(name).gameObject.GetComponent<UISprite>();
}
public void UpdateComboNum(int curCombo)
{
if (curCombo > 9999)
{
curCombo = 9999;
}
var isComboEnabled = curCombo > 0;
_combo.enabled = isComboEnabled;
if(isComboEnabled)
{
var text = curCombo.ToString();
var length = text.Length;
for(int i= 0; i < _nums.Length; ++i)
{
_nums[i].enabled = length > i;
}
for (int i = 0; i < length; ++i)
{
_spriteName[2] = text[i];
_nums[i].spriteName = new string(_spriteName);
}
}
else
{
Array.ForEach(_nums, num=>num.enabled= false);
}
}
internal static MBStarCombo Instance { get; set; }
private UISprite _combo;
private UISprite[] _nums;
private char[] _spriteName = new char[]{'U', '-', '\0'};
}