unity3dui性能优化

2017-12-20 13:53:45 wwlcsdn000 阅读数 6901

#Unity性能优化全攻略
总结自Siki的性能优化。
##优化相关前提
###Unity游戏安装包大/运行卡的原因
Mono虚拟机
这里写图片描述
解决这个问题
###DrawCall
https://zhuanlan.zhihu.com/p/26386905
https://www.zhihu.com/question/36357893
SetPasscall DrawCall和Batches-http://www.manew.com/4702.html
###Unity Profiler
https://docs.unity3d.com/Manual/Profiler.html
###Unity Statistics统计面板
http://blog.csdn.net/wdmzjzlym/article/details/51335915
http://www.mycmessia.com/2015/09/22/unity-camera-组件部分参数详解/
##资源优化
###资源优化标准
Mesh 动态模型:面片数<3000 材质数<3 骨骼数<50 静态模型:顶点<500
Audio 长时间音乐(背景音乐)压缩格式 mp3 短时间音乐(音效)非压缩格式 wav http://blog.csdn.net/u012565990/article/details/51794486
Texture 贴图长宽<1024
Shader 尽量减少复杂数学运算 减少discard操作
###模型优化
限制模型面数和定点数
https://jingyan.baidu.com/article/c74d60007b4d510f6a595db2.html
###贴图优化
贴图-材质-DrawCall的关系 材质合并 共享材质
贴图合并
https://mp.weixin.qq.com/s?__biz=MzAxMDcxOTkxNQ==&mid=402245125&idx=1&sn=6f6e768fa88e3c16f110f21813dcae02&scene=1&srcid=0425OWRBedO3NCVjYX72ntOj&from=singlemessage&isappinstalled=0#wechat_redirect
###如何减少冗余资源和重复资源
A、Resources目录下的资源不管是否被引用,都会打包进安装包 不使用的资源不要放在Resources目录下
B、不同目录下的相同资源文件,如果都被引用,那么都会打包进资源包,造成冗余 保证同一个资源文件在项目中只存放在一个目录位置
###资源的检测和分析
https://www.uwa4d.com/#assetbundle
##渲染优化(GPU)
###CPU GPU分工
https://www.zhihu.com/question/21475727
###LOD - 层级细节
添加组件LOD Group 在LOD2和LOD1可以修改贴图的品质,Cast Shadow和 ReceiveShadow可以移除
http://blog.csdn.net/yu__jiaoshou/article/details/52088894
Occlusion Culling - 遮挡剔除
http://blog.csdn.net/cartzhang/article/details/52684127
Lightmapping - 光照贴图
https://www.cnblogs.com/tgycoder/p/4853534.html
###合并Mesh
http://blog.csdn.net/chenggong2dm/article/details/41699029
Mesh Baker

##代码优化(CPU)
DrawCall减少,批处理,合并Mesh
资源池 - Object Pooling http://blog.csdn.net/wwlcsdn000/article/details/78778970
##其他优化
###优化工具
UWA Game Optimization Toolkit
###文章
使用Unity开发安卓游戏 应该如何进行性能优化
http://gad.qq.com/article/detail/17252
###编译性能优化
http://gad.qq.com/article/detail/27927
http://forum.china.unity3d.com/thread-13028-1-1.html

附带幕布结构图
这里写图片描述
链接: https://pan.baidu.com/s/16AWjmWz_EMYiF3_J06j1ew 提取码: 28qv 百度脑图打开

2016-10-24 14:26:18 jiexuan357 阅读数 7594

原贴地址:

http://blog.csdn.net/lijing_hi/article/details/11657887


听到过很多用Unity 3D开发游戏的程序员抱怨引擎效率太低,资源占用太高,包括我自己在以往项目的开发中也头疼过。最近终于有了空闲,可以仔细的研究一下该如何优化Unity 3D下的游戏性能。其实国外有不少有关U3D优化的资料,Unity官方的文档中也有简略的章节涉及这方面的内容,不过大多都是以优化美术资源为主,比如贴图的尺寸,模型静态及动态的batch以减少draw call,用lightmap替代动态光影,不同渲染模式在不同环境下的性能等等。鉴于此,加上美术资源方面的东西本人不是特别了解,所以都撇开不谈,这里先试着分析分析U3D脚本中常用代码段的执行效率


GetComponent


这是一个U3D脚本中使用频率最高的函数之一,这一族函数包括GetComponent,GetComponents,GetComponentInChildren,GetComponentsInChildren以及他们的泛型版本,此外GameObject类以及Component类上的很多属性也可以归于这一范畴,比如Component类的gameObject属性,GameObject类和Component类都有的transform属性等等这一系列从GameObject实例以及Component实例上获取其他挂载的内建组件的属性接口。

先来看看GetComponent函数的几种重载形式:

[csharp] view plain copy
  1. Component GetComponent(Type type);  
  2. T GetComponent<T>() where T : Component;  
  3. Component GetComponent(string type);  

通过ILSpy查看UnityEngine部分源码,发现泛型形式的GetComponent其实不过是在函数体中对泛型类型T调用了typeof,然后就直接调用了非泛型形式的GetComponent,因此在此不对泛型形式的GetComponent函数做讨论。下面设计一个小实验来看看两种不同GetComponent函数的效率,以及对GetComponent的不同使用方式会带来什么样的影响:


设计实验——实验执行的主要过程是对同一个gameObject连续获取同一类型的Component 8×1024×1024次,统计不同方法下的时间开销,单位是毫秒。在实验用的gameObject上一共挂在了五个各不相同的组件,所有的实验操作都是获取这五个组件中的第一个。

方案一,最直接的方式,直接在循环中对gameObject调用GetComponent(Type type)方法;

方案二,同样直接的方式,直接在循环中对gameObject调用GetComponent(string type)方法;

方案三,在循环外事先以GetComponent获取gameObject上的Component并缓存引用,然后在循环中直接访问缓存的引用;

方案四,利用C#扩展方法,对GameObject类添加扩展方法,以一个静态字典Dictionary<GameObject, Component>存储gameObject和gameObject上要取用的Component的键值对,然后在扩展方法里做字典查询以获得Component;

实验结果——方案一约1700ms,方案二约18500ms,方案三约30ms,方案四约1500ms。

