首页 > 代码库 > unity3d优化总结篇

unity3d优化总结篇

此总结由自己经验及网上收集整理优化内容 包括:
1.代码方面;
2.函数使用方面;
3.ngui注意方面;
4.数学运算方面;
5.内存方面;
6.垃圾回收方面 等等...

 

第一部分:

1. 尽量避免每帧处理,可以每隔几帧处理一次。比如:
1 function Update() { DoSomeThing(); }2 //可改为每5帧处理一次:3 function Update() { if(Time.frameCount % 5 == 0) { DoSomeThing(); } }
 
2. 定时重复处理用InvokeRepeating 函数实现
比如,启动0.5秒后每隔1秒执行一次 DoSomeThing 函数:
1 function Start() { InvokeRepeating("DoSomeThing", 0.5, 1.0); }2 CancelInvoke("你调用的方法"); // 停止InvokeRepeating
3. 优化 Update,FixedUpdate, LateUpdate 等每帧处理的函数,函数里面的变量尽量在头部声明。比如:
function Update() { var pos: Vector3 = transform.position; }//可改为private var pos: Vector3; function Update(){ pos = transform.position; }
 
4. 主动回收垃圾  给某个 GameObject 绑上以下的代码:
1 function Update() { if(Time.frameCount % 50 == 0) { System.GC.Collect(); } }
 
5. 运行时尽量减少 Tris 和 Draw Calls

预览的时候,可点开 Stats,查看图形渲染的开销情况。特别注意 Tris 和 Draw Calls 这两个参数。一般来说,要做到:Tris 保持在 7.5k 以下,Draw Calls 保持在 35 以下。

 
6. 压缩 Mesh
导入 3D 模型之后,在不影响显示效果的前提下,最好打开 Mesh Compression。Off, Low, Medium, High 这几个选项,可酌情选取。对于单个Mesh最好使用一个材质。

 

7. 避免大量使用 unity 自带的 Sphere 等内建 Mesh
Unity 内建的 Mesh,多边形的数量比较大,如果物体不要求特别圆滑,可导入其他的简单3D模型代替。
 
8. 优化数学计算
尽量避免使用float,而使用int,特别是在手机游戏中,尽量少用复杂的数学函数,比如sin,cos等函数。改除法/为乘法,例如:使用x*0.5f而不是 x/2.0f 。

9.如果你做了一个图集是1024X1024的。此时你的界面上只用了图集中的一张很小的图,那么很抱歉1024X1024这张大图都需要载入你的内存里面,1024就是4M的内存,如果你做了10个1024的图集,你的界面上刚好都只用了每个图集里面的一张小图,那么再次抱歉你的内存直接飙40M。意思是任何一个4096的图片,不管是图集还是texture,他都占用4*4=16M?
 
第二部分:
1、在使用数组或ArrayList对象时应当注意
1 length=myArray.Length;  2 for(int i=0;i<length;i++)  {} 3 //避免4 for(int i=0;i<myArray.Length;i++)  {}  

2、少使用临时变量,特别是在Update OnGUI等实时调用的函数中。

 1 void Update()   2 {   3    Vector3 pos;   4    pos=transform.position;   5 }   6  7 //可以改为: 8 private Vector3 pos;   9 void Update()  10 {  11    pos=transform.position;  12 }

3、如果可能,将GameObject上不必要的脚本disable掉。
如果你有一个大的场景在你的游戏中,并且敌方的位置在数千米意外,
这时你可以disable你的敌方AI脚本直到它们接近摄像机为止。
一个好的途径来开启或关闭GameObject是使用SetActiveRecursively(false),并且球形或盒型碰撞器设为trigger。

4、删除空的Update方法。
当通过Assets目录创建新的脚本时,脚本里会包括一个Update方法,当你不使用时删除它。

5、引用一个游戏对象的最合乎逻辑的组件。
有人可能会这样写someGameObject.transform,gameObject.rigidbody.transform.gameObject.rigidbody.transform,但是这样做了一些不必要的工作,
你可以在最开始的地方引用它,像这样:

1 privateTransform myTrans;2 void Start()3 {4     myTrans=transform;5 6 }

 

6、协同是一个好方法。
可以使用协同程序来代替不必每帧都执行的方法。(还有InvokeRepeating方法也是一个好的取代Update的方法)。

