惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The Cloudflare Blog
U
Unit 42
F
Fortinet All Blogs
雷峰网
雷峰网
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
月光博客
月光博客
Y
Y Combinator Blog
罗磊的独立博客
V
Visual Studio Blog
大猫的无限游戏
大猫的无限游戏
J
Java Code Geeks
量子位
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
爱范儿
爱范儿
B
Blog RSS Feed
aimingoo的专栏
aimingoo的专栏
有赞技术团队
有赞技术团队
T
Tailwind CSS Blog
Microsoft Security Blog
Microsoft Security Blog
L
LangChain Blog
I
InfoQ
博客园 - 叶小钗
博客园 - 聂微东
Last Week in AI
Last Week in AI

博客园 - 李现民

聚人才,戏天下,以为本 一些强大的Shell命令(转) Bash Shortcuts(转) Unity3d中BlinnPhong光照模型注解 扩展方法之二分查找 各种箭头的unicode编码值 深度排序与alpha混合 SendMessage使用技巧 Unity3d基本优化策划 Destroy()方法的延时特性 如何使GameObject在超出Camera视野后自动销毁 避免在代码中直接使用delete关键字 J2ME中的基础碰撞检测算法 告别cpp&h:hpp文件编写心得(转) 平台无关性多线程库-C++ Boost Thread 编程指南(转) BitBlt()双缓冲解决图象闪烁(转) 后缀数组(转) 2006百度笔试题 百度笔试题
一次非典型的程序优化
李现民 · 2012-07-06 · via 博客园 - 李现民

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

}