Office及VBA技术交流

 找回密码
 立即注册
搜索
热搜: 活动 交友 discuz
查看: 6906|回复: 7

Unity 3D优化游戏运行性能的经验总结

  [复制链接]

该用户从未签到

18

主题

23

帖子

110

积分

注册会员

Rank: 2

积分
110
发表于 2016-1-29 23:29:09 | 显示全部楼层 |阅读模式
        
流畅的游戏玩法来自流畅的帧率,而我们即将推出的动作平台游戏《Shadow Blade》已经将在标准iPhone和iPad设备上实现每秒60帧视为一个重要目标。
以下是我们在紧凑的优化过程中提升游戏运行性能,并实现目标帧率时需要考虑的事项。
当基本游戏功能到位时,就要确保游戏运行表现能够达标。我们衡量游戏运行表现的一个基本工具是Unity内置分析器以及Xcode分析工具。使用Unity分析器来分析设备上的运行代码真是一项宝贵的功能。
我们总结了这种为将目标设备的帧率控制在60fps而进行衡量、调整、再衡量过程的中相关经验。

一、遇到麻烦时要调用“垃圾回收器”(Garbage Collector,无用单元收集程序,以下简称GC)
由于具有C/C++游戏编程背景,我们并不习惯无用单元收集程序的特定行为。确保自动清理你不用的内存,这种做法在刚开始时很好,但很快你就公发现自己的分析器经常显示CPU负荷过大,原因是垃圾回收器正在收集垃圾内存。这对移动设备来说尤其是个大问题。要跟进内存分配,并尽量避免它们成为优先数,以下是我们应该采取的主要操作:
1.移除代码中的任何字符串连接,因为这会给GC留下大量垃圾。
2.用简单的“for”循环代替“foreach”循环。由于某些原因,每个“foreach”循环的每次迭代会生成24字节的垃圾内存。一个简单的循环迭代10次就可以留下240字节的垃圾内存。
3.更改我们检查游戏对象标签的方法。用“if (go.CompareTag (“Enemy”)”来代替“if (go.tag == “Enemy”)” 。在一个内部循环调用对象分配的标签属性以及拷贝额外内存,这是一个非常糟糕的做法。
4.对象库很棒,我们为所有动态游戏对象制作和使用库,这样在游戏运行时间内不会动态分配任何东西,不需要的时候所有东西反向循环到库中。
5.不使用LINQ命令,因为它们一般会分配中间缓器,而这很容易生成垃圾内存。
二、谨慎处理高级脚本和本地引擎C++代码之间的通信开销。
所有使用Unity3D编写的游戏玩法代码都是脚本代码,在我们的项目中是使用Mono执行时间处理的C#代码。任何与引擎数据的通信需求都要有一个进入高级脚本语言的本地引擎代码的调用。这当然会产生它自己的开销,而尽量减少游戏代码中的这些调用则要排在第二位。
1.在这一情景中四处移动对象要求来自脚本代码的调用进入引擎代码,这样我们就会在游戏玩法代码的一个帧中缓存某一对象的转换需求,并一次仅向引擎发送一个请求,以便减少调用开销。这种模式也适用于其他相似的地方,而不仅局限于移动和旋转对象。
2.将引用本地缓存到元件中会减少每次在一个游戏对象中使用 “GetComponent” 获取一个元件引用的需求,这是调用本地引擎代码的另一个例子。
三、物理效果
1.将物理模拟时间步设置到最小化状态。在我们的项目中就不可以将让它低于16毫秒。
2.减少角色控制器移动命令的调用。移动角色控制器会同步发生,每次调用都会耗损极大的性能。我们的做法是缓存每帧的移动请求,并且仅运用一次。
3.修改代码以免依赖“ControllerColliderHit” 回调函数。这证明这些回调函数处理得并不十分迅速。
4.面对性能更弱的设备,要用skinned mesh代替physics cloth。cloth参数在运行表现中发挥重要作用,如果你肯花些时间找到美学与运行表现之间的平衡点,就可以获得理想的结果。
5.在物理模拟过程中不要使用ragdolls,只有在必要时才让它生效。
6.要谨慎评估触发器的“onInside”回调函数,在我们的项目中,我们尽量在不依赖它们的情况下模拟逻辑。
7.使用层次而不是标签。我们可以轻松为对象分配层次和标签,并查询特定对象,但是涉及碰撞逻辑时,层次至少在运行表现上会更有明显优势。更快的物理计算和更少的无用分配内存是使用层次的基本原因。
8.千万不要使用Mesh对撞机。
9.最小化碰撞检测请求(例如ray casts和sphere checks),尽量从每次检查中获得更多信息。
四、让AI代码更迅速
我们使用AI敌人来阻拦忍者英雄,并同其过招。以下是与AI性能问题有关的一些建议:
1.AI逻辑(例如能见度检查等)会生成大量物理查询。可以让AI更新循环设置低于图像更新循环,以减少CPU负荷。
五、最佳性能表现根本就不是来自代码!
没有发生什么情况的时候,就说明性能良好。这是我们关闭一切不必要之物的基本原则。我们的项目是一个侧边横向卷轴动作游戏,所以如果不具有可视性时,就可以关闭许多动态关卡物体。
1.使用细节层次的定制关卡将远处的敌人AI关闭。
2.移动平台和障碍,当它们远去时其物理碰撞机也会关闭。
3.Unity内置的“动画挑选”系统可以用来关闭未被渲染对象的动画。
4.所有关卡内的粒子系统也可以使用同样的禁用机制。
六、回调函数!那么空白的回调函数呢?
要尽量减少Unity回调函数。即使敌人回调函数存在性能损失。没有必要将空白的回调函数留在代码库中(有时候介于大量代码重写和重构之间)。
七、让美术人员来救场
在程序员抓耳挠腮,绞尽脑汁去想该如何让每秒运行更多帧时,美术人员总能神奇地派上大用场。
1.共享游戏对象材料,令其在Unity中处于静止状态,可以让它们绑定在一起,由此产生的简化绘图调用是呈现良好移动运行性能的重要元素。
2.纹理地图集对UI元素来说尤其有用。
3.方形纹理以及两者功率的合理压缩是必不可少的步骤。
4.我们的美术人员移除了所有远处背景的网格,并将其转化为简单的2D位面。
5.光照图非常有价值。
6.我们的美术人员在一些关口移除了额外顶点。
7.使用合理的纹理mip标准是一个好主意(游戏邦注:要让不同分辨率的设备呈现良好的帧率时尤其如此)。
8.结合网格是美术人员可以发挥作用的另一个操作。
9.我们的动画师尽力让不同角色共享动画。
10.要找到美学/性能之间的平衡,就免不了许多粒子效果的迭代。减少发射器数量并尽量减少透明度需求也是一大挑战。
八、要减少内存使用
使用大内存当然会对性能产生负面影响,但在我们的项目中,我们的iPod由于超过内存上限而遭遇了多次崩溃事件。我们的游戏中最耗内存的是纹理。
1.不同设备要使用不同的纹理大小,尤其是UI和大型背景中的纹理。《Shadow Blade》使用的是通用型模板,但如果在启动时检测到设备大小和分辨率,就会载入不同资产。
2.我们要确保未使用的资产不会载入内存。我们必须迟一点在项目中找到仅被一个预制件实例引用,并且从未完全载入内存中实例化的资产。
3.去除网格中的额外多边形也能实现这一点。
4.我们应该重建一些资产的生周期管理。例如,调整主菜单资产的加载/卸载时间,或者关卡资产、游戏音乐的有效期限。
5.每个关卡都要有根据其动态对象需求而量身定制的特定对象库,并根据最小内存需求来优化。对象库可以灵活一点,在开发过程中包含大量对象,但知道游戏对象需求后就要具体一点。
6.保持声音文件在内存的压缩状态也是必要之举。
加强游戏运行性能是一个漫长而具有挑战性的过程,游戏开发社区所分享的大量知识,以及Unity提供的出色分析工具为《Shadow Blade》实现目标运行性能提供了极大帮助。
回复

使用道具 举报

该用户从未签到

18

主题

23

帖子

110

积分

注册会员

Rank: 2

积分
110
 楼主| 发表于 2016-2-1 20:03:06 | 显示全部楼层
一、程序方面
  01、务必删除脚本中为空或不需要的默认方法;
  02、只在一个脚本中使用OnGUI方法;
  03、避免在OnGUI中对变量、方法进行更新、赋值,输出变量建议在Update内;
  04、同一脚本中频繁使用的变量建议声明其为全局变量,脚本之间频繁调用的变量或方法建议声明为全局静态变量或方法;
  05、不要去频繁获取组件,将其声明为全局变量;
  06、数组、集合类元素优先使用Array,其次是List;
  07、脚本在不使用时脚本禁用之,需要时再启用;
  08、可以使用Ray来代替OnMouseXXX类方法;
  09、需要隐藏/显示或实例化来回切换的对象,尽量不要使用SetActiveRecursively或active,而使用将对象远远移出相机范围和移回原位的做法;
  10、尽量少用模运算和除法运算,比如a/5f,一定要写成a*0.2f。
  11、对于不经常调用或更改的变量或方法建议使用Coroutines & Yield;
  12、尽量直接声明脚本变量,而不使用GetComponent来获取脚本;
iPhone
  13、尽量使用整数数字,因为iPhone的浮点数计算能力很差;
  14、不要使用原生的GUI方法;
  15、不要实例化(Instantiate)对象,事先建好对象池,并使用Translate“生成”对象;
 

二、模型方面
  01、合并使用同贴图的材质球,合并使用相同材质球的Mesh;
  02、角色的贴图和材质球只要一个,若必须多个则将模型离分离为多个部分;
  02、骨骼系统不要使用太多;
  03、当使用多角色时,将动画单独分离出来;
  04、使用层距离来控制模型的显示距离;
  05、阴影其实包含两方面阴暗和影子,建议使用实时影子时把阴暗效果烘焙出来,不要使用灯光来调节光线阴暗。
  06、少用像素灯和使用像素灯的Shader;
  08、如果硬阴影可以解决问题就不要用软阴影,并且使用不影响效果的低分辨率阴影;
  08、实时阴影很耗性能,尽量减小产生阴影的距离;
  09、允许的话在大场景中使用线性雾,这样可以使远距离对象或阴影不易察觉,因此可以通过减小相机和阴影距离来提高性能;
  10、使用圆滑组来尽量减少模型的面数;
  11、项目中如果没有灯光或对象在移动那么就不要使用实时灯光;
  12、水面、镜子等实时反射/折射的效果单独放在Water图层中,并且根据其实时反射/折射的范围来调整;
  13、碰撞对效率的影响很小,但碰撞还是建议使用Box、Sphere碰撞体;
  14、建材质球时尽量考虑使用Substance;
  15、尽量将所有的实时反射/折射(如水面、镜子、地板等等)都集合成一个面;
  16、假反射/折射没有必要使用过大分辨率,一般64*64就可以,不建议超过256*256;
  17、需要更改的材质球,建议实例化一个,而不是使用公共的材质球;
  18、将不须射线或碰撞事件的对象置于IgnoreRaycast图层;
  19、将水面或类似效果置于Water图层
  20、将透明通道的对象置于TransparentFX图层;
  21、养成良好的标签(Tags)、层次(Hieratchy)和图层(Layer)的条理化习惯,将不同的对象置于不同的标签或图层,三者有效的结合将很方便的按名称、类别和属性来查找;
  22、通过Stats和Profile查看对效率影响最大的方面或对象,或者使用禁用部分模型的方式查看问题到底在哪儿;
  23、使用遮挡剔除(Occlusion Culling)处理大场景,一种较原生的类LOD技术,并且能够“分割”作为整体的一个模型。

三、其它
  场景中如果没有使用灯光和像素灯,就不要使用法线贴图,因为法线效果只有在有光源(Direct Light/Point Light/Angle Light/Pixel Light)的情况下才有效果。
回复 支持 反对

使用道具 举报

该用户从未签到

18

主题

23

帖子

110

积分

注册会员

Rank: 2

积分
110
 楼主| 发表于 2016-2-1 21:29:30 | 显示全部楼层
最简单的优化建议:

1.PC平台的话保持场景中显示的顶点数少于200K~3M,移动设备的话少于10W,一切取决于你的目标GPU与CPU。
2.如果你用U3D自带的SHADER,在表现不差的情况下选择Mobile或Unlit目录下的。它们更高效。
3.尽可能共用材质。
4.将不需要移动的物体设为Static,让引擎可以进行其批处理。
5.尽可能不用灯光。
6.动态灯光更加不要了。
7.尝试用压缩贴图格式,或用16位代替32位。
8.如果不需要别用雾效(fog)
9.尝试用OcclusionCulling,在房间过道多遮挡物体多的场景非常有用。若不当反而会增加负担。
10.用天空盒去“褪去”远处的物体。
11.shader中用贴图混合的方式去代替多重通道计算。
12.shader中注意float/half/fixed的使用。
13.shader中不要用复杂的计算pow,sin,cos,tan,log等。
14.shader中越少Fragment越好。
15.注意是否有多余的动画脚本,模型自动导入到U3D会有动画脚本,大量的话会严重影响消耗CPU计算。
16.注意碰撞体的碰撞层,不必要的碰撞检测请舍去。


1.为什么需要针对CPU(中央处理器)与GPU(图形处理器)优化?

CPU和GPU都有各自的计算和传输瓶颈,不同的CPU或GPU他们的性能都不一样,所以你的游戏需要为你目标用户的CPU与GPU能力进行针对开发。


2.CPU与GPU的限制

GPU一般具有填充率(Fillrate)和内存带宽(Memory Bandwidth)的限制,如果你的游戏在低质量表现的情况下会快很多,那么,你很可能需要限制你在GPU的填充率。

CPU一般被所需要渲染物体的个数限制,CPU给GPU发送渲染物体命令叫做DrawCalls。一般来说DrawCalls数量是需要控制的,在能表现效果的前提下越少越好。通常来说,电脑平台上DrawCalls几千个之内,移动平台上DrawCalls几百个之内。这样就差不多了。当然以上并不是绝对的,仅作一个参考。

往往渲染(Rendering)并不是一个问题,无论是在GPU和CPU上。很可能是你的脚本代码效率的问题,用Profiler查看下。

关于Profiler介绍:http://docs.unity3d.com/Documentation/Manual/Profiler.html

需要注意的是:
在GPU中显示的RenderTexture.SetActive()占用率很高,是因为你同时打开了编辑窗口的原因,而不是U3D的BUG。

3.关于顶点数量和顶点计算

CPU和GPU对顶点的计算处理都很多。GPU中渲染的顶点数取决于GPU性能和SHADER的复杂程度,一般来说,每帧之内,在PC上几百万顶点内,在移动平台上不超过10万顶点。

CPU中的计算主要是在蒙皮骨骼计算,布料模拟,顶点动画,粒子模拟等。GPU则在各种顶点变换、光照、贴图混合等。

【个人认为,具体还是看各位的项目需求,假设你项目的是3d游戏。你游戏需要兼容低配置的硬件、流畅运行、控制硬件发热的话,还要达到一定效果(LIGHTMAP+雾效),那么顶点数必定不能高。此时同屏2W顶点我认为是个比较合适的数目,DRAWCALL最好低于70。另,控制发热请控制最高上限的帧率,流畅的话,帧率其实不需要太高的。】



4.针对CPU的优化——减少DRAW CALL 的数量

为了渲染物体到显示器上,CPU需要做一些工作,如区分哪个东西需要渲染、区分开物体是否受光照影响、使用哪个SHADER并且为SHADER传参、发送绘图命令告诉显示驱动,然后发送命令告诉显卡删除等这些。

假设你有一个上千三角面的模型却用上千个三角型模型来代替,在GPU上花费是差不多的,但是在CPU上则是极其不一样,消耗会大很多很多。为了让CPU更少的工作,需要减少可见物的数目:

a.合并相近的模型,手动在模型编辑器中合并或者使用UNITY的Draw call批处理达到相同效果(Draw call batching)。具体方法和注意事项查看以下链接:

Draw call batching : http://docs.unity3d.com/Document ... awCallBatching.html


b.在项目中使用更少的材质(material),将几个分开的贴图合成一个较大的图集等方式处理。

如果你需要通过脚本来控制单个材质属性,需要注意改变Renderer.material将会造成一份材质的拷贝。因此,你应该使用Renderer.sharedMaterial来保证材质的共享状态。

有一个合并模型材质不错的插件叫Mesh Baker,大家可以考虑试下。

c.尽量少用一些渲染步骤,例如reflections,shadows,per-pixel light 等。

d.Draw call batching的合并物体,会使每个物体(合并后的物体)至少有几百个三角面。

假设合并的两个物体(手动合并)但不共享材质,不会有性能表现上的提升。多材质的物体相当于两个物体不用一个贴图。所以,为了提升CPU的性能,你应该确保这些物体使用同样的贴图。

另外,用灯光将会取消(break)引擎的DRAW CALL BATCH,至于为什么,查看以下:

Forward Rendering Path Details:
http://docs.unity3d.com/Document ... rwardRendering.html

e.使用相关剔除数量直接减少Draw Call数量,下文有相关提及。


5.优化几何模型

最基本的两个优化准则:
a.不要有不必要的三角面。
b.UV贴图中的接缝和硬边越少越好。

需要注意的是,图形硬件需要处理顶点数并跟硬件报告说的并不一样。不是硬件说能渲染几个点就是几个点。模型处理应用通展示的是几何顶点数量。例如,一个由一些不同顶点构成的模型。在显卡中,一些集合顶点将会被分离(split)成两个或者更多逻辑顶点用作渲染。如果有法线、UV坐标、顶点色的话,这个顶点必须会被分离。所以在游戏中处理的实际数量显然要多很多。


6.关于光照

若不用光肯定是最快的。移动端优化可以采用用光照贴图(Lightmapping)去烘培一个静态的贴图,以代替每次的光照计算,在U3D中只需要非常短的时间则能生成。这个方法能大大提高效率,而且有着更好的表现效果(平滑过渡处理,还有附加阴影等)。

在移动设备上和低端电脑上尽量不要在场景中用真光,用光照贴图。这个方法大大节省了CPU和GPU的计算,CPU得到了更少的DRAWCALL,GPU则需要更少顶点处理和像素栅格化。

Lightmapping : http://docs.unity3d.com/Documentation/Manual/Lightmapping.html


7.对GPU的优化——图片压缩和多重纹理格式

Compressed Textures(图片压缩):

http://docs.unity3d.com/Document ... lass-Texture2D.html

图片压缩将降低你的图片大小(更快地加载更小的内存跨度(footprint)),而且大大提高渲染表现。压缩贴图比起未压缩的32位RGBA贴图占用内存带宽少得多。

之前U3D会议还听说过一个优化,贴图尽量都用一个大小的格式(512 * 512 , 1024 * 1024),这样在内存之中能得到更好的排序,而不会有内存之间空隙。这个是否真假没得到过测试。

MIPMAps(多重纹理格式):

http://docs.unity3d.com/Document ... lass-Texture2D.html

跟网页上的略缩图原理一样,在3D游戏中我们为游戏的贴图生成多重纹理贴图,远处显示较小的物体用小的贴图,显示比较大的物体用精细的贴图。这样能更加有效的减少传输给GPU中的数据。


8.LOD 、 Per-Layer Cull Distances 、 Occlusion Culling

LOD (Level Of Detail) 是很常用的3D游戏技术了,其功能理解起来则是相当于多重纹理贴图。在以在屏幕中显示模型大小的比例来判断使用高或低层次的模型来减少对GPU的传输数据,和减少GPU所需要的顶点计算。

摄像机分层距离剔除(Per-Layer Cull Distances):为小物体标识层次,然后根据其距离主摄像机的距离判断是否需要显示。

遮挡剔除(Occlusion Culling)其实就是当某个物体在摄像机前被另外一个物体完全挡住的情况,挡住就不发送给GPU渲染,从而直接降低DRAW CALL。不过有些时候在CPU中计算其是否被挡住则会很耗计算,反而得不偿失。

以下是这几个优化技术的相关使用和介绍:

Level Of Detail :
http://docs.unity3d.com/Documentation/Manual/LevelOfDetail.html

Per-Layer Cull Distances :
http://docs.unity3d.com/Document ... rCullDistances.html

Occlusion Culling :
http://docs.unity3d.com/Document ... clusionCulling.html


9.关于Realtime Shadows(实时阴影)

实时阴影技术非常棒,但消耗大量计算。为GPU和CPU都带来了昂贵的负担,细节的话参考下面:

http://docs.unity3d.com/Documentation/Manual/Shadows.html


10.对GPU优化:采用高效的shader

a.需要注意的是有些(built-in)Shader是有mobile版本的,这些大大提高了顶点处理的性能。当然也会有一些限制。

b.自己写的shader请注意复杂操作符计算,类似pow,exp,log,cos,sin,tan等都是很耗时的计算,最多只用一次在每个像素点的计算。不推荐你自己写normalize,dot,inversesqart操作符,内置的肯定比你写的好。

c.需要警醒的是alpha test,这个非常耗时。

d.浮点类型运算:精度越低的浮点计算越快。

在CG/HLSL中--

float :32位浮点格式,适合顶点变换运算,但比较慢。
half:16位浮点格式,适合贴图和UV坐标计算,是highp类型计算的两倍。
fixed: 10位浮点格式,适合颜色,光照,和其他。是highp格式计算的四倍。

写Shader优化的小提示:
http://docs.unity3d.com/Document ... derPerformance.html


11.另外的相关优化:

a.对Draw Call Batching的优化
http://docs.unity3d.com/Document ... awCallBatching.html

b.对Rendering Statistics Window的说明和提示:
http://docs.unity3d.com/Document ... ringStatistics.html

c.角色模型的优化建议
用单个蒙皮渲染、尽量少用材质、少用骨骼节点、移动设备上角色多边形保持在300~1500内(当然还要看具体的需求)、PC平台上1500~4000内(当然还要看具体的需求)。

http://docs.unity3d.com/Document ... izedCharacters.html
回复 支持 反对

使用道具 举报

  • TA的每日心情
    开心
    2016-3-4 07:46
  • 签到天数: 3 天

    [LV.2]偶尔看看I

    56

    主题

    124

    帖子

    736

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    736
    发表于 2016-2-2 20:27:12 | 显示全部楼层
    1. 尽量避免每帧处理,可以每隔几帧处理一次
    比如:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    function Update() { DoSomeThing(); }
    1

    function Update() { DoSomeThing(); }





    可改为每5帧处理一次:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    function Update() { if(Time.frameCount % 5 == 0) { DoSomeThing(); } }
    1

    function Update() { if(Time.frameCount % 5 == 0) { DoSomeThing(); } }





    2. 定时重复处理用InvokeRepeating 函数实现
    比如,启动0.5秒后每隔1秒执行一次 DoSomeThing 函数:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    function Start() { InvokeRepeating("DoSomeThing", 0.5, 1.0); } CancelInvoke("你调用的方法"); 停止InvokeRepeating
    1
    2

    function Start() { InvokeRepeating("DoSomeThing", 0.5, 1.0); }
    CancelInvoke("你调用的方法"); 停止InvokeRepeating





    3. 优化 Update,FixedUpdate, LateUpdate 等每帧处理的函数,函数里面的变量尽量在头部声明。
    比如:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    function Update() { var pos: Vector3 = transform.position; }
    1

    function Update() { var pos: Vector3 = transform.position; }





    可改为
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    private var pos: Vector3; function Update(){ pos = transform.position; }
    1

    private var pos: Vector3; function Update(){ pos = transform.position; }





    4. 主动回收垃圾
    给某个 GameObject 绑上以下的代码:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    function Update() { if(Time.frameCount % 50 == 0) { System.GC.Collect(); } }
    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的内存,如果你做了101024的图集,你的界面上刚好都只用了每个图集里面的一张小图,那么再次抱歉你的内存直接飙40M。意思是任何一个4096的图片,不管是图集还是texture,他都占用4*4=16M
    ====================================================================分割线=====================================================
    1、在使用数组或ArrayList对象时应当注意
    本帖隐藏的内容[C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    length=myArray.Length;  for(int i=0;i<length;i++)  {    }
    1
    2
    3
    4
    5

    length=myArray.Length;  
    for(int i=0;i<length;i++)  
    {  

    }





    避免
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    for(int i=0;i<myArray.Length;i++)  {    }
    1
    2
    3
    4

    for(int i=0;i<myArray.Length;i++)  
    {  

    }





    2、少使用临时变量,特别是在Update OnGUI等实时调用的函数中。
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    void Update()  {     Vector3 pos;     pos=transform.position;  }
    1
    2
    3
    4
    5

    void Update()  
    {  
       Vector3 pos;  
       pos=transform.position;  
    }





    可以改为:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    private Vector3 pos;  void Update()  {     pos=transform.position;  }
    1
    2
    3
    4
    5

    private Vector3 pos;  
    void Update()  
    {  
       pos=transform.position;  
    }





    3、如果可能,将GameObject上不必要的脚本disable掉。
    如果你有一个大的场景在你的游戏中,并且敌方的位置在数千米意外,
    这时你可以disable你的敌方AI脚本直到它们接近摄像机为止。
    一个好的途径来开启或关闭GameObject是使用SetActiveRecursively(false),并且球形或盒型碰撞器设为trigger。
    4、删除空的Update方法。
    当通过Assets目录创建新的脚本时,脚本里会包括一个Update方法,当你不使用时删除它。
    5、引用一个游戏对象的最合乎逻辑的组件。
    有人可能会这样写someGameObject.transform,gameObject.rigidbody.transform.gameObject.rigidbody.transform,但是这样做了一些不必要的工作,
    你可以在最开始的地方引用它,像这样:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    privateTransform myTrans;void Start(){    myTrans=transform;}
    1
    2
    3
    4
    5

    privateTransform myTrans;
    void Start()
    {
        myTrans=transform;
    }





    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是非常平常的一件事情,以下代码看起来很简单:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    transform.localPosition += new Vector3 ( 10.0f * Time.deltaTime, 0.0f, 0.0f );
    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等于:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    Vector3 offset = new Vector3( my.localPosition.x * parent.lossyScale.x, my.localPosition.y * parent.lossyScale.y, my.localPosition.z * parent.lossyScale.z );Vector3 worldPosition = parent.position + parent.rotation * offset;
    1

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





    换句话说,上面这种直接操作localPosition的方式是在没有考虑scale计算的时候进行的,为了解决这个问题,unity3d提供了Translate函数,
    所以正确的做法应该是:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    transform.Translate ( 10.0f * Time.deltaTime, 0.0f, 0.0f );
    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的类型,如:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    public float foobar = 1.0f;
    1

    public float foobar = 1.0f;





    这样,这个变量不仅会在Inspector中出现,还可以在animation view中进行操作,生成AnimationClip供我们通过AnimationComponent调用。
    范例:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    public class TestCurve : MonoBehaviour{public float foobar = 0.0f;IEnumerator Start (){yield return new WaitForSeconds (2.0f);animation.Play("foobar_op"); InvokeRepeating ( "LogFoobar", 0.0f, 0.2f ); yield return new WaitForSeconds (animation["foobar_op"].length);CancelInvoke ("LogFoobar");} void LogFoobar (){ Debug.Log("foobar = " + foobar); }}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    public class TestCurve : MonoBehaviour
    {
    public float foobar = 0.0f;
    IEnumerator Start ()
    {
    yield return new WaitForSeconds (2.0f);
    animation.Play("foobar_op");
    InvokeRepeating ( "LogFoobar", 0.0f, 0.2f );
    yield return new WaitForSeconds (animation["foobar_op"].length);
    CancelInvoke ("LogFoobar");
    }
    void LogFoobar ()
    {
    Debug.Log("foobar = " + foobar); }}





    2.GetComopnent<T> 可以取父类类型
    Unity3D 允许我们对MonoBehavior做派生,所以你可能有以下代码:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    public class foo : MonoBehaviour { ...} public class bar : foo { ...}
    1

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





    假设我们现在有A,B两个GameObject, A包含foo, B包含bar, 当我们写
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    foo comp1 = A.GetComponent&lt;foo&gt;();bar comp2 = B.GetComponent&lt;bar&gt;();
    1

    foo comp1 = A.GetComponent&lt;foo&gt;();bar comp2 = B.GetComponent&lt;bar&gt;();





    可以看到comp1, comp2都得到了应得的Component。那如果我们对B的操作改成:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    foo comp2 = B.GetComponent&lt;foo&gt;();
    1

    foo comp2 = B.GetComponent&lt;foo&gt;();





    答案是comp2还是会返回bar Component并且转换为foo类型。你同样可以用向下转换得到有效变量:
    bar comp2_bar = comp2 as bar;
    合理利用GetComponent<base_type>()可以让我们设计Component的时候耦合性更低。
    3.Invoke, yield 等函数会受 Time.timeScale 影响
    Unity3D提供了一个十分方便的调节时间的函数Time.timeScale。对于初次使用Unity3D的使用者,
    会误导性的认为Time.timeScale同样可以适用于游戏中的暂停(Pause)和开始(Resume)。
    所以很多人有习惯写:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    Time.timeScale = 0.0f
    1

    Time.timeScale = 0.0f





    对于游戏的暂停/开始,是游戏系统设计的一部分,而Time.timeScale不不是用于这个部分的操作。
    正确的做法应该是搜集需要暂停的脚本或 GameObject,
    通过设置他们的enabled = false 来停止他们的脚本活动或者通过特定函数来设置这些物件暂停时需要关闭那些操作。
    Time.timeScale 更多的是用于游戏中慢镜头的播放等操作,在服务器端主导的游戏中更应该避免此类操作。
    值得一提的是,Unity3D的许多时间相关的函数都和 timeScale挂钩,而timeScale = 0.0f将使这些函数或动画处于完全停止的状态,这也是为什么它不适合做暂停操作的主要原因。
    这里列出受timeScale影响的一些主要函数和Component:
    MonoBehaviour.Invoke(…)
    MonoBehaviour.InvokeRepeating(…)
    yield WaitForSeconds(…)
    GameObject.Destroy(…)
    Animation Component
    Time.time, Time.deltaTime

    4.Coroutine 和 IEnumerator的关系
    初写Unity3D C#脚本的时候,我们经常会犯的错误是调用Coroutine函数忘记使用StartCoroutine的方式。如:
    TestCoroutine.cs
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    IEnumerator CoLog () { yield return new WaitForSeconds (2.0f); Debug.Log("hello foobar");}
    1

    IEnumerator CoLog () { yield return new WaitForSeconds (2.0f); Debug.Log("hello foobar");}





    当我们用以下代码去调用上述函数:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    TestCoroutine testCo = GetComponent&lt;TestCoroutine&gt;();testCo.CoLog ();testCo.StartCoroutine ( "CoLog" );
    1

    TestCoroutine testCo = GetComponent&lt;TestCoroutine&gt;();testCo.CoLog ();testCo.StartCoroutine ( "CoLog" );





    那么testCo.CoLog()的调用将不会起任何作用。
    5.StartCoroutine, InvokeRepeating 和其调用者关联
    通常我们只在一份GameObject中去调用StartCoroutine或者InvokeRepeating,
    我们写:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    StartCoroutine ( Foobar() );InvokeRepeating ( "Foobar", 0.0f, 0.1f );
    1

    StartCoroutine ( Foobar() );InvokeRepeating ( "Foobar", 0.0f, 0.1f );





    所以如果这个GameObject被disable或者destroy了,这些coroutine和invokes将会被取消。就好比我们手动调用:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    StopAllCoroutines ();CancelInvoke ();
    1

    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中。
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    // class TestCortouinepublic class TestCoroutine : MonoBehaviour{public IEnumerator CoLog ( string _name ) {Debug.Log(_name + " hello foobar 01");yield return new WaitForSeconds (2.0f); Debug.Log(_name + " hello foobar 02"); }} // component attached on GameObject Apublic class A: MonoBehaviour { public GameObject B;  void Start (){ TestCoroutine compB = B.GetComponent&lt;TestCoroutine&gt;();  // GOOD, thread state in B // same as: compB.StartCoroutine ( "CoLog", "B" ); compB.StartCoroutine ( compB.CoLog("B") );// BAD, thread state in A StartCoroutine ( compB.CoLog("A") ); Debug.Log("Bye bye A, we'll miss you"); Destroy(gameObject); // T_T I don't want to die... }}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    // class TestCortouine
    public class TestCoroutine : MonoBehaviour
    {
    public IEnumerator CoLog ( string _name )
    {
    Debug.Log(_name + " hello foobar 01");
    yield return new WaitForSeconds (2.0f);
    Debug.Log(_name + " hello foobar 02"); }}
    // component attached on GameObject A
    public class A: MonoBehaviour
    {
    public GameObject B;  void Start ()
    { TestCoroutine compB = B.GetComponent&lt;TestCoroutine&gt;();  
    // GOOD, thread state in B
    // same as: comp
    B.StartCoroutine ( "CoLog", "B" );
    compB.StartCoroutine ( compB.CoLog("B") );
    // BAD, thread state in A
    StartCoroutine ( compB.CoLog("A") );
    Debug.Log("Bye bye A, we'll miss you");
    Destroy(gameObject);
    // T_T I don't want to die... }}





    以上代码,得到的结果将会是:
    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中,有利于脚本整体效率的提升。
    我们可以从这两个脚本中看到区别:
    Update_01.cs
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    public class Update_01 : MonoBehaviour { void Start () {} void Update () {}}
    1

    public class Update_01 : MonoBehaviour { void Start () {} void Update () {}}





    Update_02.cs
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    public class Update_02 : MonoBehaviour { }
    1
    2

    public class Update_02 : MonoBehaviour {
    }





    ===========================================分割线==============
    1.减少固定增量时间
    将固定增量时间值设定在0.04-0.067区间(即,每秒15-25帧)。您可以通过Edit->Project Settings->Time来改变这个值。这样做降低了FixedUpdate函数被调用的频率以及物理引擎执行碰撞检测与刚体更新的频率。如果您使用了较低的固定增量时间,并且在主角身上使用了刚体部件,那么您可以启用插值办法来平滑刚体组件。
    2.减少GetComponent的调用使用 GetComponent或内置组件访问器会产生明显的开销。您可以通过一次获取组件的引用来避免开销,并将该引用分配给一个变量(有时称为”缓存”的引用)。
    例如,如果您使用如下的代码:
    [JavaScript] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    function Update (){transform.Translate(0, 1, 0);}
    1
    2
    3
    4

    function Update ()
    {
    transform.Translate(0, 1, 0);
    }





    通过下面的更改您将获得更好的性能:
    [JavaScript] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    var myTransform : Transform;function Awake () {myTransform = transform;}function Update () {myTransform.Translate(0, 1, 0);}
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    var myTransform : Transform;

    function Awake () {

    myTransform = transform;

    }

    function Update () {

    myTransform.Translate(0, 1, 0);

    }





    3.避免分配内存
    您应该避免分配新对象,除非你真的需要,因为他们不再在使用时,会增加垃圾回收系统的开销。
    您可以经常重复使用数组和其他对象,而不是分配新的数组或对象。这样做好处则是尽量减少垃圾的回收工作。
    同时,在某些可能的情况下,您也可以使用结构(struct)来代替类(class)。
    这是因为,结构变量主要存放在栈区而非堆区。因为栈的分配较快,并且不调用垃圾回收操作,所以当结构变量比较小时可以提升程序的运行性能。
    但是当结构体较大时,虽然它仍可避免分配/回收的开销,而它由于”传值”操作也会导致单独的开销,实际上它可能比等效对象类的效率还要低。
    4.最小化GUI
    使用GUILayout 函数可以很方便地将GUI元素进行自动布局。然而,这种自动化自然也附带着一定的处理开销。
    您可以通过手动的GUI功能布局来避免这种开销。
    此外,您也可以设置一个脚本的useGUILayout变量为 false来完全禁用GUI布局:
    [JavaScript] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    function Awake () {useGUILayout = false;}
    1
    2
    3

    function Awake () {
    useGUILayout = false;
    }





    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。
    因此,在普通帧的间隔期进行垃圾回收有时候是一个不错的选择。
    通常,这种做法会让回收操作执行的更加频繁(有些回收操作并不是严格必须进行的),
    但它们可以快速处理并且对游戏的影响很小:
    [C#] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    if (Time.frameCount % 30 == 0){System.GC.Collect();}
    1
    2
    3
    4

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





    但是,您应该小心地使用这种技术,并且通过检查Profiler来确保这种操作确实可以降低您游戏的垃圾回收时间
    如果堆比较大,则进行缓慢且不频繁的垃圾回收
    这一策略适合于那些内存分配 (和回收)相对不频繁,并且可以在游戏停顿期间进行处理的游戏。
    如果堆足够大,但还没有大到被系统关掉的话,这种方法是比较适用的。
    但是,Mono运行时会尽可能地避免堆的自动扩大。
    因此,您需要通过在启动过程中预分配一些空间来手动扩展堆(ie,你实例化一个纯粹影响内存管理器分配的”无用”对象):
    [JavaScript] [color=#336699 !important]纯文本查看 [color=#336699 !important]复制代码
    function Start() <br>{<br>var tmp = new System.Object[1024];<br>// make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks<br>for (var i : int = 0; i < 1024; i++)<br>tmp = new byte[1024];<br>// release reference<br>tmp = null;<br>}
    1
    2
    3
    4
    5
    6
    7
    8
    9

    function Start() <br>
    {<br>
    var tmp = new System.Object[1024];<br>
    // make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks<br>
    for (var i : int = 0; i < 1024; i++)<br>
    tmp = new byte[1024];<br>
    // release reference<br>
    tmp = null;<br>
    }





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


    回复 支持 反对

    使用道具 举报

  • TA的每日心情

    2016-2-20 08:02
  • 签到天数: 1 天

    [LV.1]初来乍到

    88

    主题

    155

    帖子

    625

    积分

    高级会员

    Rank: 4

    积分
    625
    发表于 2016-3-30 14:11:32 | 显示全部楼层
    原文出处: 慕容小匹夫的博客(@慕容小匹夫)   
    前言:

    刚开始写这篇文章的时候选了一个很土的题目。。。《Unity3D优化全解析》。因为这是一篇临时起意才写的文章,而且陈述的都是既有的事实,因而给自己“文(dou)学(bi)”加工留下的余地就少了很多。但又觉得这块是不得不提的一个地方,平时见到很多人对此处也给予了忽略了事,需要时才去网上扒一些只言片语的资料。也恰逢年前,寻思着周末认真写点东西遇到节假日没准也没什么人读,所以索性就写了这篇临时的文章。题目很土,因为用了指向性很明确的“Unity3D”,让人少了遐(瞎)想的空间,同时用了“高大全”这样的构词法,也让匹夫有成为众矢之的的可能。。。所以最后还是改成了现在各位看到的题目。话不多说,下面就开始正文~正所谓“草蛇灰线,伏脉千里”。那咱们首先~~~~~~

    看看优化需要从哪里着手?

    匹夫印象里遇到的童靴,提Unity3D项目优化则必提DrawCall,这自然没错,但也有很不好影响。因为这会给人一个错误的认识:所谓的优化就是把DrawCall弄的比较低就对了。

    对优化有这种第一印象的人不在少数,drawcall的确是一个很重要的指标,但绝非全部。为了让各位和匹夫能达成尽可能多的共识,匹夫首先介绍一下本文可能会涉及到的几个概念,之后会提出优化所涉及的三大方面:

    • drawcall是啥?其实就是对底层图形程序(比如:OpenGL ES)接口的调用,以在屏幕上画出东西。所以,是谁去调用这些接口呢?CPU。
    • fragment是啥?经常有人说vf啥的,vertex我们都知道是顶点,那fragment是啥呢?说它之前需要先说一下像素,像素各位应该都知道吧?像素是构成数码影像的基本单元呀。那fragment呢?是有可能成为像素的东西。啥叫有可能?就是最终会不会被画出来不一定,是潜在的像素。这会涉及到谁呢?GPU。
    • batching是啥?都知道批处理是干嘛的吧?没错,将批处理之前需要很多次调用(drawcall)的物体合并,之后只需要调用一次底层图形程序的接口就行。听上去这简直就是优化的终极方案啊!但是,理想是美好的,世界是残酷的,一些不足之后我们再细聊。
    • 内存的分配:记住,除了Unity3D自己的内存损耗。我们可是还带着Mono呢啊,还有托管的那一套东西呢。更别说你一激动,又引入了自己的几个dll。这些都是内存开销上需要考虑到的。

    好啦,文中的几个概念提前讲清楚了,其实各位也能看的出来匹夫接下来要说的匹夫关注的优化时需要注意的方面:

    • CPU方面
    • GPU方面
    • 内存方面

    所以,这篇文章也会按照CPU—->GPU—->内存的顺序进行。

    CPU的方面的优化:

    上文中说了,drawcall影响的是CPU的效率,而且也是最知名的一个优化点。但是除了drawcall之外,还有哪些因素也会影响到CPU的效率呢?让我们一一列出暂时能想得到的:

    • DrawCalls
    • 物理组件(Physics)
    • GC(什么?GC不是处理内存问题的嘛?匹夫你不要骗我啊!不过,匹夫也要提醒一句,GC是用来处理内存的,但是是谁使用GC去处理内存的呢?)
    • 当然,还有代码质量

    DrawCalls:

    前面说过了,DrawCall是CPU调用底层图形接口。比如有上千个物体,每一个的渲染都需要去调用一次底层接口,而每一次的调用CPU都需要做很多工作,那么CPU必然不堪重负。但是对于GPU来说,图形处理的工作量是一样的。所以对DrawCall的优化,主要就是为了尽量解放CPU在调用图形接口上的开销。所以针对drawcall我们主要的思路就是每个物体尽量减少渲染次数,多个物体最好一起渲染。所以,按照这个思路就有了以下几个方案:

    • 使用Draw Call Batching,也就是描绘调用批处理。Unity在运行时可以将一些物体进行合并,从而用一个描绘调用来渲染他们。具体下面会介绍。
    • 通过把纹理打包成图集来尽量减少材质的使用。
    • 尽量少的使用反光啦,阴影啦之类的,因为那会使物体多次渲染。

    Draw Call Batching

    首先我们要先理解为何2个没有使用相同材质的物体即使使用批处理,也无法实现Draw Call数量的下降和性能上的提升。

    因为被“批处理”的2个物体的网格模型需要使用相同材质的目的,在于其纹理是相同的,这样才可以实现同时渲染的目的。因而保证材质相同,是为了保证被渲染的纹理相同。

    因此,为了将2个纹理不同的材质合二为一,我们就需要进行上面列出的第二步,将纹理打包成图集。具体到合二为一这种情况,就是将2个纹理合成一个纹理。这样我们就可以只用一个材质来代替之前的2个材质了。

    而Draw Call Batching本身,也还会细分为2种。

    Static Batching 静态批处理

    看名字,猜使用的情景。

    静态?那就是不动的咯。还有呢?额,听上去状态也不会改变,没有“生命”,比如山山石石,楼房校舍啥的。那和什么比较类似呢?嗯,聪明的各位一定觉得和场景的属性很像吧!所以我们的场景似乎就可以采用这种方式来减少draw call了。

    那么写个定义:只要这些物体不移动,并且拥有相同的材质,静态批处理就允许引擎对任意大小的几何物体进行批处理操作来降低描绘调用。

    那要如何使用静态批来减少Draw Call呢?你只需要明确指出哪些物体是静止的,并且在游戏中永远不会移动、旋转和缩放。想完成这一步,你只需要在检测器(Inspector)中将Static复选框打勾即可,如下图所示:

    至于效果如何呢?

    举个例子:新建4个物体,分别是Cube,Sphere, Capsule, Cylinder,它们有不同的网格模型,但是也有相同的材质(Default-Diffuse)。

    首先,我们不指定它们是static的。Draw Call的次数是4次,如图:

    我们现在将它们4个物体都设为static,在来运行一下:

    如图,Draw Call的次数变成了1,而Saved by batching的次数变成了3。

    静态批处理的好处很多,其中之一就是与下面要说的动态批处理相比,约束要少很多。所以一般推荐的是draw call的静态批处理来减少draw call的次数。那么接下来,我们就继续聊聊draw call的动态批处理。

    Dynamic Batching 动态批处理

    有阴就有阳,有静就有动,所以聊完了静态批处理,肯定跟着就要说说动态批处理了。首先要明确一点,Unity3D的draw call动态批处理机制是引擎自动进行的,无需像静态批处理那样手动设置static。我们举一个动态实例化prefab的例子,如果动态物体共享相同的材质,则引擎会自动对draw call优化,也就是使用批处理。首先,我们将一个cube做成prefab,然后再实例化500次,看看draw call的数量。

    1

    2

    3

    4

    5

    for(int i = 0; i < 500; i++)
    {
        GameObject cube;
        cube = GameObject.Instantiate(prefab) as GameObject;
    }



    draw call的数量:

    可以看到draw call的数量为1,而 saved by batching的数量是499。而这个过程中,我们除了实例化创建物体之外什么都没做。不错,unity3d引擎为我们自动处理了这种情况。

    但是有很多童靴也遇到这种情况,就是我也是从prefab实例化创建的物体,为何我的draw call依然很高呢?这就是匹夫上文说的,draw call的动态批处理存在着很多约束。下面匹夫就演示一下,针对cube这样一个简单的物体的创建,如果稍有不慎就会造成draw call飞涨的情况吧。

    我们同样是创建500个物体,不同的是其中的100个物体,每个物体的大小都不同,也就是Scale不同。

    1

    2

    3

    4

    5

    6

    7

    8

    9

    for(int i = 0; i < 500; i++)
    {
        GameObject cube;
        cube = GameObject.Instantiate(prefab) as GameObject;
        if(i / 100 == 0)
        {
            cube.transform.localScale = new Vector3(2 + i, 2 + i, 2 + i);
        }
    }



    draw call的数量:

    我们看到draw call的数量上升到了101次,而saved by batching的数量也下降到了399。各位看官可以看到,仅仅是一个简单的cube的创建,如果scale不同,竟然也不会去做批处理优化。这仅仅是动态批处理机制的一种约束,那我们总结一下动态批处理的约束,各位也许也能从中找到为何动态批处理在自己的项目中不起作用的原因:

    • 批处理动态物体需要在每个顶点上进行一定的开销,所以动态批处理仅支持小于900顶点的网格物体。
    • 如果你的着色器使用顶点位置,法线和UV值三种属性,那么你只能批处理300顶点以下的物体;如果你的着色器需要使用顶点位置,法线,UV0,UV1和切向量,那你只能批处理180顶点以下的物体。
    • 不要使用缩放。分别拥有缩放大小(1,1,1) 和(2,2,2)的两个物体将不会进行批处理。
    • 统一缩放的物体不会与非统一缩放的物体进行批处理。
    • 使用缩放尺度(1,1,1) 和 (1,2,1)的两个物体将不会进行批处理,但是使用缩放尺度(1,2,1) 和(1,3,1)的两个物体将可以进行批处理。
    • 使用不同材质的实例化物体(instance)将会导致批处理失败。
    • 拥有lightmap的物体含有额外(隐藏)的材质属性,比如:lightmap的偏移和缩放系数等。所以,拥有lightmap的物体将不会进行批处理(除非他们指向lightmap的同一部分)。
    • 多通道的shader会妨碍批处理操作。比如,几乎unity中所有的着色器在前向渲染中都支持多个光源,并为它们有效地开辟多个通道。
    • 预设体的实例会自动地使用相同的网格模型和材质。

    所以,尽量使用静态的批处理。

    物理组件

    曾几何时,匹夫在做一个策略类游戏的时候需要在单元格上排兵布阵,而要侦测到哪个兵站在哪个格子匹夫选择使用了射线,由于士兵单位很多,而且为了精确每一帧都会执行检测,那时候CPU的负担叫一个惨不忍睹。后来匹夫果断放弃了这种做法,并且对物理组件产生了心理的阴影。

    这里匹夫只提2点匹夫感觉比较重要的优化措施:

    1.设置一个合适的Fixed Timestep。设置的位置如图:

    那何谓“合适”呢?首先我们要搞明白Fixed Timestep和物理组件的关系。物理组件,或者说游戏中模拟各种物理效果的组件,最重要的是什么呢?计算啊。对,需要通过计算才能将真实的物理效果展现在虚拟的游戏中。那么Fixed Timestep这货就是和物理计算有关的啦。所以,若计算的频率太高,自然会影响到CPU的开销。同时,若计算频率达不到游戏设计时的要求,有会影响到功能的实现,所以如何抉择需要各位具体分析,选择一个合适的值。

    2.就是不要使用网格碰撞器(mesh collider):为啥?因为实在是太复杂了。网格碰撞器利用一个网格资源并在其上构建碰撞器。对于复杂网状模型上的碰撞检测,它要比应用原型碰撞器精确的多。标记为凸起的(Convex )的网格碰撞器才能够和其他网格碰撞器发生碰撞。各位上网搜一下mesh collider的图片,自然就会明白了。我们的手机游戏自然无需这种性价比不高的东西。

    当然,从性能优化的角度考虑,物理组件能少用还是少用为好。

    处理内存,却让CPU受伤的GC

    在CPU的部分聊GC,感觉是不是怪怪的?其实小匹夫不这么觉得,虽然GC是用来处理内存的,但的确增加的是CPU的开销。因此它的确能达到释放内存的效果,但代价更加沉重,会加重CPU的负担,因此对于GC的优化目标就是尽量少的触发GC。

    首先我们要明确所谓的GC是Mono运行时的机制,而非Unity3D游戏引擎的机制,所以GC也主要是针对Mono的对象来说的,而它管理的也是Mono的托管堆。 搞清楚这一点,你也就明白了GC不是用来处理引擎的assets(纹理啦,音效啦等等)的内存释放的,因为U3D引擎也有自己的内存堆而不是和Mono一起使用所谓的托管堆。

    其次我们要搞清楚什么东西会被分配到托管堆上?不错咯,就是引用类型咯。比如类的实例,字符串,数组等等。而作为int,float,包括结构体struct其实都是值类型,它们会被分配在堆栈上而非堆上。所以我们关注的对象无外乎就是类实例,字符串,数组这些了。

    那么GC什么时候会触发呢?两种情况:

    • 首先当然是我们的堆的内存不足时,会自动调用GC。
    • 其次呢,作为编程人员,我们自己也可以手动的调用GC。

    所以为了达到优化CPU的目的,我们就不能频繁的触发GC。而上文也说了GC处理的是托管堆,而不是Unity3D引擎的那些资源,所以GC的优化说白了也就是代码的优化。那么匹夫觉得有以下几点是需要注意的:

    • 字符串连接的处理。因为将两个字符串连接的过程,其实是生成一个新的字符串的过程。而之前的旧的字符串自然而然就成为了垃圾。而作为引用类型的字符串,其空间是在堆上分配的,被弃置的旧的字符串的空间会被GC当做垃圾回收。
    • 尽量不要使用foreach,而是使用for。foreach其实会涉及到迭代器的使用,而据传说每一次循环所产生的迭代器会带来24 Bytes的垃圾。那么循环10次就是240Bytes。
    • 不要直接访问gameobject的tag属性。比如if (go.tag == “human”)最好换成if (go.CompareTag (“human”))。因为访问物体的tag属性会在堆上额外的分配空间。如果在循环中这么处理,留下的垃圾就可想而知了。
    • 使用“池”,以实现空间的重复利用。
    • 最好不用LINQ的命令,因为它们会分配临时的空间,同样也是GC收集的目标。而且我很讨厌LINQ的一点就是它有可能在某些情况下无法很好的进行AOT编译。比如“OrderBy”会生成内部的泛型类“OrderedEnumerable”。这在AOT编译时是无法进行的,因为它只是在OrderBy的方法中才使用。所以如果你使用了OrderBy,那么在IOS平台上也许会报错。

    代码?脚本?

    聊到代码这个话题,也许有人会觉得匹夫多此一举。因为代码质量因人而异,很难像上面提到的几点,有一个明确的评判标准。也是,公写公有理,婆写婆有理。但是匹夫这里要提到的所谓代码质量是基于一个前提的:Unity3D是用C++写的,而我们的代码是用C#作为脚本来写的,那么问题就来了~脚本和底层的交互开销是否需要考虑呢?也就是说,我们用Unity3D写游戏的“游戏脚本语言”,也就是C#是由mono运行时托管的。而功能是底层引擎的C++实现的,“游戏脚本”中的功能实现都离不开对底层代码的调用。那么这部分的开销,我们应该如何优化呢?

    1.以物体的Transform组件为例,我们应该只访问一次,之后就将它的引用保留,而非每次使用都去访问。这里有人做过一个小实验,就是对比通过方法GetComponent<Transform>()获取Transform组件, 通过MonoBehavor的transform属性去取,以及保留引用之后再去访问所需要的时间:

    • GetComponent = 619ms
    • Monobehaviour = 60ms
    • CachedMB = 8ms
    • Manual Cache = 3ms

    2.如上所述,最好不要频繁使用GetComponent,尤其是在循环中。

    3.善于使用OnBecameVisible()和OnBecameVisible(),来控制物体的update()函数的执行以减少开销。

    4.使用内建的数组,比如用Vector3.zero而不是new Vector(0, 0, 0);

    5.对于方法的参数的优化:善于使用ref关键字。值类型的参数,是通过将实参的值复制到形参,来实现按值传递到方法,也就是我们通常说的按值传递。复制嘛,总会让人感觉很笨重。比如Matrix4x4这样比较复杂的值类型,如果直接复制一份新的,反而不如将值类型的引用传递给方法作为参数。

    好啦,CPU的部分匹夫觉得到此就介绍的差不多了。下面就简单聊聊其实匹夫并不是十分熟悉的部分,GPU的优化。

    GPU的优化

    GPU与CPU不同,所以侧重点自然也不一样。GPU的瓶颈主要存在在如下的方面:

    • 填充率,可以简单的理解为图形处理单元每秒渲染的像素数量。
    • 像素的复杂度,比如动态阴影,光照,复杂的shader等等
    • 几何体的复杂度(顶点数量)
    • 当然还有GPU的显存带宽

    那么针对以上4点,其实仔细分析我们就可以发现,影响的GPU性能的无非就是2大方面,一方面是顶点数量过多,像素计算过于复杂。另一方面就是GPU的显存带宽。那么针锋相对的两方面举措也就十分明显了。

    • 少顶点数量,简化计算复杂度。
    • 缩图片,以适应显存带宽。

    减少绘制的数目

    那么第一个方面的优化也就是减少顶点数量,简化复杂度,具体的举措就总结如下了:

    • 保持材质的数目尽可能少。这使得Unity更容易进行批处理。
    • 使用纹理图集(一张大贴图里包含了很多子贴图)来代替一系列单独的小贴图。它们可以更快地被加载,具有很少的状态转换,而且批处理更友好。
    • 如果使用了纹理图集和共享材质,使用Renderer.sharedMaterial 来代替Renderer.material 。
    • 使用光照纹理(lightmap)而非实时灯光。
    • 使用LOD,好处就是对那些离得远,看不清的物体的细节可以忽略。
    • 遮挡剔除(Occlusion culling)
    • 使用mobile版的shader。因为简单。

    优化显存带宽

    第二个方向呢?压缩图片,减小显存带宽的压力。

    • OpenGL ES 2.0使用ETC1格式压缩等等,在打包设置那里都有。
    • 使用mipmap。

    MipMap

    这里匹夫要着重介绍一下MipMap到底是啥。因为有人说过MipMap会占用内存呀,但为何又会优化显存带宽呢?那就不得不从MipMap是什么开始聊起。一张图其实就能解决这个疑问。

    上面是一个mipmap 如何储存的例子,左边的主图伴有一系列逐层缩小的备份小图

    是不是很一目了然呢?Mipmap中每一个层级的小图都是主图的一个特定比例的缩小细节的复制品。因为存了主图和它的那些缩小的复制品,所以内存占用会比之前大。但是为何又优化了显存带宽呢?因为可以根据实际情况,选择适合的小图来渲染。所以,虽然会消耗一些内存,但是为了图片渲染的质量(比压缩要好),这种方式也是推荐的。

    内存的优化

    既然要聊Unity3D运行时候的内存优化,那我们自然首先要知道Unity3D游戏引擎是如何分配内存的。大概可以分成三大部分:

    • Unity3D内部的内存
    • Mono的托管内存
    • 若干我们自己引入的DLL或者第三方DLL所需要的内存。

    第3类不是我们关注的重点,所以接下来我们会分别来看一下Unity3D内部内存和Mono托管内存,最后还将分析一个官网上Assetbundle的案例来说明内存的管理。

    Unity3D内部内存

    Unity3D的内部内存都会存放一些什么呢?各位想一想,除了用代码来驱动逻辑,一个游戏还需要什么呢?对,各种资源。所以简单总结一下Unity3D内部内存存放的东西吧:

    • 资源:纹理、网格、音频等等
    • GameObject和各种组件。
    • 引擎内部逻辑需要的内存:渲染器,物理系统,粒子系统等等

    Mono托管内存

    因为我们的游戏脚本是用C#写的,同时还要跨平台,所以带着一个Mono的托管环境显然必须的。那么Mono的托管内存自然就不得不放到内存的优化范畴中进行考虑。那么我们所说的Mono托管内存中存放的东西和Unity3D内部内存中存放的东西究竟有何不同呢?其实Mono的内存分配就是很传统的运行时内存的分配了:

    • 值类型:int型啦,float型啦,结构体struct啦,bool啦之类的。它们都存放在堆栈上(注意额,不是堆所以不涉及GC)。
    • 引用类型:其实可以狭义的理解为各种类的实例。比如游戏脚本中对游戏引擎各种控件的封装。其实很好理解,C#中肯定要有对应的类去对应游戏引擎中的控件。那么这部分就是C#中的封装。由于是在堆上分配,所以会涉及到GC。

    而Mono托管堆中的那些封装的对象,除了在在Mono托管堆上分配封装类实例化之后所需要的内存之外,还会牵扯到其背后对应的游戏引擎内部控件在Unity3D内部内存上的分配。

    举一个例子:

    一个在.cs脚本中声明的WWW类型的对象www,Mono会在Mono托管堆上为www分配它所需要的内存。同时,这个实例对象背后的所代表的引擎资源所需要的内存也需要被分配。

    一个WWW实例背后的资源:

    • 压缩的文件
    • 解压缩所需的缓存
    • 解压缩之后的文件

    如图:

    那么下面就举一个AssetBundle的例子:

    Assetbundle的内存处理

    以下载Assetbundle为例子,聊一下内存的分配。匹夫从官网的手册上找到了一个使用Assetbundle的情景如下:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    IEnumerator DownloadAndCache (){
            // Wait for the Caching system to be ready
            while (!Caching.ready)
                yield return null;

            // Load the AssetBundle file from Cache if it exists with the same version or download and store it in the cache
            using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){
                yield return www; //WWW是第1部分
                if (www.error != null)
                    throw new Exception("WWW download had an error:" + www.error);
                AssetBundle bundle = www.assetBundle;//AssetBundle是第2部分
                if (AssetName == "")
                    Instantiate(bundle.mainAsset);//实例化是第3部分
                else
                    Instantiate(bundle.Load(AssetName));
                        // Unload the AssetBundles compressed contents to conserve memory
                        bundle.Unload(false);

            } // memory is freed from the web stream (www.Dispose() gets called implicitly)
        }
    }



    内存分配的三个部分匹夫已经在代码中标识了出来:

    • Web Stream:包括了压缩的文件,解压所需的缓存,以及解压后的文件。
    • AssetBundle:Web Stream中的文件的映射,或者说引用。
    • 实例化之后的对象就是引擎的各种资源文件了,会在内存中创建出来。

    那就分别解析一下:

    1

    WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)



    • 将压缩的文件读入内存中
    • 创建解压所需的缓存
    • 将文件解压,解压后的文件进入内存
    • 关闭掉为解压创建的缓存
    1

    AssetBundle bundle = www.assetBundle;



    • AssetBundle此时相当于一个桥梁,从Web Stream解压后的文件到最后实例化创建的对象之间的桥梁。
    • 所以AssetBundle实质上是Web Stream解压后的文件中各个对象的映射。而非真实的对象。
    • 实际的资源还存在Web Stream中,所以此时要保留Web Stream。
    1

    Instantiate(bundle.mainAsset);



    通过AssetBundle获取资源,实例化对象

    最后各位可能看到了官网中的这个例子使用了:

    1

    2

    using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){
    }



    这种using的用法。这种用法其实就是为了在使用完Web Stream之后,将内存释放掉的。因为WWW也继承了idispose的接口,所以可以使用using的这种用法。其实相当于最后执行了:

    1

    2

    //删除Web Stream
    www.Dispose();



    OK,Web Stream被删除掉了。那还有谁呢?对Assetbundle。那么使用

    1

    2

    //删除AssetBundle
    bundle.Unload(false);



    ok,写到这里就先打住啦。写的有点超了。有点赶也有点临时,日后在补充编辑。


    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2016-3-4 07:46
  • 签到天数: 3 天

    [LV.2]偶尔看看I

    56

    主题

    124

    帖子

    736

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    736
    发表于 2016-4-6 20:28:06 | 显示全部楼层
    项目进入了中期之后,就需要对程序在移动设备上的表现做分析评估和针对性的优化了,首先前期做优化,很多瓶颈没表现出来,能做的东西不多,而且很多指标会凭预想,如果太后期做优化又会太晚,到时发现一些问题改起来返工量就有太大。前一阵子花了大量时间从 cpu gpu 内存 启动时间 到发热量对项目做了一翻大规模的体检和优化,效果还是显著的,在这里做个笔记,以后开发项目时可以作为经验和提前关注
          1.项目情况:笔者所在项目是一个非常重度的手游,甚至开始就是瞄着端游做的,3D世界,2.5D视角,RPG,即使战斗,美术品质要求极高(模型 贴图精度高 ,超过目前市场同类产品)。对于目前大多数移动设备来看,挑战不小,对手机的各种硬件都是挑战。、
         2.目标机型:偏中高端,尽量兼容低端,android至少sumsung S3能流畅,ios至少iphone4s流畅。
         3.性能指标:内存占用250M以下(这样大量512的机器不会挂掉),初始包100m之内(太多运营不干,太少实在是装不下。。)

    用于分析和测试性能的一些利器:
    1.首先是unity在编辑器下的statics窗口:提供了dc和顶点数这两个重要指标的查看。缺点在设备看不到,但是对于dc数和顶点数来说,设备和编辑器差不多,用它可以大体看出渲染的压力。
    2.unity自带的profiler:可以连接设备看到设备上cpu gpu mem的信息,使用的时候需要勾选development模式。有点是cpu的占用在脚本的层面看的非常仔细,哪个函数占用了太多时间一眼就能看出,基本是分析脚本效率的最佳工具,但是gpu大部分设备不支持看不到,显示的mem信息不太准确,基本上偏离实际占用的内存
    3.unity的internal profiler:在playersetting上可以勾选这个选项,勾选后,连接设备,在android的adb或者mac的xcode里会每隔几秒打印出很多关键指标,这个其实非常有用,不过这个功能直到很后期才发现,详细文档见http://docs.unity3d.com/Manual/iphone-InternalProfiler.html
    4.android上的adb:adb提供了一组非常强大的分离android程序的工具,http://developer.android.com/tools/help/adb.html
    而最常用的是用adb的dumpsys 指令,https://source.android.com/devices/tech/input/dumpsys.html
    最常见的包括: adb shell dumpsys meminfo appname  查看实时的内存占用,android的内存分为ps rs,我们一般看ps为准,关于ps rs这些概念可参看http://stackoverflow.com/questio ... lication-in-android
    adb shell dumpsys cpuinfo appname 查看实时的cpu占用,注意这里的cpu可能过百,这是因为多核的原因
    adb shell dumpsys gpuinfo appname 查看实时的gpu情况
    5.android  的monitor
    安装adt后,在sdk\tools\monitor.bat下面有个monitor,是我认为android看性能最好的工具之一,因为它是图形化的,而且基本集成了adb的功能,从内存到cpu到gpu,还有很有用的网络流量使用情况,它的cpu占用是c++层面的统计,看不到脚本,这需要突破那个profilor结合。
    6.android上的mongkey测试:它可以模拟随机的用户输入,用来验证你的程序的强壮性吧
    adb shell monkey -p -v packname 1000
    随机模拟1000条用户事件
    7.ios:ios上的工具则显得更加专业更加统一一些,ios就用xcode自带的instruments了
    这里有个详细的文档https://developer.apple.com/libr ... n/Introduction.html
    看来这么多工具,其实很多是要配合使用的,做u3d开发,其实不只是学会U3D的事情,要让U3D在手机上运行的好,还需要对各个平台有较深的了解。

    利用这些工具法线了一些瓶颈,同时也采取了各种策略来提高性能,反正目标就是cpu占用降低,内存占用减少,启动快,发热小,帧率高,GPU占用少,发现的一些问题和做出的具体的一些努力列举如下:
    1.使用assetbundle,实现资源分离和共享,将内存控制到200m之内,同时也可以实现资源的在线更新
    2.顶点数对渲染无论是cpu还是gpu都是压力最大的贡献者,降低顶点数到8万以下,fps稳定到了30帧左右
    3.只使用一盏动态光,不是用阴影,不使用光照探头
    粒子系统是cpu上的大头
    4.剪裁粒子系统
    5.合并同时出现的粒子系统
    6.自己实现轻量级的粒子系统
    animator也是一个效率奇差的地方
    7.把不需要跟骨骼动画和动作过渡的地方全部使用animation,控制骨骼数量在30根以下
    8.animator出视野不更新
    9.删除无意义的animator
    10.animator的初始化很耗时(粒子上能不能尽量不用animator)
    11.除主角外都不要跟骨骼运动apply root motion
    12.绝对禁止掉那些不带刚体带包围盒的物体(static collider )运动
    NUGI的代码效率很差,基本上runtime的时候对cpu的贡献和render不相上下
    13每帧递归的计算finalalpha改为只有初始化和变动时计算
    14去掉法线计算
    15不要每帧计算viewsize 和windowsize
    16filldrawcall时构建顶点缓存使用array.copy
    17.代码剪裁:使用strip level ,使用.net2.0 subset
    18.尽量减少smooth group
    19.给美术定一个严格的经过科学验证的美术标准,并在U3D里面配以相应的检查工具

    后面的文章会对这些点做以更详细的讨论
    回复 支持 反对

    使用道具 举报

  • TA的每日心情

    2016-2-20 08:02
  • 签到天数: 1 天

    [LV.1]初来乍到

    88

    主题

    155

    帖子

    625

    积分

    高级会员

    Rank: 4

    积分
    625
    发表于 2016-4-9 00:22:04 | 显示全部楼层
    最近研究U3D开发,个人认为,精通一种新的技术,最快最好的方法就是看它的document,而且个人习惯不喜欢看中文的资料,原汁原味的东西是最正确的,一翻译过来很多东西就都不那么准确了。于是通读了unity的官方manuel,最后面几章都是精华,里面给了非常非常多的官方的优化建议,尤其是做移动平台的开发,这些建议就是非常重要的。我将官方manuel advanced后面的那几个章节的东西提炼了一下出来,总结了这样一些优化tips:

    1不是每个主流手机都支持的技术(就是如果可以不用就不用或有备选方案)
    屏幕特效
    动态的pixel光照计算(如法线)
    实时的阴影

    2优化建议
    2.1渲染
    1.不使用或少使用动态光照,使用light mapping和light probes(光照探头)
    2.不使用法线贴图(或者只在主角身上使用),静态物体尽量将法线渲染到贴图
    3.不适用稠密的粒子,尽量使用UV动画
    4.不使用fog,使用渐变的面片(参考shadow gun)
    5.不要使用alpha –test(如那些cutout shader),使用alpha-blend代替
    6.使用尽量少的material,使用尽量少的pass和render次数,如反射、阴影这些操作
    7.如有必要,使用Per-Layer Cull Distances,Camera.layerCullDistances
    8.只使用mobile组里面的那些预置shader
    9.使用occlusion culling
    11.远处的物体绘制在skybox上
    12.使用drawcall batching:
            对于相邻动态物体:如果使用相同的shader,将texture合并
            对于静态物体,batching要求很高,详见Unity Manual>Advanced>Optimizing Graphics Performance>Draw Call Batching

    规格上限
    1.      每个模型只使用一个skinned mesh renderer
    2.      每个mesh不要超过3个material
    3.      骨骼数量不要超过30
    4.      面数在1500以内将得到好的效率
    2.2物理
    1.真实的物理(刚体)很消耗,不要轻易使用,尽量使用自己的代码模仿假的物理
    2.对于投射物不要使用真实物理的碰撞和刚体,用自己的代码处理
    3.不要使用mesh collider
    4.在edit->project setting->time中调大FixedTimestep(真实物理的帧率)来减少cpu损耗
    2.3脚本编写
    1.尽量不要动态的instantiate和destroy object,使用object pool
    2.尽量不要再update函数中做复杂计算,如有需要,可以隔N帧计算一次
    3.不要动态的产生字符串,如Debug.Log("boo" + "hoo"),尽量预先创建好这些字符串资源
    4.cache一些东西,在update里面尽量避免search,如GameObject.FindWithTag("")、GetComponent这样的调用,可以在start中预先存起来
    5.尽量减少函数调用栈,用x = (x > 0 ? x : -x);代替x = Mathf.Abs(x)
    6.下面的代码是几个gc“噩梦”
    String的相加操作,会频繁申请内存并释放,导致gc频繁,使用System.Text.StringBuilder代替
       function ConcatExample(intArray: int[]) {
                   var line = intArray[0].ToString();

                   for (i = 1; i < intArray.Length; i++) {
                                   line += ", " + intArray[i].ToString();
                   }

                   return line;
    }
    在函数中动态new array,最好将一个array、传进函数里修改
    function RandomList(numElements: int) {
              var result = new float[numElements];

              for (i = 0; i < numElements; i++) {
                             result[i] = Random.value;
              }

              return result;
    }

    2.4 shader编写
    1.数据类型
    fixed / lowp - for colors, lighting information and normals,
    half / mediump - for texture UV coordinates,
    float / highp - avoid in pixel shaders, fine to use in vertex shader for position calculations.
    2.少使用的函数:pow,sin,cos等
    2.4 GUI
    1.不要使用内置的onGUii函数处理gui,使用其他方案,如NGUI

    3.格式
    1.贴图压缩格式:ios上尽量使用PVRTC,android上使用ETC
    回复 支持 反对

    使用道具 举报

  • TA的每日心情

    2016-2-20 08:02
  • 签到天数: 1 天

    [LV.1]初来乍到

    88

    主题

    155

    帖子

    625

    积分

    高级会员

    Rank: 4

    积分
    625
    发表于 2016-4-10 13:31:01 | 显示全部楼层
    官方优化文档--优化图像性能
    http://docs.unity3d.com/Document ... icsPerformance.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/1 ... thash.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/ ... %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 。
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    手机版|小黑屋|Office Master ( 蜀ICP备16003423号 )

    GMT+8, 2018-11-18 20:17 , Processed in 0.329003 second(s), 23 queries .

    Office Master

    快速回复 返回顶部 返回列表