(可能有人会对方案四抱有怀疑,担心字典中gameObject数量会影响查询效率,虽然我可以直接告诉你正常游戏里可能同时存在的GameObject数据量下对字典查询根本没有能够被觉察到的影响,但还是以数据来说明问题:

继续设计子实验,针对方案四,调整场景中gameObject的数量,每个gameObject上都挂载上述实验里的五个组件,并且都向字典中注册,对每种gameObject数量的情况都执行上述实验里的8×1024×1024次组件访问。

子实验结果——1个gameObject时约1500ms,5个gameObject时约1500ms,10个gameObject时约1500ms,100个gameObject约1500ms,1000个gameObject时约1500ms,10000个gameObject时还是约1500ms,此时向字典中注册所消耗的时间已经远远大于之后进行的循环的消耗。其实熟悉C#字典表的人根本不会有疑问,字典是散列表,查询复杂度O(1)。)


由上述实验可以得出结论,如果要获取一个gameObject上挂载的某个组件,在逻辑允许或者架构允许的情况下尽量事先缓存这个组件的引用,这是最高效的做法,开销可以忽略不计;假如情况不允许事先缓存引用,那么在调用频率不是很频繁的情况下可以使用GetComponent<T>()或者GetComponent(Type type)的重载形式;如果确实调用比较频繁,那么最好是自己对GameObject或者Component类进行扩展,以字典查询代替每次的GetComponent调用,毕竟效率稍微高那么一点点(当然了,如果组件是动态的,那么这个办法就不适用了,还是乖乖的用GetComponent);而GetComponent(string type)这个重载如无必要就不要使用,因为它每次调用时都必须进行类型反射,以至于效率只有另外两个重载形式的十分之一不到,即便是只能以字符串的形式得知所需组件的类型,也可以事先手动进行类型反射,而不是在频繁的GetComponent时直接传递字符串参数,只有一种情况下不得不使用GetComponent(string type)这个重载形式,那就是:每一次调用前都只能以字符串的形式的到组件类型,而且每一次调用前所获得到的组件类型是无法预测的,这中情况下手动做类型反射跟直接调用GetComponent没有区别。



看完GetComponent族函数之后,接下来就是GameObject类和Component类内置的组件访问属性。

在实际脚本代码编写中,你是否经常这样一长串代码就轻易写出来了:

[csharp] view plain copy
  1. Vector3 pos = gameObject.transform.position;  
  2. gameObject.collider.enabled = false;  

以我们的直觉,GameObject类和Component类所提供的这些属性应该都是直接访问的事先缓存好的组件引用,因此对这些属性的使用便无所顾忌。但是事情真的是如我们所想的那样吗?如果我告诉你,有时候哪怕是用GetComponent函数的string参数形式都会比使用这些属性来的要快,你相信么?还是用实验数据说话吧。


设计实验——对某gameObject上的Transform组件,采用不同的方法,访问8×1024×1024次。

方案一,实现缓存gameObject上transform组件的引用,然后所有访问都直接取用缓存的引用;

方案二,在脚本中直接以Component类的transform属性调用的方式访问(U3D脚本都是从MoniBehaviour类派生,而MonoBehaviour又派生自Component类,所以在脚本中可以直接访问transform属性,这一点相信很多人都知道);

方案三,在脚本中以gameObject.transform的形式访问组件(注意哦,很多人都有这个习惯,觉得组件是gameObject的组件,所以访问时都喜欢加上gameObject);

方案四,在脚本中以GetComponent<Transform>()函数访问组件;

实验结果——方案一约30ms,方案二约550ms,方案三约850ms,方案四约1700ms。


吃惊吧?transform属性访问的开销居然比直接访问引用要大这么多!而且通过gameObject转一道手之后开销居然又增加了这么多!不过还好,直接属性调用还是比用GetComponent要快的多……别太早下结论,Transform组件在每个GameObject实例上都有,对它的访问是不会失败的,那么如果被访问的组件在GameObject上不存在的时候呢?比如访问一个Rigidbody组件,而gameObject上没有挂载这样的组件,这时有会怎样?接着看实验。


设计实验——尝试对某gameObject上的Rigidbody组件进行访问8×1024×1024次。

方案一,gameObject上确实挂载了Rigidbody组件,事先缓存组件的引用,访问时取用缓存的引用;

方案二,gameObject上确实挂载了Rigidbody组件,脚本中以Component类的rigidbody属性访问组件;

方案三,gameObject上确实挂载了Rigidbody组件,脚本中以gameObject.rigidbody的方式访问组件;

方案四,gameObject上确实挂载了Rigidbody组件,脚本中以GetComponent<Rigidbidy>()访问组件;

方案五,gameObject上没有Rigidbody组件,事先缓存组件(当然获取到的是null),访问时取用引用;

方案六,gameObject上没有Rigidbody组件,脚本中以Component类的rigidbidy属性访问组件;

方案七,gameObject上没有Rigidbody组件,脚本中以gameObject.rigidbody方式访问组件;

方案八,gameObject上没有Rigidbody组件,脚本中以GetComponent<Rigidbody>()访问组件;

实验结果——方案一约30ms,方案二约800ms,方案三约1200ms,方案四约1700ms,方案五约30ms,方案六不少于60000ms,方案七不少于60000ms,方案八约1700ms。


更吃惊了吧?这一次的实验,前四组跟上一次实验差别不太大,但对rigidbody属性的访问还是要比transform属性慢了一点,后四组数据才是吃惊的根源,在组件不存在的情况下,通过属性访问组件居然会有如此大的额外开销!相比之下,GetComponent方法倒是不在乎组件是否真的存在,开销一如既往。

由于属性实现的代码无法通过ILSpy查看,所以在这里我只能用猜的了。首先是,U3D在实现这些组件访问属性的时候,必然做了各种查询和容错处理,绝非简单的缓存和取用引用那么简单,这也是属性访问比事先缓存引用的访问方式要慢那么多的原因;其次,Transform组件在每个GameObject实例上都必然存在,因此transform属性的实现比其他组件访问属性的实现必然要少那么一些步骤,这就造成对transform属性的访问要比其他组件属性快上一些;最后,当组件不存在时,对组件属性的访问应该是走入了大量的容错处理代码,这就造成这种情况下属性访问开销大增。

从这个实验又可以得出结论,我们的脚本代码里经常会需要访问gameObject引用或者某个组件的引用,最好的方式当然是在脚本Awake的时候就把这些可能访问的东西都缓存下来;如果需要访问临时gameObject实例的某属性或者临时某组件的gameObject实例,在能够确保组件一定存在的情况下,可以用属性访问,毕竟它们比GetComponent要快上一倍,但是如果不能确定组件是否存在,甚至是需要对组件的存在性做判断时,一定不要用对属性访问结果判空的方式,而要用GetComponent,这里面节省的开销不是一点半点。




IsAlive

U3D的粒子系统脚本接口相信很多人都用过,ParticleSyetem类的一系列接口都有一个bool类型的参数——withChildren,通过这个参数可以直接将相同的判断或者操作应用到一整个通过Transform父子关系树关联起来的ParticleSystem实例集合上。然而,但凡方便的功能,里面就必然有性能陷阱……

以IsAlive这个接口为例(用来判断粒子系统是否所有粒子都已经消亡,一般用在非loop的例子发射器上),看看U3D里是如何实现这个接口的:

[csharp] view plain copy
  1. public bool IsAlive()  
  2. {  
  3.     bool withChildren = true;  
  4.     return this.IsAlive(withChildren);  
  5. }  
[csharp] view plain copy
  1. public bool IsAlive(bool withChildren)  
  2. {  
  3.     if (withChildren)  
  4.     {  
  5.         ParticleSystem[] particleSystems = ParticleSystem.GetParticleSystems(this);  
  6.         ParticleSystem[] array = particleSystems;  
  7.         for (int i = 0; i < array.Length; i++)  
  8.         {  
  9.             ParticleSystem particleSystem = array[i];  
  10.             if (particleSystem.Internal_IsAlive())  
  11.             {  
  12.                 return true;  
  13.             }  
  14.         }  
  15.         return false;  
  16.     }  
  17.     return this.Internal_IsAlive();  
  18. }  

可以看到,如果传递的withChildren参数为true,那么函数会先尝试调用GetParticleSystems(this)来获取包括下级gameObject在内的所有能找得到的粒子系统组件,然后对这些粒子系统组件依次再调用IsAlive判断。而如果withChildren为false,就仅仅会判断自身。那么自然,开销大小与否,关键就在GetParticleSystems的实现上了。

[csharp] view plain copy
  1. internal static ParticleSystem[] GetParticleSystems(ParticleSystem root)  
  2. {  
  3.     if (!root)  
  4.     {  
  5.         return null;  
  6.     }  
  7.     List<ParticleSystem> list = new List<ParticleSystem>();  
  8.     list.Add(root);  
  9.     ParticleSystem.GetDirectParticleSystemChildrenRecursive(root.transform, list);  
  10.     return list.ToArray();  
  11. }  
[csharp] view plain copy
  1. private static void GetDirectParticleSystemChildrenRecursive(Transform transform, List<ParticleSystem> particleSystems)  
  2. {  
  3.     foreach (Transform transform2 in transform)  
  4.     {  
  5.         ParticleSystem component = transform2.gameObject.GetComponent<ParticleSystem>();  
  6.         if (component != null)  
  7.         {  
  8.             particleSystems.Add(component);  
  9.             ParticleSystem.GetDirectParticleSystemChildrenRecursive(transform2, particleSystems);  
  10.         }  
  11.     }  
  12. }  

U3D对获取所有下级gameObject实例上的粒子系统组件使用了递归的方式,并在递归结束后返回结果列表时做了一次列表元素复制(List.ToArray()),并且在获取粒子系统组件的时候用的是transform2.gameObject.GetComponent<ParticleSystem>(),而不是transform2.GetComponent<ParticleSystem>(),从上一篇文章里我们已经用实验证实了,前一种方式开销更大。看到这里,我们心里大概已经有谱了,那就是——效率绝对不会高到哪里去,影响性能的地方太多了……还是设计一个小实验来看看这种情况下应该用什么样的方式更好吧:

设计实验——一个两层结构,一个父gameObject挂载一个ParticleSystem组件,两个子gameObject分别挂载一个PariticleSystem组件,采用两种不同的方式对这个组合判断IsAlive各8×1024×1024次。
方案一,直接对父gameObject上的PariticleSystem调用IsAlive(true);
方案二,在循环前,先用GetComponentsInChildren将所有的PariticleSystem存入一个List,循环中对这个List做遍历,对List里每一个ParticleSystem调用IsAlive(false);
实验结果——方案一约3900ms,方案二约65ms。

结果对比很明显。其实,U3D提供的这个接口的意义在于,当不是需要进行那么频繁的调用时,可以用IsAlive(true)来省掉手动获取所有子粒子系统的过程,让代码简洁一些,虽然U3D目前对这个接口的实现有的地方还值得斟酌。ParticleSystem提供的这一族接口(IsAlive只是其中之一,此外还有Play,Pause,Stop等等),如果使用频率不是很高,比如仅仅是初始化或者销毁的时候做一次性调用,那么即便是withChildren参数是true也没有什么大不了的,还能少些很多代码,何乐而不为;但如果需要频繁调用,比如每帧都对粒子系统集合判断IsAlive,这种情况下,一定不能懒惰,该写的东西还是要写的。另外值得注意的一点,IsAlive这一族接口的无参形式,是默认withChildren为true的,使用的时候可别搞错了。

PS,留意一下GetDirectParticleSystemRecursive的实现方式,你会发现它有一个递归条件,就是节点上必须要有PariticleSystem组件,在递归过程中,一旦发现某个节点上没有ParticleSystem组件时,父子关系树上的这一枝就算遍历到头了,再往下即便是还有ParticleSystem存在也会被忽略。因此,如果你面对的ParticleSystem集合就恰好存在这样的断层,那最好还是自己勤快一点,自己动手用GetComonentsInChildren来查找所有的粒子系统组件。




CenterOfMass

在处理物理碰撞时,尤其是OnTrigger族消息,由于要手动计算碰撞点会经常要用到碰撞体的质心。获取质心常用的有三种方式:
1、Collider.bounds.center
2、Collider.rigidbody.worldCenterOfMass
3、Collider.attachedRigidbody.worldCenterOfMass
第一种方式其实是认为碰撞体是均匀几何体,所以取碰撞盒中心作为质心。注意,这里的Collider并不是collider属性,而是表示一个Collider引用,所以不必考虑collider属性访问引起的性能开销(其实在处理碰撞时,Collider引用是能够直接得到的,也不需要做collider属性访问)。

设计实验——针对同一个碰撞体,分别调用以上三种方式各8×1024×1024次
实验结果——第一种约4500ms,第二种约2500ms,第三种约2000ms。

在碰撞不涉及刚体的情况下,要获取质心只能使用性能最差的第一种方式。而有刚体的情况下,自然推荐使用第三种方式。

2018-07-15 18:19:17 qq_39741605 阅读数 9307

Quote from:https://zhuanlan.zhihu.com/p/39529241

性能优化是游戏项目开发中一个重要且必须的元素。用户和项目的需求在并且会持续增长。而即便在硬件设备高速发展的今天,游戏特效、画质、场景复杂度的需求也都向着榨干硬件性能的趋势提升,无论研发团队有多么丰富的经验积累,性能优化永远是一个非常棘手而又无法绕开的问题。

实际上,通过百度、谷歌、知乎可以搜到大把关于Unity性能优化的文章,但大多只是简单的论述、介绍、翻译和转载,或针对某中特定问题优化方式的教程。因此,我们在这里推出Unity3D性能优化系列文章,旨在给读者提供一个全面、易懂、可操作的unity优化教程,以便初学者学习使用。

在这里,我们的第一篇文章,主要作为一个引导,通过讲解对unity优化工具的了解及使用,给读者提供在实际项目中,进行游戏性能优化的流程和思路。


游戏性能简述

提起游戏性能,首先要提到的就是,不仅开发人员,所有游戏玩家都应该会接触到的一个名词:帧率(Frame rate)

帧率是衡量游戏性能的基本指标。在游戏中,“一帧”便是是绘制到屏幕上的一个静止画面。绘制一帧到屏幕上也叫做渲染一帧。每秒的帧数(fps)或者说帧率表示GPU处理时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。

现阶段大多数游戏的理想帧率是60FPS,其带来的交互感和真实感会更加强烈。通常来说,帧率在30FPS以上都是可以接受的,特别是对于不需要快速反应互动的游戏,例如休闲、解密、冒险类游戏等。有些项目有特殊的需求,比如VR游戏,至少需要90FPS。当帧率降低到30FPS以下时,玩家通常会有不好的体验。

而现阶段随着支持144HZ刷新率的硬件设备的涌现,能否维持对应高帧率又是一项新的指标,尤其是在电竞领域


但在游戏中重要的不仅仅帧率的速度,帧率同时也必须非常稳定。玩家通常对帧率的变化比较敏感,不稳定的帧率通常会比低一些但是很稳定的帧率表现更差。

虽然帧率是一个我们谈论游戏性能的基本标准,但是当我们提升游戏性能时,更因该想到的是渲染一帧需要多少毫秒。帧率的相对改变在不同范围会有不同的变化。比如,从60到50FPS呈现出的是额外3.3毫秒的运行时间,但是从30到20FPS呈现出的是额外的16.6毫秒的运行时间。在这里,同样降低了10FPS,但是渲染一帧上时间的差别是很显著的。

我们还需要了解渲染一帧需要多少毫秒才能满足当前帧率。通过公式 1000/(想要达到的帧率)。通过这个公式可以得到,30FPS必须在33.3毫秒之内渲染完一帧,60FPS必须在16.6毫秒内渲染完一帧。

渲染一帧,Unity需要执行很多任务。比如,Unity需要更新游戏的状态。有一些任务在每一帧都需要执行,包括执行脚本,运行光照计算等。除此之外,有许多操作是在一帧执行多次的,例如物理运算。当所有这些任务都执行的足够快时,我们的游戏才会有稳定且理想的帧率。当这些任务执行不满足需求时,渲染一帧将花费更多的时间,并且帧率会因此下降。

知道哪些任务花费了过多的时间,是游戏性能问题的关键。一旦我们知道了哪些任务降低了帧率,便可以尝试优化游戏的这一部分。这就是为什么性能分析工具是游戏优化的重点之一。



Unity3d性能分析工具

工欲善其事必先利其器,这里我们来讲解Unity3D优化所需的工具

如果游戏存在性能问题,游戏运行就会出现缓慢、卡顿、掉帧甚至直接闪退等现象。在我们尝试解决问题前,需要先知道其问题的起因,而尝试不同的解决方案。若仅靠猜测或者依据自身原有的经验去解决问题,那我们可能会做无用功,甚至引申出更复杂的问题。

在这些时候我们就需要用到性能分析工具,性能分析工具主要测试游戏运行时各个方面性能,如CPU、GPU、内存等。通过性能分析工具,我们能够透过游戏运行的外在表现,获取内在信息,而这些信息便是我们锁定引起性能问题的关键所在。

在我们进行Unity性能优化的过程中,最主要用的到性能分析工具包括,Unity自带的Unity Profile,IOS端的XCode ,以及一些第三方插件,如腾讯推出的UPA性能分析工具。

我们主要针对Unity Profile进行讲解,之后也会略微介绍另外一些性能分析工具。


Unity Profile

Unity Profile是Unity中最常用的官方性能分析工具,在使用Unity开发游戏的过程中,借助Profiler来分析CPU、GPU及内存使用状况是至关重要的。

首先我们来了解Unity Profile的面板:
我们通过Window——>Profiler来激活Unity Profile面板

在下图中我们可以看到Unity Profile面板,其中有很多profilers,每个profiler显示我们当前项目一个方面的信息,如CPU、GPU、渲染(Rendering)、内存(Memory)、声音(Audio)、视屏(Video)、物理(Physics)、ui及全局光照(global illumination)。


当项目运行时,每个profilers会随着运行时间来显示数据,有些性能问题是持续性的,有些仅在某一帧中出现,还有些性能问题可能会随时间推移而逐渐显出出来。

在面板的下半部分显示了我们选中的profilers当前帧的详细内容,我们可以通过选择列标题,通过这一列的信息值来排序。
在CPU usage profiler中的列表题分别为:
Total:当前任务的时间消耗占当前帧cpu消耗的时间比例。 
Self:任务自身时间消耗占当前帧cpu消耗的时间比例。 
Calls:当前任务在当前帧内被调用的次数。 
GC Alloc:当前任务在当前帧内进行过内存回收和分配的次数。 
Time ms:当前任务在当前帧内的耗时总时间。 
Self ms:当前任务自身(不包含内部的子任务)时间消耗。

当我们在层级视图中点击函数名字时,CPU usage profiler将在Profiler窗口上部的图形视图中高亮显示这个函数的信息。比如我们选中Cameta.Render,Rendering的信息就会被高亮显示出来。


我们可以Profiler的左下的下拉菜单中选择Timeline。


Timeline显示了两件事:cpu任务的执行顺序和哪个线程负责什么任务。线程允许不同的任务同时执行。当一个线程执行一个任务时,另外的线程可以执行另一个完全不同的任务。和Unity的渲染过程相关的线程有三种:主线程,渲染线程和worker threads。了解哪个线程负责哪些任务的用处非常之大,一旦我们知道了在哪个线程上的任务执行的速率最低,那么我们就应该集中优化在那个线程上的操作。


以上所显示的数据依赖于我们当前选择的profiler。例如,当选中内存时,这个区域显示例如游戏资源使用的内存和总内存占用等。如果选中渲染profiler,这里会显示被渲染的对象数量或者渲染操作执行次数等数据。

这些profiler会提供很多详细信息,但是我们并不总需要使用所有的profiler。实际上,我们在分析游戏性能时通常只是观察一个或者两个profiler,而不需要观察的我们可以通过右上角的”X”关闭,如果需要在添加回来,可以通过左上角Add Profiler。
例如,当我们的游戏运行的比较慢时,我们可能一开始先查看CPU usage profiler,CPU usage profiler也是在我们进行优化分析时最常用的Profiler。


当然,除了CPU usage profiler,Unity Profiler中其他的Profiler在一些场合也非常的有用,比如GPU、内存、渲染等,其使用方法和CPU usage profiler也是大同小异,可以按照以上的步骤来查看并学习。

我们在观察数据时,需要观察的目标有如下几点:

CPU:
1. GC Allow: 任何一次性内存分配大于2KB的选项。
每帧都具有20B以上内存分配的选项 。

GC相关的问题和优化,在之后我们会详细的介绍。

2. Time ms:

注意占用5ms以上的选项

内存 
1. Texture: 检查是否有重复资源和超大内存是否需要压缩等.。
2. AnimationClip: 重点检查是否有重复资源.。
3. Mesh: 重点检查是否有重复资源。



实际项目中的优化建议

在了解了Unity Profiler后,现在我们在一个实际项目中来进行一次性能分析。同时来了解一般在实际项目中,主要会引起也是我们主要去观察的性能问题出现在什么地方。

以下是我做的一个简单的游戏项目,并未做任何性能优化并且有大量引起性能问题的代码,可以更方便大家观察其性能问题,在之后我会把工程上传到github供初学者下载分析。


我们来看一下在CPU usage profiler面板中的可观察项,在项目中我们可以先关闭VSync垂直同步来提高帧率。

下图中我关闭了除VSync之外的显示,可以看到VSync的消耗


具体步骤是edit->project settings->Quality,在Inspector面板中,V Sync count选择don’t Sync.


我们来简单的介绍一下什么是垂直同步,以及关闭它之后会发生什么。

要理解垂直同步,首先明白显示器的工作原理。

显示器上的所有图像都是单个像素组成了水平扫描线,水平扫描线在垂直方向的堆积形成了完整的画面,无论是隔行扫描还是逐行扫描,显示器都有两种同步参数——水平同步和垂直同步。 
垂直和水平是CRT显示器中两个基本的同步信号,水平同步信号决定了CRT画出一条横越屏幕线的时间,垂直同步信号决定了CRT从屏幕顶部画到底部,再返回原始位置的时间,而垂直同步代表着CRT显示器的刷新率水准。 
在游戏项目中,如果我们选择等待垂直同步信号也就是打开垂直同步,在游戏中或许性能较强的显卡会迅速的绘制完一屏的图像,但是没有垂直同步信号的到达,显卡无法绘制下一屏,只有等85单位的信号到达,才可以绘制。这样FPS自然要受到刷新率运行值的制约。 
而如果我们选择不等待垂直同步信号,那么游戏中绘制完一屏画面,显卡和显示器无需等待垂直同步信号就可以开始下一屏图像的绘制,自然可以完全发挥显卡的实力。 
但是,正是因为垂直同步的存在,才能使得游戏进程和显示器刷新率同步,使得画面更加平滑和稳定。取消了垂直同步信号,固然可以换来更快的速度,但是在图像的连续性上势必会打折扣。 

需要注意,LCD显示器其实也是存在刷新率的,但其机制与CRT不同,这里不做过多赘述,但是垂直同步和水平同步对于LCD显示器来说,一样是有必要的。


在关闭垂直同步后,我们继续看我们的项目



可以看到,我们以Total和Time ms排序,在图中拉黑的项(Camera Render)始终排在最前面。
Camera Render是相机渲染工作的CPU占用量,在实际项目中,渲染是最常见的引起性能问题的原因。 而因为渲染而引起的性能问题的优化是一个非常大的工程,这方面的优化方法在我们后续的文章中会有详细的教程去学习和分析。在这里,我们只需要先了解。
我们这个项目的优化中,无疑,渲染造成的性能损耗是一个大头。

如果说,在我们性能分析中,渲染已经没有什么问题,那么我们接下来要重点观察的就是GC,也就是垃圾回收性能分析
我们按照GC Alloc的顺序来显示,可以看到下图。




在之前我们提到过,GC Alloc中,任何一次性内存分配大于2KB的选项,每帧都具有20B以上内存分配的选项 ,是需要我们重点关注的,显而易见,我们的项目中,对于GC的优化,也有很大的问题。关于CG的问题,我们会在下一篇Unity3D性能优化——CPU篇中,详细的介绍。


这里我们大致介绍一下GC的机制,要想了解垃圾回收如何工作以及何时被触发,我们首先需要了解unity的内存管理机制。Unity主要采用自动内存管理的机制,开发时在代码中不需要详细地告诉unity如何进行内存管理,unity内部自身会进行内存管理。 
Unity内部有两个内存管理池,堆内存和栈内存,垃圾回收主要是指堆上的内存分配和回收,unity中会定时对堆内存进行GC操作。 
当堆内存上一个变量不再处于激活状态的时候,其所占用的内存并不会立刻被回收,不再使用的内存只会在GC的时候才会被回收。 
每次运行GC的时候,GC会检查堆内存上的每个存储变量,对每个变量会检测其引用是否处于激活状态,如果变量的引用不再处于激活状态,则会被标记为可回收,被标记的变量会被移除,其所占有的内存会被回收到堆内存上。 

GC操作是一个极其耗费的操作,堆内存上的变量或者引用越多则其运行的操作会更多,耗费的时间越长。


如果我们也排除了GC的问题, 那么再接下来,我们就要考虑到是否是脚本的一些问题造成性能损耗。

这里的脚本,可能是我们自己写的代码,也有可能是我们使用的一些插件的代码。在CPU usage profiler面板中,我们可以关注Script这一项。



如果在一个很慢的帧中,一大部分时间被脚本运行所消耗,这意味着这些慢的脚本可能就是引起性能问题的主因。我们可以更加深入的分析数据来确认。

首先我们按照Time ms来排序,然后选择信息列表中的项目,如果是用户脚本的函数,那么在Profiler上方会有高亮脚本的部分。这种情况,说明游戏的性能问题是和用户脚本相关的,如下图中的显示,这部分脚本性能问题一定是与我们FixedUpdate有关。



同时,我们还可以再关注一些物理、ui方面的性能问题。

在上面我们讨论的,是几种最常见的性能问题,在实际项目优化中,如果有性能问题也逃不开这些,如果在这些方向都已经达到了我们的要求,但我们的游戏仍然有性能问题,我们应该遵循上面的方法解决问题:收集数据——>使用CPU usage profiler查看信息——>找到引起问题的函数。一旦我们知道了引起问题函数的名字,我们便可以针对性的,对其进行优化处理。




其他性能分析工具

在开头我们说过,在我们进行Unity性能优化的过程中,最主要用的到性能分析工具包括,Unity自带的Unity Profile,IOS端XCode Capture GPU frame以及一些第三方插件,如腾讯推出的UPA性能分析工具。

这里我们简单的介绍一下XCode和UPA.

Xcode是 Mac OS X上的集成开发工具。在我们打包Unity IOS的项目时,必须使用到Xcode来帮助我们打包及发布。

Xcode的功能也十分的强大,在我们开发IOS端时,可以使用其GPU frame Capture 功能为我们的项目进行性能优化分析。

在unity中,我们打包时在Run In Xcode as 选择debug模式,并且勾选Development Build





打包完成后,使用Xcode打开文件,在Xcode中选择Product ——> Scheme——> Manage Schemes




然后会出现如下界面



我们双击这个项目会出现如下界面


.


然后我们在左侧选中Run,然后在右侧面板选择Options




在GPU frame Capture中选择OpenGL ES或者Metal。

在Debug模式下运行项目,当项目在真机上完全加载后,就可以进入Debug Navigator(View ——> Navigators ——> Show Debug Navigator)




以下是GPU frame Capture具体功能的界面,在图形化界面中,可以在游戏运行时清晰的了解到CPU、GPU、内存的使用情况。









XCode的Capture GPU frame功能能高效且定量地定位到GPU中shader的消耗。

UPA是腾讯和Unity联合打造的一款性能分析工具,据说王者荣耀的性能优化分析就有使用到UPA,具体的使用方法可以通过

客户端性能测试【腾讯WeTest】去了解


2016-07-12 19:37:28 hcud024 阅读数 3898


官方优化文档--优化图像性能
http://docs.unity3d.com/Documentation/Manual/OptimizingGraphicsPerformance.html

Unity3D性能优化专题
性能优化是一个异常繁琐而又涉及到项目开发的方方面面的一个过程,它的本质是在运行时的一个时间里尽可能完美展现丰富的内容。
实现优化可以通过优化资源、渲染、粒子、物理等模式;
也可以通过修改模型大小、减少纹理尺寸并结合Unity3D的一些相关特性来提升游戏的性能。
随着移动端的设备硬件能力的提升,如何使用尽可能优化的资源和程序效率来展现出更多的细节内容就成为了每个开发者都应该思考的内容,这也使得优化变成了项目开发中非常重要的一环。
***********
首先介绍下draw call(这个东西越少你的游戏跑的越快):
在游戏中每一个被展示的独立的部分都被放在了一个特别的包中,我们称之为“描绘指令”(draw call),然后这个包传递到3D部分在屏幕上呈现出来。这就和你希望你的亲友收到准备好的圣诞礼物需要包装好然后穿过城市准时放在他应该出现的地方一样没什么不同。你的CPU来完成包装和传递他们的活,同时会消耗很多的带宽,所以最终分配好这些关键性资源很重要。目前,真正可怕的事情是从描绘指令消耗远景开始,每一个独立的飞溅到地板上的血迹和一个角色或者一具死尸消耗的字节是一样的多的:他们都消耗同样的描绘指令。除此之外,没有什么更多的差别。
那么如何降低 draw call 呢??那么我们就用到Culling(剔除)技术。如果不应用这个技术,电脑是不管3721把场景里所有的东西都送去渲染的。看得见的也渲染,看不见得照样也送去渲染。很傻是吧,那咋办呢。得告诉电脑,那个你
看得见的渲染,看不见的就算了。于是就有了
1.视锥体剔除(Frustum Culling)这个unity系统自带了好像,就不用操心了。
2.遮挡剔除(Occlusion Culling)
Unity 3专业版内置了一个强大的 Occlusion Culling 插件 Umbra免费的
遮挡剔除(Occlusion Culling)
遮挡剔除是一种什么样的特性呢, 当一个物体被其他物体遮挡住而不在摄像机的可视范围内时不对其进行渲染。. 遮挡剔除在3D图形计算中并不是自动进行的。因为在绝大多数情况下离 camera 最远的物体首先被渲染,靠近摄像机的物体后渲染并覆盖先前渲染的物体(这被称为重复渲染,无效渲染"overdraw"). 遮挡剔除不同于视锥体剔除. 视锥体剔除只是不渲染摄像机视角范围外的物体而对于被其他物体遮挡但依然在视角范围内的物体则不包括在内. 注意当你使用遮挡剔除时你依然受益于视锥体剔除(Frustum Culling).
***********
Unity(或者说基本所有图形引擎)生成一帧画面的处理过程大致可以这样简化描述:引擎首先经过简单的可见性测试,确定摄像机可以看到的物体,然后把这些物体的顶点(包括本地位置、法线、UV等),索引(顶点如何组成三角形),变换(就是物体的位置、旋转、缩放、以及摄像机位置等),相关光源,纹理,渲染方式(由材质/Shader决定)等数据准备好,然后通知图形API——或者就简单地看作是通知GPU——开始绘制,GPU基于这些数据,经过一系列运算,在屏幕上画出成千上万的三角形,最终构成一幅图像。
在Unity中,每次引擎准备数据并通知GPU的过程称为一次Draw Call。这一过程是逐个物体进行的,对于每个物体,不只GPU的渲染,引擎重新设置材质/Shader也是一项非常耗时的操作。因此每帧的Draw Call次数是一项非常重要的性能指标,对于iOS来说应尽量控制在20次以内,这个值可以在编辑器的Statistic窗口看到。 Unity内置了Draw Call Batching技术,从名字就可以看出,它的主要目标就是在一次Draw Call中批量处理多个物体。只要物体的变换和材质相同,GPU就可以按完全相同的方式进行处理,即可以把它们放在一个Draw Call中。Draw Call Batching技术的核心就是在可见性测试之后,检查所有要绘制的物体的材质,把相同材质的分为一组(一个Batch),然后把它们组合成一个物体(统一变换),这样就可以在一个Draw Call中处理多个物体了(实际上是组合后的一个物体)。
但Draw Call Batching存在一个缺陷,就是它需要把一个Batch中的所有物体组合到一起,相当于创建了一个与这些物体加起来一样大的物体,与此同时就需要分配相应大小的内存。这不仅会消耗更多内存,还需要消耗CPU时间。特别是对于移动的物体,每一帧都得重新进行组合,这就需要进行一些权衡,否则得不偿失。但对于静止不动的物体来说,只需要进行一次组合,之后就可以一直使用,效率要高得多。 - See more at: http://ravenw.com/blog/2011/10/14/unity-optimization-of-draw-call/#sthash.0WfH4KnX.dpuf
***********

输出设置相关:
1:最终输出的时候,在unity - edit - project setting - player 里, 把 other settings - script call optimization 改成 快速, 但没有例外. fast but no exceptions
2: unity - edit - project setting - time 里, 把 maximum allowed timestep 改成0.1
3:

常规优化
1: 联结(combine)优化
显卡对于一个含100个面片的物体的和含1500个面片的物体的渲染消耗几乎是等价的。所以如果你有N个同一材质的东西,那么把他们联成同一个物体再统一用一个material那么对于显卡的渲染消耗就要降低N倍。
在unity里再联结,这个要怎么做呢,其实也挺简单的,经常看Island Demo项目的人应该很早就注意到里面的石头这些都是连在一起的,原因就在这里,他提供了现成就脚本实现联结。
先到Island Demo的Assets/Script下找到CombineChildren.cs和MeshCombineUtility.cs两个脚本复制到自己的项目文件(我们要用的只是前者,但他会调用后者,没有后者unity会报错,所以把后者扔到项目里不管就好)
然后把你项目里那些用同一Materials的东西扔到一个空物体里面去,再把CombineChildren.cs贴到那个空物体上,搞定!
2: 模型
(1)只用一个mesh renderer, 少用多materials, 最多3个
每个角色尽量使用一个 Skinned Mesh Renderer
这是因为当角色仅有一个 Skinned Mesh Renderer 时, Unity 会 使用可见性裁剪和包围体更新的方法来优化角色的运动,而这种优化只有在角色仅含有一个 Skinned Mesh Renderer 时才会启动。
(2)模型骨骼不超过30个.
(3)尽量减少面 300-1500
(4)模型尽量不要分开, 如果多个模块, 会多次调用dc
(5)一般角色应该没有 IK 结点
这是因为角色的动作大多数都是事先设定好的,并不需要经过 IK 操作来进行实时计算( Rogdoll 除外),所以在模型导入时,不要将 IK 结点一起导入。
(6) 不要附加 Animation Component 在静态实体上附加 Animation 部件虽然对结果没有影响,但却会增加一定的 CPU 开销来调用这一组件,所以尽量去掉该组件。

3:尽量不用像素光(pixels Lights)
4:不用 软阴影(soft shadow), 或者不用阴影(No Shadows)
灯光能不用就不用, 阴影可以用面来代替.
5:少用实时灯光, 尽量使用lightmap
http://blog.sina.com.cn/s/blog_409cc4b00100no8y.html
动态实时灯光相比静态灯光,非常耗费资源。所以除了能动的角色和物体(比如可以被打的到处乱飞的油桶)静态的地形和建筑,通通使用Lightmap。
强大的Unity内置了一个强大的光照图烘焙工具Beast,这个东东是Autodesk公司的产品
6:transform和OnGUI (运算上的优化远比不上 绘制效率上的优化,少个dc可能就比得上这些了)
http://fredxxx123.wordpress.com/2013/05/22/unity%E6%80%A7%E8%83%BD%E5%84%AA%E5%8C%96%E4%B9%8B%E5%B0%8F%E6%B8%AC%E8%A9%A6/
// transfrom
少用transform, 多用 myCachedTransform
頻繁使用該物件的地方,就是先把該物件cache起來,理論上就會避免這種不必的開銷。
※這也可延伸至其他是property的變數。例如:transform.position。
***直接使用的版本:
public class TestUnit : MonoBehaviour
{
void Update()
{
var t = this.transform;
t = this.transform;
t = this.transform;
t = this.transform;
t = this.transform;
}
}
***cache 使用的版本:
public class TestUnit : MonoBehaviour
{
Transform myTransform;
void Start()
{
myTransform = transform;
}

void Update ()
{
var t = this.myTransform;
t = this.myTransform;
t = this.myTransform;
t = this.myTransform;
t = this.myTransform;
}
}
// OnGUI
空物件版本:
public class TestUnit : MonoBehaviour
{
}
外加一個空白OnGUI的版本:
public class TestUnit : MonoBehaviour
{
void OnGUI()
{ }
}
實際上是被GUI.Begin()和GUI.End()所佔據,function本身是無辜的~~
非必要避免使用OnGUI(),即使它裡面甚麼事情都沒做!
如需使用,請盡可能集中到同一個OnGUI()底下執行繪製,好避免Begin End的開銷。

7:动态物体的相关优化

优化主要分为两个方向,一个是资源相关优化和引擎相关的优化。资源相关的优化,大概分为动态物体、静态物体、纹理数据、音频数据、程序包数据。对于动态物体比如NPC、怪物等,需要对面片数量的控制,大概在300到2000面。1500面就可以体现人物细节,但如果是人物比较多,可能要降低面数,不要低于300。另外,一方面是控制Skinned Mesh Renderer的数量;另一方面是控制材质数量在1到3种。人物最好用小于30根骨骼,如果你用的骨骼越多,耗费的CPU就更多,所以在移动平台上尽量少于30根。现在我们看其他动态物体,利用Dynamic Batching进行合批。这个下雨特效并不是系统做的,是包含很多雨点的网格进行重复拷贝,然后错乱移动实现的。每一个雨点并不是一个粒子,这样能减少很多CPU的消耗,每一个整体网格都会有一个顶点的控制,通过控制顶点数量,对系统实现雨点效果来说,这是一个相当省时省力的方法。

8:静态物体的相关优化

下面我们来看静态物体,静态物体也是要控制面数和顶点数,顶点数少于500个。static是不会进行移动缩放、旋转的,把它标记为static,当然他们的材质是一样的。不要添加animation组建,对于静态物体来说,这个组件毫无意义,能把他丢掉就丢掉,因为这对CPU的消耗是非常客观的。
9:音频程序的优化
关于音频时间的播放,比如背景音乐,建议使用MP3压缩格式,比如音效,要求数据尽快加载,这些数据比较小就可以,使用WAV和AIF未压缩音频格式。关于程序包的优化,很多开发者会埋怨说打出来的包太大,现在介绍减少程序包的方法,首先使用压缩格式的纹理,以显卡的压缩格式保存,使用压缩网格和动画数据。网格压缩是先采用量化处理,当然这个压缩是保证在包里面的数据小,但运行时占用的内存没有减少,因为我们并没有把顶点删除,但是对动画数据来说,动画数据经过压缩处理后降低,可以减少游戏列层。

关于代码尽量不要使用System.xml,我们建议使用Mono.xml。启用Stripping来减少库的大小,使用剥离方式。

10:引擎相关优化和物理相关优化
下来是引擎相关的优化,例如光照设置、相继设置、粒子特效、物理特效等。那拿光照设置来说,光源全部的实时光照这是很恐怖的,每一次实施光照代表着每一次使用消耗,怎么优化?有人使用LightMapping来制作静态场景,他的好处是不需要用多张实施光照,而给场景很好的光照效果。有人使用Light Probes代替实时光照,好处是完全不用怎么消耗,而且运作性能也非常高。在有些时候使用Light Probes代替光照,他能跟场景很好的融合,在一个角落里,这个任务会被阴影打得暗一些。如果说场景中确实需要一些实时光源,那么肯定是需要做过优化设置的实时光源,控制important的光源个数。如果说光源有些地方产生了交叉光,这个时候你可以通过设置Pxel Light,控制每一个光源都只接受一个动态光照,数目大概是1—2个。对于关闭光源的实时阴影,并不是所有平台都支持实时阴影,消耗也非常大,不建议大家使用。关于相机方面的设置,平面越近,渲染越少。我们更建议使用分层,比如远处的建筑,对于建筑物的裁减平面远一些,如果是花草,就可以使用平面就近一些。现在看一下粒子特效,粒子也是游戏中需要优化的东西,建议屏幕中最大的粒子数不要超过200,同时每个发射器发射的最大粒子数不要超过50。粒子尺寸也要尽可能小,最终在屏幕有多少像素。他们中间的像素可能会被渲染很多次,至少四五次,这时发现粒子系统仅像素就填充了更多屏幕,这时候对游戏来说非常耗费,对游戏的其他功能性能也有所影响。另外一方面,对于非常小的粒子,尽量不要开启粒子碰撞功能。

现在我们看一下物理相关优化,物理尽可能使用Sphere Coillider、Box Coillider等,尽量避免使用Meh Colllider等。渲染设置,避免使用Alpha Test,因为非常耗时,性价比很低。关于Sttic Batching,对静态物体进行Batch,对几何数据的大小没有限制。物体被合并后会带来一些内存消耗,比如说有控制网格的物体,用Batch会合并成大物体。Dynamic Batching目前仅支持小于900顶点的网格物体。如何理解900呢,其实就相当于900个顶点数据大小的物体,如果说使用Position、Normal和UV三种属性,那么你只能Batch300个顶点。整体缩放的物体不能被Batch,除非他们的缩放值相同。之前有一个客户做特效,使用Batch机制把面片合并,最终让所有面片共享一个纹理,这时候发现这些面片没有被Batch出来,导致运行游戏时大概放三个技能就10多个招套。对于非整体用户体,他们的Batch是需要很好利用到。

11:纹理合并优化
现在来看纹理合并,纹理合并就是为了特到Batch数量,合并物体首先需要合并工具,还要修改使用纹理的网格的UV,使他们使用纹理。合并纹理主要是参照Batch,提高渲染性能。但在合并材质后需要注意的是脚本访问Renderer被拷贝。/*安挡剔除,建议使用PVS技术。建议大家使用自定义shader,例如高光效果,高光效果可能不需要做一些入射线的检测,只是简单把他的值放大也可以模拟高光效果,从而减少一些消耗。
另外一个是用profiler,通过他给的数据进行针对性的优化。以上是跟大家介绍优化的内容,如何作出良好优化,一定要做好良好的规划,到后期就不会很麻烦,如果规划没有做好有可能会给程序带来很大压力,结果可能很不乐观。*/最后,要不断实验不断总结才能达到自己满意的效果。

12:
要想降低Drawcal的话,有如下两点小建议
(1)不要用Unity自带UI或者iGUI, 用NUI 或者EZ GUI
(2)创建好的GameObject不用了就最好及时 删除 / 设置active为false/移出屏幕 。 这几种方法都可以去掉该物体导致增加的Drawcall

13:

*********************************
*********************************
*********************************
最近一段时间一直在做Unity 在IOS设备上的资源优化,结合Unity的官方文档以及自己遇到的实际问题,我把自己认为一些重要的信息罗列在下面,并尽可能对将其量化,以方便更多需要做优化的朋友。
1、 角色
每个角色尽量使用一个 Skinned Mesh Renderer
这是因为当角色仅有一个 Skinned Mesh Renderer 时, Unity 会 使用可见性裁剪和包围体更新的方法来优化角色的运动,而这种优化只有在角色仅含有一个 Skinned Mesh Renderer 时才会启动。
角色 Material 数量
2-3 个
骨骼数量
小于 30 个
面片数量
300-1500
一般角色应该没有 IK 结点
这是因为角色的动作大多数都是事先设定好的,并不需要经过 IK 操作来进行实时计算( Rogdoll 除外),所以在模型导入时,不要将 IK 结点一起导入。
2、 静态实体
不要附加 Animation Component
在静态实体上附加 Animation 部件虽然对结果没有影响,但却会增加一定的 CPU 开销来调用这一组件,所以尽量去掉该组件。
网格顶点数
小于 500
UV 值范围尽量不要超过( 0, 1 )区间
尽量保证 UV 值不越界,这对于将来的纹理拼合优化很有帮助。
3、 地形
地形的分辨率大小
长宽均尽量小于 257 。这是因为地形太大,会造成大量顶点数据,给你的内存带宽造成一定的影响,在目前的 ios 设备中,内存带宽是非常有限的,需要尽量节省。同时,如果用 Unity 自带的地形,一定也要使用 Occlusion Culling ,因为 Unity 的刷地形工具虽然方便,但却是 framekiller ,刷过之后,你会发现 drawcall 增加的非常多。
混合纹理数量
不要超过 4 。地形的混合操作是很耗时的,应该尽量避免。能合并的纹理尽量合并。
4、 纹理
纹理格式
建议 png 或 tga 。不用转成 ios 硬件支持的 PVRTC 格式,因为 Unity 在发布时会帮你自动转的。
纹理尺寸
长宽小于 1024 。同时应该尽可能地小,够用就好,以保证纹理对内存带宽的影响达到最小。
支持 Mipmap
建议生成 Mipmap 。虽然这种做法会增加一些应用程序的大小,但在游戏运行时,系统会根据需求应用 Mipmap 来渲染,从而减少内存带宽。
检查 Alpha 值
如果纹理的 alpha 通道均为 1 ,则用 RGB 的 24 位纹理来代替 RGBA 的 32 位纹理。(据说 Unity 内部会进行自动检测)
5、 光源
光源“ Important ”个数
建议 1 个,一般为方向光。“ Important ”个数应该越小越少。个数越多, drawcall 越多。
Pixel Light 数目
1-2 个。
6、 粒子特效
屏幕上的最大粒子数
建议小于 200 个粒子。
每个粒子发射器发射的最大粒子数
建议不超过 50 个。
粒子大小
如果可以的话,粒子的 size 应该尽可能地小。因为 Unity 的粒子系统的 shader 无论是 alpha test 还是 alpha blending 都是一笔不小的开销。同时,对于非常小的粒子,建议粒子纹理去掉 alpha 通道。
尽量不要开启粒子的碰撞功能。
非常耗时。
7、 音频
游戏中播放时间较长的音乐(如背景音乐)
使用 .ogg 或 .mp3 的压缩格式。
较短音乐(如枪声)
使用 .wav 和 .aif 的未压缩音频格式。
8、 相机
裁剪平面
将远平面设置成合适的距离。远平面过大会将一些不必要的物体加入渲染,降低效率。
根据不同的物体设置不同的远裁剪平面
Unity 提供了可以根据不同的 layer 来设置不同的 view distance ,所以我们可以实现将物体进行分层,大物体层设置的可视距离大些,而小物体层可以设置地小些,另外,一些开销比较大的实体(如粒子系统)可以设置得更小些等等。
9、 碰撞
尽量不用 MeshCollider
如果可以的话,尽量不用 MeshCollider ,以节省不必要的开销。如果不能避免的话,尽量用减少 Mesh 的面片数,或用较少面片的代理体来代替。
10、 其他
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 降了,渲染速度也变慢了”的原因。

非运动物体尽量打上 Static 标签
Unity 在运行时会对 static 物体进行自动优化处理,所以应该尽可能将非运行实体勾上 static 标签。

场景中尽可能地使用 prefab
尽可能地使用 prefab 的实例化物体,以降低内存带宽的负担。检查实体的 PrefabType ,尽量将其变成 PrefabInstance ,而不是 ModelPrefabInstance 。
2015-01-31 22:52:00 zzxiang1985 阅读数 42236

  一共9招。

 

1 资源分离打包与加载

 

  游戏中会有很多地方使用同一份资源。比如,有些界面共用同一份字体、同一张图集,有些场景共用同一张贴图,有些怪物使用同一个Animator,等等。可以在制作游戏安装包时将这些公用资源从其它资源中分离出来,单独打包。比如若资源A和B都引用了资源C,则将C分离出来单独打一个bundle。在游戏运行时,如果要加载A,则先加载C;之后如果要加载B,因为C的实例已经在内存,所以只要直接加载B,让B指向C即可。如果打包时不将C从A和B分离出来,那么A的包里会有一份C,B的包里也会有一份C,冗余的C会将安装包撑大;并且在运行时,如果A和B都加载进内存,内存里就会有两个C实例,增大了内存占用。

 

  资源分离打包与加载是最有效的减小安装包体积与运行时内存占用的手段。一般打包粒度越细,这两个指标就越小;而且当两个renderQueue相邻的DrawCall使用了相同的贴图、材质和shader实例时,这两个DrawCall就可以合并。但打包粒度也并不是越细就越好。如果运行时要同时加载大量小bundle,那么加载速度将会非常慢——时间都浪费在协程之间的调度和多批次的小I/O上了;而且DrawCall合并不见得会提高性能,有时反而会降低性能,后文会提到。因此需要有策略地控制打包粒度。一般只分离字体和贴图这种体积较大的公用资源。

 

  可以用AssetDatabase.GetDependencies得知一份资源使用了哪些其它资源。

 

2  贴图透明通道分离,压缩格式设为ETC/PVRTC

 

  最初我们使用了DXT5作为贴图压缩格式,希望能减小贴图的内存占用,但很快发现移动平台的显卡是不支持硬件解压DXT5的。因此对于一张1024x1024大小的RGBA32贴图,虽然DXT5可将它从4MB压缩到1MB,但系统将它送进显卡之前,会先用CPU在内存里将它解压成4MB的RGBA32格式(软件解压),然后再将这4MB送进显存。于是在这段时间里,这张贴图就占用了5MB内存和4MB显存;而移动平台往往没有独立显存,需要从内存里抠一块作为显存,于是原以为只占1MB内存的贴图实际却占了9MB!

 

  所有不支持硬件解压的压缩格式都有这个问题。经过一番调研,我们发现安卓上硬件支持最广泛的格式是ETC,苹果上则是PVRTC。但这两种格式都是不带透明(Alpha)通道的。因此我们将每张原始贴图的透明通道都分离了出来,写进另一张贴图的红色通道里。这两张贴图都采用ETC/PVRTC压缩。渲染的时候,将两张贴图都送进显存。同时我们修改了NGUI的shader,在渲染时将第二张贴图的红色通道写到第一张贴图的透明通道里,恢复原来的颜色:

 

fixed4 frag (v2f i) : COLOR
{
    fixed4 col;
    col.rgb = tex2D(_MainTex, i.texcoord).rgb;
    col.a = tex2D(_AlphaTex, i.texcoord).r;
    return col * i.color;
}

 

  这样,一张4MB的1024x1024大小的RGBA32原始贴图,会被分离并压缩成两张0.5MB的ETC/PVRTC贴图(我们用的是ETC/PVRTC 4 bits)。它们渲染时的内存占用则是2x0.5+2x0.5=2MB。

 

3 关闭贴图的读写选项

 

  Unity中导入的每张贴图都有一个启用可读可写(Read/Write Enabled)的开关,对应的程序参数是TextureImporter.isReadable。选中贴图后可在Import Setting选项卡中看到这个开关。只有打开这个开关,才可以对贴图使用Texture2D.GetPixel,读取或改写贴图资源的像素,但这就需要系统在内存里保留一份贴图的拷贝,以供CPU访问。一般游戏运行时不会有这样的需求,因此我们对所有贴图都关闭了这个开关,只在编辑中做贴图导入后处理(比如对原始贴图分离透明通道)时打开它。这样,上文提到的1024x1024大小的贴图,其运行时的2MB内存占用又可以少一半,减小到1MB。

 

4 减少场景中的GameObject数量

 

  有一次我们将场景中的GameObject数量减少了近2万个,游戏在iPhone 3S上的内存占用立马减了20MB。这些GameObject虽然基本是在隐藏状态(activeInHierarchy为false),但仍然会占用不少内存。这些GameObject身上还挂载了不少脚本,每个GameObject中的每个脚本都要实例化,又是一比不菲的内存占用。因此后来我们规定场景中的GameObject数量不得超过1万,并且将GameObject数量列为每周版本的性能监测指标。

 

整理图集

 

  整理图集的主要目的是节省运行时内存(虽然有时也能起到合并DrawCall的作用)。从这个角度讲,显示一个界面时送进显存的图集尺寸之和是越小越好。一般有如下方法可以帮助我们做到这点:

 

  1)在界面设计上,尽量让美术将控件设计为可以做九宫格拉伸,即UISprite的类型为Sliced。这样美术就可以只切出一张小图,我们在Unity中将它拉大。当然,一个控件做九宫格也就意味着其顶点数量从4个增加到至少16个(九宫格的中心格子采用Tiled做平铺类型的话,顶点数会更多),构建DrawCall的开销会更大(见第6点),但一般只要DrawCall安排合理(同样见第6点)就不会有问题。

 

  2)同样是在界面设计上,尽量让美术将图案设计成对称的形式。这样切图的时候,美术就可以只切一部分,我们在Unity中将完整的图案拼出来。比如对一个圆形图案,美术可以只切出四分之一;对一张脸,美术可以只切出一半。不过,与第1)点类似,这个方法同样有其它性能代价——一个图案所对应的顶点数和GameObject数量都增多了。第4点已经提到,GameObject数量的增多有时也会显著占用更多内存。因此一般只对尺寸较大的图案采用这个方法。

 

  3)确保不要让不必要的贴图素材驻留内存,更不要在渲染时将无关的贴图素材送进显存。为此需要将图集按照界面分开,一般一张图集只放一个界面的素材,一个界面中的UISprite也不要使用别的界面的图集。假设界面A和界面B上都有一个小小的一模一样的金币图标,不要因为在制作时贪图方便,就让界面A的UISprite直接引用界面B中的金币素材;否则界面A显示的时候,会将整个界面B的图集也送进显存,而且只要A还在内存中,B的图集也会驻留内存。对于这种情况,应该在A和B的图集中各放一个一模一样的金币图标,A中的UISprite只使用A的图集,B中的UISprite只使用B的图集。

 

  不过,如果两个界面之间存在大量相同的素材,那么这两个界面就可以共用同一张图集。这样可以减少所有界面的总内存占用量。具体操作时需要根据美术的设计进行权衡。一般界面之间相同的通用的素材越多,程序的内存负担就越小。但界面之间相同的东西太多的话,美术效果可能就不生动,这是美术和程序之间又一个需要寻求平衡的地方。

 

  另外,数量庞大的图标资源(如物品图标)不要做在图集里,而应该采用UITexture。

 

  4)减少图集中的空白地方。图集中完全透明的像素和不透名的像素所占的内存空间其实是一样的。因此在素材量不变的情况下,要尽量减少图集中的空白。有时一张1024x1024的图集中,素材所占的面积还没超过一半,这时可以考虑将这张图集切成两张512x512的图集。(可能有人会问为什么不能做成一张1024x512的图集,这是因为iOS平台似乎要求送进显存的贴图一定是方形。)当然,两张不同图集的DrawCall是无法合并的,但这并不是什么问题(见第6点)。

 

  应该说,图集的整理在具体操作时并没有一成不变的标准,很多时候需要权衡利弊来最终决定如何整理,因为不管哪种措施都会有别的性能代价。

 