7、尽可能不要再Update或FixedUpdate中使用搜索方法(例如GameObject.Find()),你可以像前面那样在Start方法里获得它。

8、不要使用SendMessage之类的方法,他比直接调用方法慢了100倍,你可以直接调用或通过C#的委托来实现。

9、使用javascript或Boo语言时,你最好确定变量的类型,不要使用动态类型,这样会降低效率,
你可以在脚本开头使用#pragmastrict 来检查,这样当你编译你的游戏时就不会出现莫名其妙的错误了。

 

第三部分:

1、顶点性能     
一般来说,如果您想在iPhone 3GS或更新的设备上每帧渲染不超过40,000可见点,
那么对于一些配备 MBX GPU的旧设备(比如,原始的 iPhone,如 iPhone 3g和 iPod Touch第1和第2代)来说,你应该保证每帧的渲染顶点在10000以下。

2、光照性能     
像素的动态光照将对每个受影响的像素增加显著的计算开销,并可能导致物体会被渲染多次。
为了避免这种情况的发生,您应该避免对于任何单个物体都使用多个像素光照,并尽可能地使用方向光。
需要注意的是像素光源是一个渲染模式(Render Mode)设置为重要(Important)的光源。
像素的动态光照将对顶点变换增加显著的开销。所以,应该尽量避免任何给定的物体被多个光源同时照亮的情况。
对于静态物体,采用烘焙光照方法则是更为有效的方法。

3、角色     
每个角色尽量使用一个Skinned Mesh Renderer,这是因为当角色仅有一个 Skinned Mesh Renderer 时,
Unity 会使用可见性裁剪和包围体更新的方法来优化角色的运动,而这种优化只有在角色仅含有一个 Skinned Mesh Renderer时才会启动。
角色的面数一般不要超过1500,骨骼数量少于30就好,角色Material数量一般1~2个为最佳。

4、静态物体     
对于静态物体定点数要求少于500,UV的取值范围不要超过(0,1)区间,这对于纹理的拼合优化很有帮助。
不要在静态物体上附加Animation组件,虽然加了对结果没什么影响,但是会增加CPU开销。

5、摄像机     
将远平面设置成合适的距离,远平面过大会将一些不必要的物体加入渲染,降低效率。
另外我们可以根据不同的物体来设置摄像机的远裁剪平面。Unity 提供了可以根据不同的 layer 来设置不同的 view distance ,
所以我们可以实现将物体进行分层,大物体层设置的可视距离大些,而小物体层可以设置地小些,
另外,一些开销比较大的实体(如粒子系统)可以设置得更小些等等。

6、DrawCall      
尽可能地减少 Drawcall 的数量。 IOS 设备上建议不超过 100 。
减少的方法主要有如下几种: Frustum Culling ,Occlusion Culling , Texture Packing 。 Frustum Culling 是 Unity 内建的,我们需要做的就是寻求一个合适的远裁剪平面;
Occlusion Culling ,遮挡剔除, Unity 内嵌了 Umbra ,一个非常好 OC 库。
但 Occlusion Culling 也并不是放之四海而皆准的,有时候进行 OC 反而比不进行还要慢,
建议在 OC 之前先确定自己的场景是否适合利用 OC 来优化; Texture Packing ,或者叫 Texture Atlasing ,
是将同种 shader 的纹理进行拼合,根据 Unity 的 static batching 的特性来减少 draw call 。
建议使用,但也有弊端,那就是一定要将场景中距离相近的实体纹理进行拼合,否则,拼合后很可能会增加每帧渲染所需的纹理大小,
加大内存带宽的负担。这也就是为什么会出现“ DrawCall 降了,渲染速度也变慢了”的原因。

 

第四部分:

1.粒子系统运行在iPhone上时很慢,怎么办?
答:iPhone拥有相对较低的fillrate 。
如果您的粒子效果覆盖大部分的屏幕,而且是multiple layers的,这样即使最简单的shader,也能让iPhone傻眼。
我们建议把您的粒子效果baking成纹理序列图。
然后在运行时可以使用1-2个粒子,通过动画纹理来显示它们。这种方式可以取得很好的效果,以最小的代价。 

 

第五部分:

1.操作transform.localPosition的时候请小心
移动GameObject是非常平常的一件事情,以下代码看起来很简单:

1 transform.localPosition += new Vector3 ( 10.0f * Time.deltaTime, 0.0f, 0.0f );

