一次非典型的程序优化


版本: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'};

}






posted @ 2012-07-06 12:21  李现民  阅读(1253)  评论(0编辑  收藏  举报