6 根据各个UI控件的设计安放Panel,隔开DrawCall

 

  有一次我们发现NGUI的UIPanel.LateUpdate函数的CPU开销非常大。仔细研究之后,发现是合并了太多的DrawCall所致,尤其是将运行时会运动变化的UI控件和静止不变的UI控件的DrawCall合在了一起。当一个UI控件(UIWidget)的位置、大小或颜色等属性发生变化时,UIPanel就需要重建这个控件所用的DrawCall,某些情况下还要重建Panel上的所有DrawCall。有时重建一个DrawCall会消耗不少CPU开销,它需要重新计算这个DrawCall上所有控件的顶点信息,包括顶点位置、UV和颜色等。如果很多控件都集中在同一个DrawCall上,那么只要一个控件有一点点变化,这个DrawCall上的所有控件的顶点就都要重新遍历一边;而我们的UI又大量采用了九宫格拉伸,使控件的顶点数量变得更多,因此重建一个DrawCall的开销就更大。

 

  因此我们将UI控件分组,将一段时间内会发生变化的控件——比如怪物头顶的血条和伤害跳字放在同一个Panel上,并且这个Panel上只有这些控件,其余基本不变化的控件就放在别的Panel上。这样两类控件就被隔开到不同的DrawCall不同的Panel中,当一个控件发生变化而导致DrawCall重建时,就不需要遍历那些没有变化的控件。因为在美术设计上,一段时间内在变化的控件总是少数,所以优化效果十分明显,节省的CPU占用率能达到25%。

 

  这种方法会增加一些DrawCall,但不会有什么影响。我们项目中前期曾经过于重视DrawCall数量的压缩,但后来发现增加几个DrawCall并不是那么可怕的事情。主程有一次甚至用Cocos2d-x做过试验,即使在500个DrawCall的情况下,动画依然可以跑得很流畅,相比之下贴图大小对流畅度的影响要大得多。

 