但是小心了,假设上面这个GameObject有一个parent, 并且这个parent GameObject的localScale是(2.0f,2.0f,2.0f)。你的GameObject将会移动20.0个单位/秒。
因为该 GameObject的world position等于:

1 Vector3 offset = new Vector3( my.localPosition.x * parent.lossyScale.x, my.localPosition.y * parent.lossyScale.y, my.localPosition.z * parent.lossyScale.z );2 Vector3 worldPosition = parent.position + parent.rotation * offset;

换句话说,上面这种直接操作localPosition的方式是在没有考虑scale计算的时候进行的,为了解决这个问题,unity3d提供了Translate函数,
所以正确的做法应该是: 

1 transform.Translate ( 10.0f * Time.deltaTime, 0.0f, 0.0f );

曝出在Inspector的变量同样的也能被Animation View Editor所使用
有时候我们会想用Unity3D自带的Animation View Editor来做一些简单的动画操作。而Animation Editor不仅可以操作Unity3D自身的component,
还可以操作我们自定义的MonoBehavior中的各个Property。所以加入 你有个float值需要用曲线操作,你可以简单的将它曝出到成可以serialize的类型,如:

1 public float foobar = 1.0f;

这样,这个变量不仅会在Inspector中出现,还可以在animation view中进行操作,生成AnimationClip供我们通过AnimationComponent调用。
范例:

 1 public class TestCurve : MonoBehaviour 2 { 3 public float foobar = 0.0f; 4 IEnumerator Start () 5 { 6 yield return new WaitForSeconds (2.0f); 7 animation.Play("foobar_op");  8 InvokeRepeating ( "LogFoobar", 0.0f, 0.2f );  9 yield return new WaitForSeconds (animation["foobar_op"].length);10 CancelInvoke ("LogFoobar");11 } 12 13 void LogFoobar ()14 { 15 Debug.Log("foobar = " + foobar);16  }17 }

 

2.GetComopnent<T> 可以取父类类型
Unity3D 允许我们对MonoBehavior做派生,所以你可能有以下代码:

1 public class foo : MonoBehaviour { ...} public class bar : foo { ...}

假设我们现在有A,B两个GameObject, A包含foo, B包含bar, 当我们写

1 foo comp1 = A.GetComponent<foo>();bar comp2 = B.GetComponent<bar>();

可以看到comp1, comp2都得到了应得的Component。那如果我们对B的操作改成:

1 foo comp2 = B.GetComponent<foo>();

答案是comp2还是会返回bar Component并且转换为foo类型。你同样可以用向下转换得到有效变量:

1 bar comp2_bar = comp2 as bar;

合理利用GetComponent<base_type>()可以让我们设计Component的时候耦合性更低。

 

3.Invoke, yield 等函数会受 Time.timeScale 影响
Unity3D提供了一个十分方便的调节时间的函数Time.timeScale。对于初次使用Unity3D的使用者,
会误导性的认为Time.timeScale同样可以适用于游戏中的暂停(Pause)和开始(Resume)。
所以很多人有习惯写:

Time.timeScale = 0.0f

对于游戏的暂停/开始,是游戏系统设计的一部分,而Time.timeScale不不是用于这个部分的操作。
正确的做法应该是搜集需要暂停的脚本或 GameObject,
通过设置他们的enabled = false 来停止他们的脚本活动或者通过特定函数来设置这些物件暂停时需要关闭那些操作。


Time.timeScale 更多的是用于游戏中慢镜头的播放等操作,在服务器端主导的游戏中更应该避免此类操作。
值得一提的是,Unity3D的许多时间相关的函数都和 timeScale挂钩,而timeScale = 0.0f将使这些函数或动画处于完全停止的状态,这也是为什么它不适合做暂停操作的主要原因。

这里列出受timeScale影响的一些主要函数和Component:

1 MonoBehaviour.Invoke(…)2 MonoBehaviour.InvokeRepeating(…)3 yield WaitForSeconds(…)4 GameObject.Destroy(…)5 Animation Component6 Time.time, Time.deltaTime7

 

4.Coroutine 和 IEnumerator的关系
初写Unity3D C#脚本的时候,我们经常会犯的错误是调用Coroutine函数忘记使用StartCoroutine的方式。如:

