U3D常见问题:优化

优化问题

CPU方面优化

(1) 减少DrawCall.
DrawCall就是CPU调用图形编程接口,比如DirectX或OpenGL,来命令GPU进行渲染的操作。
DrawCall思路就是每个物体尽量减少渲染次数,多个物体最好一起渲染,方案:

  • 通过把纹理打包成图集尽量减少材质的使用(材质相同纹理不同时可以优化)。
  • 尽量减少反光,阴影之类的效果,因为那会使物体多次渲染。
  • -使用批处理,Unity在运行时将物体合并一次渲染。
    这里分为动态批处理和静态批处理。
    静态批处理就是将没有生命的不同物体设置为Static,只要这些物体不移动,并且拥有相同的材质,静态批处理就允许引擎对任意大小的几何物体进行批处理操作来降低描绘调用。静态批处理的好处很多,其中之一就是与下面要说的动态批处理相比,约束要少很多。所以一般推荐的是draw call的静态批处理来减少draw call的次数。
    动态批处理引擎自动进行的,无需像静态批处理手动设置static。当动态实例化Prefab(预制体)时,如果动态物体共享相同的材质,引擎会自动批处理(比如循环实例化一个对象500次,此对象是一样的,则draw call的数量为1次,而 saved by batching的数量是499次),但动态批处理的弊端在于很多时候Prefab时即使是有相同的材质,drawCall的次数仍然很多,这是为什么? 原因是动态批处理有很多的限制,比如说,创建500个物体,不同的是其中的100个物体,每个物体的大小都不同,也就是Scale不同。
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的数量上升到了101次,而saved by batching的数量下降到了399。如果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中所有的着色器在前向渲染中都支持多个光源,并为它们有效地开辟多个通道。
预设体的实例会自动地使用相同的网格模型和材质。
所以,尽量使用静态的批处理。

(2) 物理组件(Physics).

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

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

(3) 减少GC(垃圾回收)次数.
虽然GC是用来处理内存的,但的确增加的是CPU的开销。因此它的确能达到释放内存的效果,但代价更加沉重,会加重CPU的负担,因此对于GC的优化目标就是尽量少的触发GC。(GC是何时触发的? 1、堆的内存不足时,自动调用GC。2、编程人员手动调用GC。)
首先我们要明确所谓的GC是Mono运行时的机制,而非Unity3D游戏引擎的机制,所以GC也主要是针对Mono的对象来说的,而它管理的也是Mono的托管堆。 搞清楚这一点,你也就明白了GC不是用来处理引擎的assets(纹理啦,音效啦等等)的内存释放的,因为U3D引擎也有自己的内存堆而不是和Mono一起使用所谓的托管堆。

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

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

(4) 脚本的代码质量.
所谓代码质量是基于一个前提的:Unity3D是用C++写的,而我们的代码是用C#作为脚本来写的,那么问题就来了~脚本和底层的交互开销是否需要考虑呢?也就是说,我们用Unity3D写游戏的“游戏脚本语言”,也就是C#是由mono运行时托管的。而功能是底层引擎的C++实现的,“游戏脚本”中的功能实现都离不开对底层代码的调用。那么这部分的开销,我们应该如何优化呢?

(1)Transfrom组件获取一次就保留引用,而不是每次都获取。这里有人做过一个小实验,就是对比通过方法GetComponent()获取Transform组件, 通过MonoBehavor的transform属性去取,以及保留引用之后再去访问所需要的时间:

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

(2)不用频繁的GetComponent,尤其是在循环中(少获取组件);务必在工程发布前删除代码里面的Debug.Log和print (重要,这2个比较消耗性能);数组、集合类元素优先使用Array,其次是List,少用ArrayList(会拆装箱操作)。

(3)使用内建数组,如Vector3.zero而不是new Vector(0,0,0)。

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

(5)少用反射,ios开发的可以忘了反射(不是不用哈,少用)

GPU方面优化

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

  • 填充率,可以简单的理解为图形处理单元每秒渲染的像素数量。
  • 像素的复杂度,比如动态阴影,光照,复杂的shader等等
  • 几何体的复杂度(顶点数量)
  • 当然还有GPU的显存带宽
    所以影响GPU性能的无非就两大方面,一方面顶点数量过多,像素计算过于复杂;另一方面就是GPU的显存带宽。
    优化方法:
    1.减少顶点数量,简化复杂度
    2.压缩图片质量,以适应显存带宽

第一个减少顶点数量的方面,措施如下:

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

第二个方面的压缩图片,减小显存带宽的压力措施如下:

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

什么是Mipmap?
Mipmap技术有点类似于LOD技术,但是不同的是,LOD针对的是模型资源,而Mipmap针对的纹理贴图资源
MipMap可以用于跑酷类游戏,当角色靠近时,贴图清晰显示,否则模糊显示。
(如果面试官问你做项目时遇到过什么问题。。手机一开始运行时很卡,使用mipmap技术后将还未跑到的远处资源模糊显示进行优化后就不卡了)
缺点:会占用内存,因为mipmap会根据摄像机远近不同而生成对应的八个贴图,所以必然占内存!
优点:会优化显存带宽,用来减少渲染,因为可以根据实际情况,会选择适合的贴图来渲染,距离摄像机越远,显示的贴图像素越低,反之,像素越高!

内存优化

Unity3D游戏引擎的内存分配可以分为三大部分:

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

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

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

Mono托管内存

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值