博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
unity优化《一》
阅读量:2157 次
发布时间:2019-05-01

本文共 8112 字,大约阅读时间需要 27 分钟。

从DrawCall 到GC

一:首先优化从哪里着手?

1.drawcall 是啥?就是CPU对底层图形程序(如OpenGL ES)接口的调用,以在屏幕上画出东西。

2.fragment是啥?经常有人说vf啥的,vertex我们都知道是顶点,那fragment呢?像素是构成数码影像 的基本单元,而fragment是有可能成为像素的东西,意思就是不一定会被画出来的潜在像素。涉及 GPU.

3.batching是啥?批处理之前需要很多次调用(drawcall)的物体合并,之后只需调用一次底层图形程序 的接口就行。

4.内存:除了unity自身,还有mono,还有托管,还有引入的dll。

二:影响CPU效率的因素。

1.DrawCall。

2.物理组件(Physics).

3.GC

4.代码质量

DrawCall是CPU调用底层图形接口。比如有上千个物体,每一个的渲染都需要去调用一次底层接口,而每一次的调用CPU都需要做很多工作,

那么CPU必然不堪重负。但是对于GPU来说,图形处理的工作量是一样的。所以对DrawCall的优化,主要就是为了尽量解放CPU在调用图形接口上的

开销。所以针对drawcall我们主要的思路就是每个物体尽量减少渲染次数,多个物体最好一起渲染。

动态批处理的约束:

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

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

物理组件:

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运行时的机制,而非引擎的机制,所以GC也主要是针对Mono的对象来说的,而它管理的也是Mono的托管堆。 搞清楚这一点,你也就明白了GC不是用来处理引擎的assets(纹理啦,音效啦等等)的内存释放的,因为引擎也有自己的内存堆而不是和Mono一起使用所谓的托管堆。

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

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

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

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

  1. 字符串连接的处理。因为将两个字符串连接的过程,其实是生成一个新的字符串的过程。而之前的旧的字符串自然而然就成为了垃圾。而作为引用类型的字符串,其空间是在堆上分配的,被弃置的旧的字符串的空间会被GC当做垃圾回收。
  2. 尽量不要使用foreach,而是使用for。foreach其实会涉及到迭代器的使用,而据传说每一次循环所产生的迭代器会带来24 Bytes的垃圾。那么循环10次就是240Bytes。
  3. 不要直接访问gameobject的tag属性。比如if (go.tag == “human”)最好换成if (go.CompareTag (“human”))。因为访问物体的tag属性会在堆上额外的分配空间。如果在循环中这么处理,留下的垃圾就可想而知了。
  4. 使用“池”,以实现空间的重复利用。
  5. 最好不用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的瓶颈主要存在在如下的方面:

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

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

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

减少绘制的数目

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

  • 保持材质的数目尽可能少。这使得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是如何分配内存的。大概可以分成三大部分:

  1. Unity3D内部的内存
  2. Mono的托管内存
  3. 若干我们自己引入的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)
    
}
}

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

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

那就分别解析一下:

1
WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)
  1. 将压缩的文件读入内存中
  2. 创建解压所需的缓存
  3. 将文件解压,解压后的文件进入内存
  4. 关闭掉为解压创建的缓存
1
AssetBundle bundle = www.assetBundle;
  1. AssetBundle此时相当于一个桥梁,从Web Stream解压后的文件到最后实例化创建的对象之间的桥梁。
  2. 所以AssetBundle实质上是Web Stream解压后的文件中各个对象的映射。而非真实的对象。
  3. 实际的资源还存在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
);

======这里讲讲从渲染优化,包体大小,再到内存,深入分析与解释如何进行游戏优化。========

一.渲染级别。

(1)NGUI部分:

它对动态移动、旋转、缩放GUI支持的是比较差的,所以我们尽量不要把过多的移动旋转缩放部分写在GUI中。

但,很多情况下是避免不了的,如:大量的伤害数字,物品掉落,图标的移动和旋转等。

为了不让GUI去控制这些渲染物体,一小部分用3D面片代替,大部分用程序去生成面片渲染,脱离了GUI的控制。另外,在那些静态的GUI中,使用静态物体优化的属性,加上排除不必要的GUI设置,使得GUI部分效率足够高。

(2)3D部分:

2.1:特效是对画面效果最有影响的部分,尽量少使用粒子或者将粒子的数量减少到最小。

2.2:尽量减少灯光的使用,而使用烘培后的图代替。但烘培贴图多了内存就爆了,要谨慎。

2.3:使用静态批处理,将那些不动的物体设置为静态,以降低CPU。但,unity说静态批处理会增加 更多内存,要谨慎。

2.4:尽可能的将多个物体使用同一个材质球,可以降低drawcall。

2.5:将多个不动的,使用相同材质球的物体合并mesh,更有效降低drawcall。

2.6:尽量关闭阴影渲染。会降低不少drawcall,减少对阴影的计算。

(3)2D部分:

3.1:将同一页面或同一类型的ui合并成一个图集,减少渲染时的drawcall.

3.2:将需要alpha和不需要alpha的图拆分开来做图集。不需要alpha的图集可以大幅度的做压缩 理。

3.3:在以上基础上,尽量将图集做压缩处理,只有那些需要高精度的图才不做压缩,可以大幅度减 少内存占用。

二:减少占用空间大小。

1.使用动态资源加载方式,WWW方式。在导出资源时使用      BuildAssetBundleOptions.DisableWriteTypeTree的打包方式,减少包的大小。

2.对每张Textrue,都设置对图片进行压缩。

3.让美术减少模型面片数,并在fbx模式设置中,设置对动画进行压缩。

4.让美术减少动画帧数,并在动画设置中,设置对动画进行压缩。

三:内存。

1。最基本的就是对需要的资源进行加载,使用完毕后,释放。

2。GUI部分在GUIManager管理类中增加对GUI进行定时检查的部分,对不展示的GUI资源进行释    放。对于一些ui出现时的释放资源卡顿问题,可以在ui出现后1秒再释放资源。

3。有时一次加载资源过多引起内存膨胀、IO过慢导致崩溃,可选择资源异步加载。

4.内存释放。

转载地址:http://lpmwb.baihongyu.com/

你可能感兴趣的文章
Windows程序设计:直线绘制
查看>>
linux之CentOS下文件解压方式
查看>>
Django字段的创建并连接MYSQL
查看>>
div标签布局的使用
查看>>
HTML中表格的使用
查看>>
(模板 重要)Tarjan算法解决LCA问题(PAT 1151 LCA in a Binary Tree)
查看>>
(PAT 1154) Vertex Coloring (图的广度优先遍历)
查看>>
(PAT 1115) Counting Nodes in a BST (二叉查找树-统计指定层元素个数)
查看>>
(PAT 1143) Lowest Common Ancestor (二叉查找树的LCA)
查看>>
(PAT 1061) Dating (字符串处理)
查看>>
(PAT 1118) Birds in Forest (并查集)
查看>>
数据结构 拓扑排序
查看>>
(PAT 1040) Longest Symmetric String (DP-最长回文子串)
查看>>
(PAT 1145) Hashing - Average Search Time (哈希表冲突处理)
查看>>
(1129) Recommendation System 排序
查看>>
PAT1090 Highest Price in Supply Chain 树DFS
查看>>
(PAT 1096) Consecutive Factors (质因子分解)
查看>>
(PAT 1019) General Palindromic Number (进制转换)
查看>>
(PAT 1073) Scientific Notation (字符串模拟题)
查看>>
(PAT 1080) Graduate Admission (排序)
查看>>