TestCoroutine.csIEnumerator CoLog () { yield return new WaitForSeconds (2.0f); Debug.Log("hello foobar");}//当我们用以下代码去调用上述函数:TestCoroutine testCo = GetComponent<TestCoroutine>();testCo.CoLog ();testCo.StartCoroutine ( "CoLog" );

那么testCo.CoLog()的调用将不会起任何作用。

 

5.StartCoroutine, InvokeRepeating 和其调用者关联
通常我们只在一份GameObject中去调用StartCoroutine或者InvokeRepeating,
我们写:

1 StartCoroutine ( Foobar() );InvokeRepeating ( "Foobar", 0.0f, 0.1f );2 3 //所以如果这个GameObject被disable或者destroy了,这些coroutine和invokes将会被取消。就好比我们手动调用:4 StopAllCoroutines ();CancelInvoke ();

这看上去很美妙,对于AI来说,这就像告诉一个NPC你已经死了,你自己的那些小动作就都听下来吧。

但是注意了,假如这样的代码用在了一个Manager类型的控制AI上,他有可能去控制其他的AI, 也有可能通过Invoke, Coroutine去做一些微线程的操作,这个时候就要明确StartCoroutine或者InvokeRepeating的调用者的设计。讨论之前我 们先要理解,StartCoroutine或InvokeRepeating的调用会在该MonoBehavior中开启一份Thread State, 并将需要操作的函数,变量以及计时器放入这份Stack中通过并在引擎每帧Update的最后,Renderer渲染之前统一做处理。所以如果这个 MonoBehavior被Destroy了,那么这份Thread State也就随之消失,那么所有他存储的调用也就失效了。

如果有两份GameObject A和B, 他们互相知道对方,假如A中通过StartCoroutine或InvokeRepeating去调用B的函数从而控制B,这个时候Thread State是存放在A里,当A被disable或者destroy了,这些可能需要一段时间的控制函数也就失效了,这个时候B明明还没死,也不会动了。更 好的做法是让在A的函数中通过B.StartCoroutine ( … ) 让这份Thread State存放于B中。

 1 // class TestCortouine 2  3 public class TestCoroutine : MonoBehaviour 4 { 5  6 public IEnumerator CoLog ( string _name )  7 { 8 Debug.Log(_name + " hello foobar 01"); 9 yield return new WaitForSeconds (2.0f); 10 Debug.Log(_name + " hello foobar 02"); }} 11 // component attached on GameObject A12 13 public class A: MonoBehaviour 14 { 15 16 public GameObject B;  void Start ()17 { 18 TestCoroutine compB = B.GetComponent<TestCoroutine>();  19 // GOOD, thread state in B 20 // same as: comp21 B.StartCoroutine ( "CoLog", "B" ); 22 compB.StartCoroutine ( compB.CoLog("B") );23 // BAD, thread state in A 24 StartCoroutine ( compB.CoLog("A") ); 25 Debug.Log("Bye bye A, we‘ll miss you"); 26 Destroy(gameObject); 27 // T_T I don‘t want to die... 28 }29 }

以上代码,得到的结果将会是:
B hello foobar 01A hello foobar 01Bye bye A, we‘ll miss youB hello foobar 02
如不需要Start, Update, LateUpdate函数,请去掉他们
当你的脚本里没有任何Start, Update, LateUpdate函数的时候,Unity3D将不会将它们加入到他的Update List中,有利于脚本整体效率的提升。

我们可以从这两个脚本中看到区别:

1 //Update_01.cs2 public class Update_01 : MonoBehaviour { void Start () {} void Update () {}}3 4 //Update_02.cs5 public class Update_02 : MonoBehaviour { 6 7 }

 

第六部分:

1.减少固定增量时间
将固定增量时间值设定在0.04-0.067区间(即,每秒15-25帧)。您可以通过Edit->Project Settings->Time来改变这个值。这样做降低了FixedUpdate函数被调用的频率以及物理引擎执行碰撞检测与刚体更新的频率。如果您使用了较低的固定增量时间,并且在主角身上使用了刚体部件,那么您可以启用插值办法来平滑刚体组件。


2.减少GetComponent的调用使用 GetComponent或内置组件访问器会产生明显的开销。您可以通过一次获取组件的引用来避免开销,并将该引用分配给一个变量(有时称为"缓存"的引用)。
例如:

 1 //如果您使用如下的代码: 2 function Update () 3 { 4 transform.Translate(0, 1, 0); 5 } 6  7  8 //通过下面的更改您将获得更好的性能: 9 var myTransform : Transform;10 11 function Awake () {12 myTransform = transform;13 }14 15 function Update () {16 myTransform.Translate(0, 1, 0);17 }

 

