unity优化Unity基础入门分享Unity技术分享

Unity性能优化总结

2019-04-02  本文已影响8人  Wongyuhang

一般游戏的性能指标有:帧率、稳定性、流畅性、加载时间(loading)、内存占用(这一项在移动设备上比较重要,有很多闪退原因就是由此造成的)、安装包大小、网络延迟、耗电量等。
程序:代码优化
美术:资源优化
策划:合理和设计方案以避免性能开销
性能优化主要从以下几个方面展开: CPU、GPU、内存

一、CPU

CPU的性能开销主要来自于以下两个方面:
1.引擎模块自身的性能开销:渲染(Draw Call)、动画、物理引擎、UI模块、粒子系统、资源加载、GC(Garbage Collection)。
2.游戏自身代码的性能开销:代码结构、循环、数据结构等。

渲染:

1.降低Draw Call
CPU每次在准备数据(顶点位置、法线、颜色、坐标纹理等)并通知GPU渲染的过程称为一次Draw Call,其实就是CPU对底层图形程序接口的调用。
降低Draw Call有以下几种方法:
①减少所渲染物体的材质种类 → 通过把纹理打包成图集来尽量减少材质的使用。
②通过Draw Call Batching来减少其数量 → 静态批处理和动态批处理。
批处理的思想:在每次调用Draw Call时尽可能多地处理多个物体(多个物体最好一起渲染,将批处理之前需要很多次调用的物体合并,之后只需要调用一次底层图形的接口就行),减少每一帧需要的Draw Call数目。
静态批处理的优点:自由度高,限制很少。
静态批处理的缺点:可能会占用更多的内存(额外的内存开销来存储合并后的几何数据),而经过静态批处理后的所有物体都不可以再移动了(即使在脚本中尝试改变物体的位置也是无效的)。
静态批处理的时间点:⑴在游戏导出的时候,在player setting中勾选static batching,这样导出包的时候就进行批处理,包体较大。⑵在游戏场景中勾选物体的static选项,在加载该场景的时候,会进行一次静态批处理的合并,这样导出来的包不大,但是在加载的时候会使得内存变大。
动态批处理的优点:Unity自动完成,实现方便,经过批处理的物体仍然可以移动,这是由于在处理每一帧时Unity都会重新合并一次网格。
动态批处理的缺点:限制有很多,可能一不小心就破坏了这种机制,导致Unity无法批处理一些使用了相同材质的物体。
③尽量少使用反光、阴影等,因为这样会使得物体多次渲染。
2.简化资源
3.LOD
Levels Of Detail是一种优化游戏效率的常用方法,它是根据物体在游戏画面中所占视图的百分比来调用不同复杂度的模型的,其缺点在于会占用大量内存,使用这个技术一般是在解决运行时流畅度的问题,以空间交换时间。
4.剔除Culling

UI模块:

1.动静分离
2.预加载、常驻、即时释放
3.图集
4.内存池
5.Active/Deactive
6.UISprite、Texture
7.不移动、不可见UI不更新
8.对于不交互的UI元素,关闭Raycast Target
9.资源预加载
10.shader预加载

加载模块

主要出现于场景切换处,且CPU占用峰值均较高 → 前一场景的场景卸载和下一场景的场景加载。
⑴场景卸载
调用SceneManger.LoadScene时,引擎即会对上一场景进行处理
其主要开销如下:
--Destroy
引擎在切换场景时会收集未标识成"DontDestroyOnload"的GameObject,然后进行Destroy。同时,代码中的OnDestroy被触发执行,这里的性能开销主要取决于OnDestroy回调函数中的代码逻辑。
--Resource.UnloadUnusedAssets
一般情况下,场景切换过程中,该API会被调用两次,一次为引擎在切换场景时自动调用,另一次则为用户手动调用(一般出现在场景加载后,用户调用他拉确保上一场景中的资源被卸载干净),其耗时开销主要取决于场景中Asset和Object的数量,数量越多、耗时越长。
⑵场景加载
--资源加载(90%以上)
其加载效率主要取决于资源的加载方式(R.L和AB加载)、加载量(纹理、网格、材质等资源数据的大小)和资源的格式(纹理格式、音频格式等)。
--Instantiate实例化
①资源加载,在Instantiate实例化时,引擎底层会查看其相关的资源是否已经被加载,如果没有,则会先加载其相关资源,再进行实例化 → Instantiate耗时的根本原因。
②除此之外,Instantiate实例化的性能开销还体现在脚本代码的序列化(Component数目)和构造函数的执行上。(Awake和Start函数中的代码逻辑,其产生的开销也会被计算在Instantiate实例化内)
资源加载是加载模块中最为耗时的部分,其CPU开销在Unity中主要体现在Loading.UpdatePreloading和Loading.ReadObject