7 优化锚点内部逻辑,使其只在必要时更新

 

  在上一点优化了Panel的DrawCall重建效率之后,我们发现NGUI锚点自身的更新逻辑也会消耗不少CPU开销。即使是在控件静止不动的情况下,控件的锚点也会每帧更新(见UIWidget.OnUpdate函数),而且它的更新是递归式的,使CPU占用率更高。因此我们修改了NGUI的内部代码,使锚点只在必要时更新。一般只在控件初始化和屏幕大小发生变化时更新即可。不过这个优化的代价是控件的顶点位置发生变化的时候(比如控件在运动,或控件大小改变等),上层逻辑需要自己负责更新锚点。

 

8 降低贴图素材分辨率

 

  这一招说白了其实就是减小贴图素材的尺寸。比如对一张在原画里尺寸是100x80的贴图,我们将它导入Unity后会把它缩小到50x40,即缩小两倍。游戏实际使用的是缩小后的贴图。不过这一招是必然会显著降低美术品质的,美术立马会发现画面变得更模糊,因此一般不到程序撑不住的时候不会采用。

 

9 界面的延迟加载和定时卸载策略(暂未实施)

 

  如果一些界面的重要性较低,并且不常被使用,可以等到界面需要打开显示的时候才从bundle加载资源,并且在关闭时将自己卸载出内存,或者等过一段时间再卸载。不过这个方法有两个代价:一是会影响体验,玩家要求打开界面时,界面的显示会有延迟;二是更容易出bug,上层写逻辑时要考虑异步情况,当程序员要访问一个界面时,这个界面未必会在内存里。因此目前为止我们仍未实施该方案。目前只是进入一个新场景时,卸载上一个场景用到但新场景不会用到的界面。

 

  以上的9个方法中,4、5、6需要在一定程度上从策划和美术的角度考虑问题,并且需要持续保持监控以维护优化状态(因为在设计上总是会有新界面的需求或改动老界面的需求);其它都是一劳永逸的解决方案,只要实施稳定后,就不需要再在上面花费精力。不过2和8都是会降低美术品质的方法,尤其是8。如果美术对品质的降低程度实在忍不了的话,也可能不会允许采用这两个方法。

 

后记

 

后来又学到一招:

避免频繁调用GameObject.SetActive