3.避免分配内存
您应该避免分配新对象,除非你真的需要,因为他们不再在使用时,会增加垃圾回收系统的开销。
您可以经常重复使用数组和其他对象,而不是分配新的数组或对象。这样做好处则是尽量减少垃圾的回收工作。
同时,在某些可能的情况下,您也可以使用结构(struct)来代替类(class)。
这是因为,结构变量主要存放在栈区而非堆区。因为栈的分配较快,并且不调用垃圾回收操作,所以当结构变量比较小时可以提升程序的运行性能。
但是当结构体较大时,虽然它仍可避免分配/回收的开销,而它由于"传值"操作也会导致单独的开销,实际上它可能比等效对象类的效率还要低。

4.最小化GUI
使用GUILayout 函数可以很方便地将GUI元素进行自动布局。然而,这种自动化自然也附带着一定的处理开销。
您可以通过手动的GUI功能布局来避免这种开销。
此外,您也可以设置一个脚本的useGUILayout变量为 false来完全禁用GUI布局:

1 function Awake () {2 useGUILayout = false;3 }

 

5.使用iOS脚本调用优化功能
UnityEngine 命名空间中的函数的大多数是在 C/c + +中实现的。
从Mono的脚本调用 C/C++函数也存在着一定的性能开销。
您可以使用iOS脚本调用优化功能(菜单:Edit->Project Settings->Player)让每帧节省1-4毫秒。
此设置的选项有:
Slow and Safe – Mono内部默认的处理异常的调用
Fast and Exceptions Unsupported –一个快速执行的Mono内部调用。
不过,它并不支持异常,因此应谨慎使用。
它对于不需要显式地处理异常(也不需要对异常进行处理)的应用程序来说,是一个理想的候选项。

6.优化垃圾回收
如上文所述,您应该尽量避免分配操作。
但是,考虑到它们是不能完全杜绝的,所以我们提供两种方法来让您尽量减少它们在游戏运行时的使用:
如果堆比较小,则进行快速而频繁的垃圾回收

这一策略比较适合运行时间较长的游戏,其中帧率是否平滑过渡是主要的考虑因素。
像这样的游戏通常会频繁地分配小块内存,但这些小块内存只是暂时地被使用。
如果在iOS系统上使用该策略,那么一个典型的堆大小是大约 200 KB,这样在iPhone 3G设备上,
垃圾回收操作将耗时大约 5毫秒。如果堆大小增加到1 MB时,该回收操作将耗时大约 7ms。
因此,在普通帧的间隔期进行垃圾回收有时候是一个不错的选择。
通常,这种做法会让回收操作执行的更加频繁(有些回收操作并不是严格必须进行的),
但它们可以快速处理并且对游戏的影响很小:

1 if (Time.frameCount % 30 == 0)2 {3 System.GC.Collect();4 }

但是,您应该小心地使用这种技术,并且通过检查Profiler来确保这种操作确实可以降低您游戏的垃圾回收时间
如果堆比较大,则进行缓慢且不频繁的垃圾回收

这一策略适合于那些内存分配 (和回收)相对不频繁,并且可以在游戏停顿期间进行处理的游戏。
如果堆足够大,但还没有大到被系统关掉的话,这种方法是比较适用的。
但是,Mono运行时会尽可能地避免堆的自动扩大。
因此,您需要通过在启动过程中预分配一些空间来手动扩展堆(ie,你实例化一个纯粹影响内存管理器分配的"无用"对象):

1 function Start() 2 {3 var tmp = new System.Object[1024];4 // make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks5 for (var i : int = 0; i < 1024; i++)6 tmp = new byte[1024];7 // release reference8 tmp = null;9 }

游戏中的暂停是用来对堆内存进行回收,而一个足够大的堆应该不会在游戏的暂停与暂停之间被完全占满。所以,当这种游戏暂停发生时,您可以显式请求一次垃圾回收:
System.GC.Collect();
另外,您应该谨慎地使用这一策略并时刻关注Profiler的统计结果,而不是假定它已经达到了您想要的效果。

 

原文链接: http://www.unitymanual.com/thread-21597-1-1.html

unity3d优化总结篇