Loading.UpdatePreloading
Loading.UpdatePreloading这一项仅在调用类似LoadLevel(Async)的接口处出现,主要负责卸载当前场景的资源并且加载下一场景中的相关资源和序列化信息等。下一场景中,自身所拥有的GameObject和资源越多,其加载开销越大。(在很多项目中,存在另外一种加载方式,及场景为空场景,绝大部分资源和GameObject都是通过OnLevelwasloaded回调函数进行加载、实例化和拼合的,对于这种情况,Loading.UpdatePreloading的开销会很小)

Loading.ReadObject
Loading.ReadObject这一项记录的则是资源加载时的真正资源读取性能开销,基本上引擎的主流资源(纹理、资源、网络资源、动画片段等)读取均是通过该项来进行体现的。可以说这一项很大程度上决定了项目场景的切换效率。

物理:

1.少用或不用mesh colider(网格碰撞器)
太复杂,网格碰撞器利用一个网格资源并在其上构建碰撞器。对于复杂网状模型上的碰撞检测,它要比应用原型碰撞器精确的多。
2.设置一个合适的Fixed Timestep
Fixed Timestep和物理计算有关,若计算的频率太高,增加CPU的开销
3.优化物理算法

GC(Garbage Collection)

GC:主要作用在于从已用内存中找出那些不再使用的内存,并进行释放
GC是Mono运行时的机制,而非Unity3D游戏引擎的机制,故GC不是用来处理引擎的Assets的内存释放的。
什么东西会被分配到托管堆上? → 引用类型(类的实例、字符串、数组等)
而值类型是分配到堆栈上而非堆上。
所以GC的优化其实就相当于是代码的优化。
下列两种情况会触发GC:
⑴当空闲内存不足时会触发GC。(PS:GC释放的内存只会留给Mono使用,并不会交还给,因此Mono堆内存是只增不减的)
⑵在代码中通过调用GC.Collect()手动进行GC,但是GC本身是比较耗时的操作,而且由于GC会暂停那些需要Mono内存分配的线程(C#代码创建的线程和主线程),因此无论是否在主线程中调用,GC都会导致游戏一定程度的卡顿,需要谨慎处理。
Mono内存泄漏:对象不再使用却没有被GC回收的情况 → 会导致空闲内存减少,GC频繁,Mono堆不断扩充,最终导致游戏占用的内存升高。
※游戏中大部分Mono内存泄漏的情况都是由于静态对象的引用而引起的,因此对于静态对象尽量少用,对于不再使用的静态对象将其引用设置为null,使其可以即使被GC回收。
GC使用注意点:
⑴字符串连接的处理。因为将两个字符串连接的过程,其实是生成一个新的字符串的过程。而作为引用类型的字符串,其空间是在堆上分配的,被弃置的旧的字符串的空间会被GC当做垃圾回收。
⑵尽量不用使用foreach
⑶不要直接访问gameobject的tag属性。比如if (MyGameObject.tag == “Player”)最好换成if (MyGameObject.CompareTag (“Player”))。因为访问物体的tag属性会在堆上额外的分配空间。
⑷使用对象池 → 实现空间的复用
⑸最好不用LINQ的命令,因为它们会分配临时的空间,同样也是GC收集的目标。
⑹尽量减少代码堆内存分配,应避免频繁创建和开辟空间,防止频繁触发GC,同时在Loading或者对性能不敏感的时候主动GC。

二、GPU

GPU的性能瓶颈主要存在于以下几个方面:
1.Fill Rate(填充率),是指显卡每帧或者说每秒能够渲染的像素数。在每帧的绘制中,如果一个像素被反复绘制(overdraw)的次数越多,那么它占用的资源也必然越多。目前在移动设备上,FillRate的压力主要来自半透明物体。因为多数情况下,半透明物体需要开启Alpha Blend并且关闭ZTest和ZWrite(shader中的渲染队列),同时如果我们绘制像alpha = 0这种实际上不会产生效果的颜色上去,也会有Blend操作,这是一种极大的浪费。
2.像素的复杂度,比如动态阴影、光照、复杂的shader等。
3.几何体的复杂度(顶点数量)。
4.GPU的显存带宽。

降低填充率
在开发过程中应注意避免overdraw和尽量降低overdraw,降低overdraw有以下几种方法:
⑴尽量减少alpha = 0的资源的使用,因为这种资源也会参与绘制,占用一定的GPU。
⑵制作图集的时候,尽量使小图排布紧凑,尽量图集中大面积留白,理由同上。
⑶避免无用对象及组件的过度使用(比如新手教学部分用了很多“不可见”的Image作为交互响应的控件;但这些东西虽然画上去没有效果,依然占用了显卡资源,特别是有很多大块的区域),这种情况下可以实现一个只在逻辑上响应Raycast但是不参与绘制的组件即可。
具体可参考以下文章:https://blog.uwa4d.com/archives/fillrate.html
⑷通过修改渲染队列也能降低overdraw。
⑸注意UI文本的空白区域引起的overdraw。UI文本字形是作为独立的面片(quad)进行渲染的,每个字符都是一个面片。这些面片通常含有大量的空白区域围绕着字体,空白区域的大小取决于字形的形状,在放置文本时很容易就会忽略(破坏其他UI的批处理,所以对字体尽可能预留一定的空间。
⑹Image Sliced模式(九宫格拉伸),情况允许下取消勾选Fill Center(中心镂空,以其他UI元素覆盖),可以减少overdraw。

减少顶点数量
⑴保持材质的数目尽可能少,使Unity容易批处理 → 尽可能减少模型中三角形的数目,尽可能重用顶点。(“软边”→减少顶点数目,并且可以使得渲染效果更加平滑。)
⑵使用纹理图集(一张大贴图里面包含了很多子贴图)来代替一系列单独的小贴图。它们可以更快地被加载,具有很少的状态转换,而且对批处理更友好。
⑶如果使用了纹理图集和共享材质,如果使用了纹理图集和共享材质,使用Renderer.sharedMaterial 来代替Renderer.material。
⑷使用光照纹理(LightMap)而非实时灯光。(LightMap是一种很常见的优化策略,它主要用于场景中整体的光照效果。这种技术主要是提前把场景中的光照信息存储在一张光照纹理中,然后在运行时刻只需要根据纹理采样得到光照信息即可)
⑸使用LOD,好处就是对那些离得远,看不清的物体的细节可以忽略。(但如果没有调整好距离的话可能会造成模型的突变,使用时需要注意)
⑹遮挡剔除(Occlusion culling),这里需要注意的是,合并方式也会影响Culling,例如把整个游戏所有的树都合并成一个DC,DC是下降了,但是只要有一棵树在摄像机里,所有合并的树模型都会被渲染,增大了渲染带宽和负载,需要权衡使用。
⑺使用mobile版的shader。因为简单。

优化显存带宽
压缩图片,减小显存带宽的压力。
显存带宽的瓶颈:
①尺寸很大且未压缩的纹理。(理解为一个通道,太大难过去)
②分辨率过高的framebuffer
优化方法:
⑴设置格式压缩
Android → ETC1
IOS → PVRTC
但对于透明纹理,ETC1不支持,而PVRTC则可能会有较大失真,因此更推荐使用RGBA16(要注意“色阶问题”,即色彩过渡不均匀,应避免大量的过渡色使用),RGBA32比较占内存,不推荐使用。
另外,针对Android上带alpha通道的图片,还有一种比较常见的做法:即把alpha通道独立出来作为另外一张纹理,从而将RGB部分和alpha部分分别采用ETC1来压缩,但渲染时就需要自定义的shader来处理。
⑵Mipmap
Mipmap中每一个层级的小图都是主图的一个特定比例的缩小细节的复制品。因为存了主图和它的那些缩小的复制品,所以内存占用会比之前大。但是为何又优化了显存带宽呢?因为可以根据实际情况,选择适合的小图来渲染。所以,虽然会消耗一些内存(大概增加30%),但是为了图片渲染的质量(比压缩要好),这种方式也是推荐的。(一般UI没有必要开启Mipmap)

三、内存

Unity内存占用主要来自一下三个方面:
1.资源内存占用
2.引擎模块自身内存占用
3.托管堆内存占用
PS:Log输出也会占用少量内存,当有大量Log输出时需要注意。

资源内存占用
⑴纹理(Texture)
①尽可能根据硬件的种类选择硬件支持的纹理格式
②纹理尺寸越大,则占用内存越大,必要时可以使用九宫格拉伸
③Mipmap
④Read & Write,此项开启后内存消耗会增大一倍
⑵网格(Mesh)
Color数据、Normal数据、Tangent数据
⑶动画片段(AnimationClip)
⑷音频片段(AudioClip)
⑸材质(Material)
⑹着色器(Shader)
⑺字体资源(Font)
⑻文本资源(TextAsset)

上一篇下一篇

猜你喜欢

热点阅读