Unity内存优化

总体来说,内存的优化在于贴图和网格

assetbundle采用unity5自带的,减少每一份的内存

合并贴图TexturePacking,共用材质球   ,拆分贴图png采用etc

手动合并网格或者自带合并网格






总结

资源内存占用:
一、纹理
(1) 纹理格式 使用ETC1代替RGBA32,占用内存变低,ETC1不支持alpha,所以把图片拆成2个
(2) 纹理尺寸 越低越好 不超过1024*1024
(3) 关闭mipmap
(4) 关闭 图片读写操作 Read & Write
(5) 尽量使用2的n次幂大小的贴图,否则GfxDriver里会有2份贴图;

二、网格
(1) Normal、Color和Tangent,注意里面的属性,如果100个里面有99个没有color属性,1个有color会导致所有都会加上color属

(2)尽量控制模型的面数,小于1500会比较合适;
(3)不同设备使用不同的模型面数;
(4)尽量保持在30根骨骼内;
(5)一个网格不要超过3个material;
 
引擎模块自身占用:
 保证assetbundle被清干净

托管堆内存占用:
 高频率地 New Class/Container/Array
 Log输出

代码优化:
 尽量避免代码中的任何字符串连接,因为这会给GC带来太多垃圾;
 用简单的“for”循环代替“foreach”循环;
 为所有游戏内的动态物体使用内存对象池,可以减少系统开销和内存碎片,复用对象实例,构建自己的内存管理模式,减少
Instantiate和Destory;
 尽量不使用LINQ命令,因为它们一般会分配中间缓器,而这很容易生成垃圾内存;
  将引用本地缓存到元件中会减少每次在一个游戏对象中使用 “GetComponent” 获取一个元件引用的需求;
 减少角色控制器移动命令的调用。移动角色控制器会同步发生,每次调用都会耗损较大的性能;
 最小化碰撞检测请求(例如ray casts和sphere checks),尽量从每次检查中获得更多信息;
 AI逻辑通常会生成大量物理查询,建议让AI更新循环设置低于图像更新循环,以减少CPU负荷;
 要尽量减少Unity回调函数,哪怕是空函数也不要留着;(例如空的Update、FixedUpdate函数)
 尽量少使用FindObjectsOfType函数,这个函数非常慢,尽量少用且一定不要在Update里调用;
 千万一定要控制mono堆内存的大小;







内存的开销无外乎以下三大部分:1.资源内存占用;2.引擎模块自身内存占用;3.托管堆内存占用。


资源内存占用

一、纹理

纹理资源可以说是几乎所有游戏项目中占据最大内存开销的资源。一个6万面片的场景,网格资源最大才不过10MB,但一个2048x2048的纹理,可能直接就达到16MB。因此,项目中纹理资源的使用是否得当会极大地影响项目的内存占用。 那么,纹理资源在使用时应该注意哪些地方呢?

(1) 纹理格式

纹理格式是研发团队最需要关注的纹理属性。因为它不仅影响着纹理的内存占用,同时还决定了纹理的加载效率。一般来说,我们建议开发团队尽可能根据硬件的种类选择硬件支持的纹理格式,比如Android平台的ETC、iOS平台的PVRTC、Windows PC上的DXT等等。因此,我们在UWA测评报告中,将纹理格式进行详细罗列,以便开发团队进行快速查找,一步定位。

 

在使用硬件支持的纹理格式时,你可能会遇到以下几个问题:

  • 色阶问题 由于ETC、PVRTC等格式均为有损压缩,因此,当纹理色差范围跨度较大时,均不可避免地造成不同程度的“阶梯”状的色阶问题。因此,很多研发团队使用RGBA32/ARGB32格式来实现更好的效果。但是,这种做法将造成很大的内存占用。比如,同样一张1024x1024的纹理,如果不开启Mipmap,并且为PVRTC格式,则其内存占用为512KB,而如果转换为RGBA32位,则很可能占用达到4MB。所以,研发团队在使用RGBA32或ARGB32格式的纹理时,一定要慎重考虑,更为明智的选择是尽量减少纹理的色差范围,使其尽可能使用硬件支持的压缩格式进行储存。
  • ETC1 不支持透明通道问题 在Android平台上,对于使用OpenGL ES 2.0的设备,其纹理格式仅能支持ETC1格式,该格式有个较为严重的问题,即不支持Alpha透明通道,使得透明贴图无法直接通过ETC1格式来进行储存。对此,我们建议研发团队将透明贴图尽可能分拆成两张,即一张RGB24位纹理记录原始纹理的颜色部分和一张Alpha8纹理记录原始纹理的透明通道部分。然后,将这两张贴图分别转化为ETC1格式的纹理,并通过特定的Shader来进行渲染,从而来达到支持透明贴图的效果。该种方法不仅可以极大程度上逼近RGBA透明贴图的渲染效果,同时还可以降低纹理的内存占用,是我们非常推荐的使用方式。

当然,目前已经有越来越多的设备支持了OpenGL ES 3.0,这样Android平台上你可以进一步使用ETC2甚至ASTC,这些纹理格式均为支持透明通道且压缩比更为理想的纹理格式。如果你的游戏适合人群为中高端设备用户,那么不妨直接使用这两种格式来作为纹理的主要存储格式。

(2)纹理尺寸

一般来说,纹理尺寸越大,则内存占用越大。所以,尽可能降低纹理尺寸,如果512x512的纹理对于显示效果已经够用,那么就不要使用1024x1024的纹理,因为后者的内存占用是前者的四倍。因此,我们在UWA测评报告中,将纹理的尺寸进行详细展示,以便开发团队进行快速检测。

Mipmap旨在有效降低渲染带宽的压力,提升游戏的渲染效率。但是,开启Mipmap会将纹理内存提升1.33倍。对于具有较大纵深感的3D游戏来说,3D场景模型和角色我们一般是建议开启Mipmap功能的,但是在我们的测评项目中,经常会发现部分UI纹理也开启了Mipmap功能。这其实就没有必要的,绝大多数UI均是渲染在屏幕最上层,开启Mipmap并不会提升渲染效率,反倒会增加无谓的内存占用。因此,建议研发团队在UWA的测评报告中通过Mipmap一项进行排序,详细检测开启Mipmap功能的资源是否为UI资源。

(4) Read & Write

一般情况下,纹理资源的“Read & Write”功能在Unity引擎中是默认关闭的。但是,我们仍然在项目深度优化时发现了不少项目的纹理资源会开启该选项。对此,我们建议研发团队密切关注纹理资源中该选项的使用,因为开启该选项将会使纹理内存增大一倍。

 



==============================================================

二、网格

网格资源在较为复杂的游戏中,往往占据较高的内存。对于网格资源来说,它在使用时应该注意哪些方面呢?

(1) Normal、Color和Tangent

在我们深度优化过的大量项目中,Mesh资源的数据中经常会含有大量的Color数据、Normal数据和Tangent数据。这些数据的存在将大幅度增加Mesh资源的文件体积和内存占用。其中,Color数据和Normal数据主要为3DMax、Maya等建模软件导出时设置所生成,而Tangent一般为导入引擎时生成。

更为麻烦的是,如果项目对Mesh进行Draw Call Batching操作的话,那么将很有可能进一步增大总体内存的占用。比如,100个Mesh进行拼合,其中99个Mesh均没有Color、Tangent等属性,剩下一个则包含有Color、Normal和Tangent属性,那么Mesh拼合后,CombinedMesh中将为每个Mesh来添加上此三个顶点属性,进而造成很大的内存开销。正因如此,我们在UWA测评报告中为每个Mesh展示了其Normal、Color和Tangent属性的具体使用情况,研发团队可以直接针对每种属性进行排序查看,直接定位出现冗余数据的资源。

 

 

一般来说这些数据主要为Shader所用,来生成较为酷炫的效果。所以,建议研发团队针对项目中的网格资源进行详细检测,查看该模型的渲染Shader中是否需要这些数据进行渲染。

限于篇幅,我们今天只针对纹理和网格资源进行详细介绍,对于动画片段、音频片段等其他资源,建议您直接通过UWA测评报告中进行查看。同时,我们会在后续的资源专题中进行详细讲解,敬请期待。



=================================================================================



引擎模块自身占用

引擎自身中存在内存开销的部分纷繁复杂,可以说是由巨量的“微小”内存所累积起来的,比如GameObject及其各种Component(最大量的Component应该算是Transform了)、ParticleSystem、MonoScript以及各种各样的模块Manager(SceneManager、CanvasManager、PersistentManager等)...

一般情况下,上面所指出的引擎各组成部分的内存开销均比较小,真正占据较大内存开销的是这两处:WebStream 和SerializedFile。其绝大部分的内存分配则是由AssetBundle加载资源所致。简单言之,当您使用new WWW或CreateFromMemory来加载AssetBundle时,Unity引擎会加载原始数据到内存中并对其进行解压,而WebStream的大小则是AssetBundle原始文件大小 + 解压后的数据大小 + DecompressionBuffer(0.5MB)。同时,由于Unity 5.3版本之前的AssetBundle文件为LZMA压缩,其压缩比类似于Zip(20%-25%),所以对于一个1MB的原始AssetBundle文件,其加载后WebStream的大小则可能是5~6MB,因此,当项目中存在通过new WWW加载多个AssetBundle文件,且AssetBundle又无法及时释放时,WebStream的内存可能会很大,这是研发团队需要时刻关注的。

 

 作者:张鑫 链接:https://zhuanlan.zhihu.com/p/21913770 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

对于SerializedFile,则是当你使用LoadFromCacheOrDownload、CreateFromFile或new WWW本地AssetBundle文件时产生的序列化文件。

对于WebStream和SerializedFile,你需要关注以下两点:

  • 是否存在AssetBundle没有被清理干净的情况。开发团队可以通过Unity Profiler直接查看其使用具体的使用情况,并确定Take Sample时AssetBundle的存在是否合理;
  • 对于占用WebStream较大的AssetBundle文件(如UI Atlas相关的AssetBundle文件等),建议使用LoadFromCacheOrDownLoad或CreateFromFile来进行替换,即将解压后的AssetBundle数据存储于本地Cache中进行使用。这种做法非常适合于内存特别吃紧的项目,即通过本地的磁盘空间来换取内存空间。

注意:关于AssetBundle的详细管理机制,建议查看我们之前的AssetBundle技术文章





=============================================================

托管堆内存占用

对于目前绝大多数基于Unity引擎开发的项目而言,其托管堆内存是由Mono分配和管理的。“托管” 的本意是Mono可以自动地改变堆的大小来适应你所需要的内存,并且适时地调用垃圾回收(Garbage Collection)操作来释放已经不需要的内存,从而降低开发人员在代码内存管理方面的门槛。

但是这并不意味着研发团队可以在代码中肆无忌惮地开辟托管堆内存,因为目前Unity所使用的Mono版本存在一个很严重的问题,即:Mono的堆内存一旦分配,就不会返还给系统。这意味着Mono的堆内存是只升不降的。举个例子,项目运行时,在场景A中开辟了60MB的托管堆内存,而到下一场景B时,只需要使用20MB的托管堆内存,那么Mono中将会存在40MB空闲的堆内存,且不会返还给系统。这是我们非常不愿意看到的现象,因为对于游戏(特别是移动游戏)来说,内存的占用可谓是寸土寸金的,让Mono毫无必要地锁住大量的内存,是一件非常浪费的事情。所以,我们在UWA测评报告中,为研发团队统计了测试过程中累积的函数堆内存分配量,大家只需要通过查看堆内存分配Top10的函数,即可快速对其底层代码实现进行查看,定位是否有分配不必要堆内存的代码存在。

 

 作者:张鑫 链接:https://zhuanlan.zhihu.com/p/21913770 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

读到这里,你可能会产生这样的疑问:我知道了哪些函数的堆内存分配大了,但是我该如何去进一步定位不必要的堆内存呢?

这是我们经常遇到的问题,所以在我们的深度项目优化服务中,我们都会直接进驻到项目团队,现场查看项目代码并对问题代码进行定位。在经过了大量的深度检测后,我们发现用户不必要的堆内存分配主要来自于以下几个方面:

  • 高频率地 New Class/Container/Array等。研发团队切记不要在Update、FixUpdate或较高调用频率的函数中开辟堆内存,这会对你的项目内存和性能均造成非常大的伤害。做个简单的计算,假设你的项目中某一函数每一帧只分配100B的堆内存,帧率是1秒30帧,那么1秒钟游戏的堆内存分配则是3KB,1分钟的堆内存分配就是180KB,10分钟后就已经分配了1.8MB。如果你有10个这样的函数,那么10分钟后,堆内存的分配就是18MB,这期间,它可能会造成Mono的堆内存峰值升高,同时又可能引起了多次GC的调用。在我们的测评项目中,一个函数在10分钟内分配上百MB的情况比比皆是,有时候甚至会分配上GB的堆内存。
  • Log输出。我们发现在大量的项目中,仍然存在大量Log输出的情况。建议研发团队对自身Log的输出进行严格的控制,仅保留关键Log,以避免不必要的堆内存分配。对此,我们在UWA测评报告中对Log的输出进行了详细的检测,不仅提供详细的性能开销,同时占用Log输出的调用路径。这样,研发团队可直接通过报告定位和控制Log的输出。

 

 UIPanel.LateUpdate。这是NGUI中CPU和堆内存开销最大的函数。它本身只是一个函数,但NGUI的大量使用使它逐渐成为了一个不可忽视规则。该函数的堆内存分配和自身CPU开销,其根源上是一致的,即是由UI网格的重建造成。因此,其对应的优化方法是直接查看CPU篇中的UI模块讲解。

作者:张鑫 链接:https://zhuanlan.zhihu.com/p/21913770 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

关于代码堆内存分配的注意点还有很多,比如String连接、部分引擎API(GetComponent)的使用等等,这些已经是老生常谈了,鉴于篇幅限制不在此处多作介绍,大家感兴趣可以Google自行搜索。后续也会有专门的代码效率专题讲解,敬请关注。

===========================================================================






UWA测评的内存标准

在大家使用过UWA之后,对于UWA推荐的内存标准值提出了很大的疑惑。在这里,我们也分享下UWA内存标准的制定规则。

(1)150MB的总体内存标准主要由以下两个因素得出:

  • 经过了大量的项目优化后总结而得。其实,对于目前市场主流的Unity游戏来说,其内存占用主要集中在120~200MB。同时,顾及到iPhone4和512MB/768MB等低端Android机型,其应用的自身总体内存占用不可超过200MB(iPhone4的安全线应该在180MB左右),所以我们将Reserved Total设定在150MB,这是Unity引擎的自身内存分配,以保证App在使用到的系统库后,其OS中的整体内存也在200MB以下。
  • 某些渠道对Android游戏的PSS内存进行了严格的限制。一般要求游戏的PSS内存在200MB以下。这是我们将Reserved Total内存设定在150MB的另外一个重要原因。

(2)当总体内存设定为150MB后,我们进一步对其具体分配进行了设定。但需要说明的是,这里的内存分配其实并没有严格的公式来进行论证,仅是我们在大量的项目优化工作中提炼出的经验值。目前,项目较为合理的内存分配如下:

  • 纹理资源: 50 MB
  • 网格资源: 20 MB
  • 动画片段: 15 MB
  • 音频片段: 15 MB
  • Mono堆内存: 40 MB
  • 其他: 10 MB

需要指出的是,150MB中并没有涵盖较为复杂的字体文件(比如微软雅黑)以及Text Asset,这些需要根据游戏需求而定。

(3)目前的UWA内存标准是较为苛刻的,对于中高端设备而言,其内容允许量其实要比150MB要大得多。但我们坚持认为,在研发过程中,一个严苛的标准对于一个项目来说是一件好事。至少,它可以为大家提个醒,让大家时刻关注自己的问题。据我们了解,目前的三到五线城市,其低端手机的覆盖率还是相当高的。同时,对于中高端移动设备,我们仍在不断试验和研究中。我们希望在不久的将来可以做到针对各种不同档次的机型都给出一个更为合理的推荐值,从而让大家更为简单地对内存进行管理。

以上所说的是游戏项目中主要的内存分配情况,希望读到此处的你,可以更加了解Unity项目的内存开销和潜在问题,并对自己的项目进行更有针对性的检测。

 

有三个更为重要的地方需要研发团队关注:内存泄露、Mono无效堆内存开销和资源冗余。这几乎是所有团队在研发过程中都会遇到的问题。今天我们就来详细说一说这些问题的解决方案。

作者:张鑫 链接:https://zhuanlan.zhihu.com/p/21913837 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

内存泄露

内存泄露是开发人员在项目研发过程中最常见也最不愿遇到的问题。就目前来看,大家对于判断项目是否存在内存泄露仍然存在一些误区:

  • 误区一 我的项目进出场景前后内存回落不一致,比如进入场景后,内存增加40MB,出来后下降30MB,仍有10MB内存没有返回给系统,即说明内存存在泄露情况。
  • 误区二 我的项目在进出场景前后,Unity Profiler中内存回落正常,但Android的PSS数值并没有完全回落(出场景后的PSS值高于进场景前的PSS值),即说明内存存在泄露情况。

以上是我们遇到的开发团队反馈给我们的典型问题。相信大多数开发团队都会遇到类似的情况。在此有必要说明一下,以上两种情况均不能表明内存存在泄漏问题。即便内存在一段时间始终保持增长的趋势,也不能简单地判定其存在内存泄露。因为造成内存不能完全回落的情况有很多,比如资源加载后常驻内存以备后续使用、Mono堆内存的只升不降等等,这些均可造成内存无法完全回落。一般来说,我们推荐的判断内存是否泄漏的方法如下:

一、检查资源的使用情况,特别是纹理、网格等资源的使用

在我们进行过的项目深度优化过程中,资源泄漏是内存泄露的主要表现形式,其具体原因是用户对加载后的资源进行了储存(比如放到Container中),但在场景切换时并没有将其Remove或Clear,从而无论是引擎本身还是手动调用Resources.UnloadUnusedAssets等相关API均无法对其进行卸载,进而造成了资源泄露。对于这种情况的排查相当困难,这是因为项目中的资源量过于巨大,泄露资源往往很难定位。因此,我们在UWA测评报告中对项目中的每个资源都进行了详细的监控,并通过“生命周期”这一衡量指标让大家可以清楚地了解到每个资源在项目运行过程中的使用范围。

 

这样,大家可以通过资源的“生命周期”属性来快速查看有哪些资源是“常驻”内存的,并且判断该资源是“预加载”资源还是“泄露”资源。

同时,项目中所使用的总资源数量往往是成百上千的,让大家逐个资源检查过来是一件很费力的事情。所以,我们推出了资源的“场景比较”功能。建议大家通过以下两种方式进行资源比较,以便更快地找到存在“泄露”问题的资源

  • 同种类型场景或同一场景进行比较

一般来说,同种场景或同一场景的资源使用应该是较为固定的,比如游戏项目中的主城场景或主界面场景。通过比较不同时刻同一场景的资源信息,可以快速帮你找到其资源使用的差异情况。这样,你只需判断这些“差异”资源的存在是否合理,即可快速判定是否存在资源泄露,已经具体的泄露资源。

  • 不同类型场景进行比较

除一些常驻资源外,不同类型的场景,其资源使用是完全不同的。比如,游戏中主城和战斗副本的资源,除少部分常驻内存的资源外,二者使用的绝大部分资源应该是不一致的。所以,通过比较两种不同类型的场景,你可以直接查看比较结果中的“共同资源”,并判断其是否确实为预先设定好的常驻资源。如果不是,则它很可能是“泄露”资源,需要你进一步查看项目的资源管理是否存在漏洞。

 

二、通过Profiler来检测WebStream或SerializedFile的使用情况

AssetBundle的管理不当也会造成一定的内存泄露,即上一场景中使用的AssetBundle在场景切换时没有被卸载掉,而被带入到了下一场场景中。对于这种情况,建议直接通过Profiler Memory中的Take Sample来对其进行检测,通过直接查看WebStream或SerializedFile中的AssetBundle名称,即可判断是否存在“泄露”情况。

 

三、通过Android PSS/iOS Instrument反馈的App线程内存来查看

承接上述“误区二”中的说法,“Unity Profiler中内存回落正常,但Android的PSS数值并没有完全回落”是有可能的,这是因为Unity Profiler反馈的是引擎的真实分配的物理内存,而PSS中记录的则包括系统的部分缓存。一般情况下,Android或iOS并不会及时将所有App卸载数据进行清理,为了保证下次使用时的流畅性,OS会将部分数据放入到缓存,待自身内存不足时,OS Kernel会启动类似LowMemoryKiller的机制来查询缓存甚至杀死一些进程来释放内存。因此,并不能通过一两次的PSS内存没有完全回落来说明内存泄露问题。

我们推荐的测试方式是在两个场景之间来回不停切换,比如主城和战斗副本间。理论上来说,多次切换同样的场景,如果Profiler中显示的Unity内存回落正常,那么其PSS/Instrument的内存数值波动范围也是趋于稳定的,但如果出现了PSS/Instrument内存持续增长的情况,则需要大家注意了。这可能有两种可能:

  • Unity引擎自身的内存泄露问题。这种概率很小,之前仅在少数版本中出现过。

  • 第三方插件在使用时出现了内存泄露。这种概率较大,因为Profiler仅能对Unity自身的内存进行监控,而无法检测到第三方库的内存分配情况。因此,在出现上述内存问题时,建议大家先对自身使用的第三方库进行排查。

无效的Mono堆内存开销

目前,Unity所使用的Mono版本中存在一个较大的问题,即内存一旦分配,则不会再返回给系统。这就衍生出另外一个问题—— 无效的Mono堆内存。它是Mono所分配的堆内存,但却没有被真正利用上,因此称之为“无效”。那么,如何查看我的项目中是否存在较大量的“无效堆内存”呢

在UWA测评报告中,我们提供了内存随项目运行的分配情况,如下图所示。其中,蓝线和紫线的分离情况,反映了无效堆内存的分配大小。比如,图中所选中时刻,蓝线的Reserved Total为当前项目所占据的总物理内存,而紫线的Used Total为当前项目所使用的总物理内存,这说明当前项目中的空闲内存为57.1MB(200.4-143.3),而这其中主要由两部分组成,空闲的Unity引擎内存和无效的Mono堆内存。其中,空闲的Unity内存为17.1MB(92.0-74.9),所以当前所选帧的无效Mono堆内存为40.0MB。并且,从图中可以看出,蓝线和紫线在运行过程中一直分得较开,这说明一直存在不小的Mono堆内存处于“无效”状态。这是一件很浪费的事情,特别是对于内存寸土寸金的移动设备而言。

 

 

那么,我们应该如何避免或减少过多“无效堆内存”的分配呢?我们推荐的做法如下:

  • 避免一次性堆内存的过大分配。Mono的堆内存也是“按需”逐步进行分配的。但如果一次性开辟过大堆内存,比如New一个较大Container、加载一个过大配置文件等,则势必会造成Mono的堆内存直接冲高,所以研发团队对堆内存的分配需要时刻注意;

  • 避免不必要的堆内存开销。UWA测评报告中将项目运行过程中堆内存分配Top10函数进行罗列,限于篇幅,我们不再此处进行一一赘述,研发团队可以直接查看之前一篇的内存优化相关文章。

资源冗余

在内存管理方面,还有一个大家必须关注的话题——资源冗余。在我们测评过的大量项目中,95%以上的项目均存在不同程度的资源冗余情况。所谓“资源冗余”,是指在某一时刻内存中存在两份甚至多份同样的资源。导致这种情况的出现主要有两种原因:

一、AssetBundle打包机制出现问题

同一份资源被打入到多份AssetBundle文件中。举个例子,同一张纹理被不同的NPC所使用,同时每个NPC被制作成独立的AssetBundle文件,那么在没有针对纹理进行依赖打包的前提下,就会出现该张纹理出现在不同的NPC AssetBundle文件中。当这些AssetBundle先后被加载到内存后,内存中即会出现纹理资源冗余的情况。对此,我们建议研发团队在发现资源冗余问题后,对相关AssetBundle的制作流程一定要进行检查。

同时,我们在UWA测评中为每个资源引入了一个衡量指标——“数量峰值”。它指的是同一资源在同一帧中出现的最大数量。如果大于1,则说明该资源很可能存在 “冗余资源”。大家可以通过这一列进行排序,即可立即查看项目中的资源冗余情况。

 

 

二、资源的实例化所致

在Unity引擎中,当我们修改了一些特定GameObject的资源属性时,引擎会为该GameObject自动实例化一份资源供其使用,比如Material、Mesh等。以Material为例,我们在研发时经常会有这样的做法:在角色被攻击时,改变其Material中的属性来得到特定的受击效果。这种做法则会导致引擎为特定的GameObject重新实例化一个Material,后缀会加上(instance)字样。其本身没有特别大的问题,但是当有改变Material属性需求的GameObject越来越多时(比如ARPG、MMORPG、MOBA等游戏类型),其内存中的冗余数量则会大量增长。如下图所示,随着游戏的进行,实例化的Material资源会增加到333个。虽然Material的内存占用不大,但是过多的冗余资源却为Resources.UnloadUnusedAssets API的调用效率增加了相当大的压力。

 

一般情况下,资源属性的改变情况都是固定的,并非随机出现。比如,假设GameObject受到攻击时,其Material属性改变随攻击类型的不同而有三种不同的参数设置。那么,对于这种需求,我们建议你直接制作三种不同的Material,在Runtime情况下通过代码直接替换对应GameObject的Material,而非改变其Material的属性。这样,你会发现,成百上千的instance Material在内存中消失了,取而代之的,则是这三个不同的Material资源。其中的益处,对于能够阅读到这里的你来说,应该已经不需要我多说了。:)

以上则是我们在内存优化工作中的经验和心得,希望它对你的项目研发有所帮助。优化永远没有统一的标准方案,只有最适合你项目的方案,希望大家可以活学活用,不要放过任何一处让你感觉“不对劲”的地方。最后提醒大家——“勿以善小而不为,勿以恶小而为之”,共勉!

转载自:https://zhuanlan.zhihu.com/p/21913837 来源:知乎





====new

前言:一提到内存这个东西我相信很多做技术的都头大,我也不例外,如何平衡内存值和手游的表现真的是需要去反反复复去琢磨的。下面记录下我们游戏的内存优化以及一些技巧

  • 首先我们需要一个目标,也就是我们需要把我们游戏优化到什么程度算是比较能接受的?

这个问题要看游戏类型把,比如我们这种3d arpg的游戏还是带水墨风格的就会非常吃力,因为很多水墨边都是半透明的资源。(换句话说我们这个能优化下来你的也一定可以做到)。我们综合了下游戏的表现和我们的硬件水平我们把内存值定在了250M 可用内存在500M的样子(王者荣耀差不多是这个数值)。我相信这个值目前的百元机随便达到了。

  • 常规的技巧:

你关闭了图片的mipmap 了吗? 这个东西是图片映射的时候需要用到的,但是我们的ui一般用不到。 
你关闭了图片的write/read 功能了吗? 这个东西一般情况也用不到,因为你一般情况不会通过代码去重组图片 
你的动作有勾选Optimize GameObjects 吗? 建议你关闭把,这个会很大程度印象你动画骨骼刷新,当然你要注意别把你的挂点也给关闭了。(轻松写个小插件做到) 
你的模型有勾选write/read功能吗? 有关闭表情吗 ? 请关闭把,这些东西都不要的 
你的其他贴图的格式都正确设置了吗? 请按照不同类型的图片格式分包设置把,不然你的内存会很容易爆掉了。

  • 上面都是一些常规的技巧,也就是我们所说的一般人都会的,如果你不会那么请对着做下,对你的内存会有非常大的影响。下面我要说说我们游戏中遇到的最大的问题。

第一个:Assetbundle的错误理解导致seriazedfile 值非常大。每个Assetbundle在加载的时候都会生成一个几十k的镜像内存,如果你不即使清理掉这部分你的内存就会非常大(unload(false))

第二个:我们游戏逻辑的bug,一般请看我相信我们都会有pool 这个东西,我们的也不例外搞了开源的pool 进行特效的缓存,而最后发现pool这个东西是根据名字来的,而不是更具instance id 来进行区别这个对象,所以导致pool 没有发挥出他应该有的功能。内存无限放大

说完了上面的问题,其实最重要的是我们如何来定位问题,如何找到对应的问题的原因,其实也没有特别的办法,请一定使用手机端的profiler功能,如果你非要使用电脑端来查看内存使用那么我只能说你在白忙活,电脑端统计得毫无卵用~具体如何使用请翻阅我之前的blog。


=====new

Unity手游的性能优化过程更像是一门时空转换的艺术, 持续在CPU和内存之间取得一个平衡。空间不足时则需要释放一些无用数据,以获得更优的空间使用率;时间太长时就需要降低不必要的函数开销。以下是腾讯游戏在Unity游戏开发过程中内存和性能优化的一系列解决策略和方法。

贴图:

控制贴图大小,尽量不要超过 1024 x1024

尽量使用2n次幂大小的贴图,否则GfxDriver里会有2份贴图;

尽量使用压缩格式减小贴图大小;

若干种贴图合并技术;

去除多余的alpha通道;

不同设备使用不同的纹理贴图,分层显示;

模型:

尽量控制模型的面数,小于1500会比较合适;

不同设备使用不同的模型面数;

尽量保持在30根骨骼内;

一个网格不要超过3material

动画:

l N种动画压缩方法;

尽量减少骨骼数量;

声音:

采用压缩MP3  wav

资源方面的优化:

使用 Resource.Load 方法在需要的时候再读取资源;

各种资源在使用完成后,尽快用Resource.UnloadAssetUnloadUnusedAsset卸载掉;

灵活运用AssetBundleLoadUnload方法动态加载资源,避免主要场景内的初始化内存占用过高;(实现起来真的很难

采用www加载了AssetBundle后,要用www.Dispose 及时释放;

在关卡内谨慎使用DontDestroyOnLoad,被标注的资源会常驻内存;

代码的优化:

尽量避免代码中的任何字符串连接,因为这会给GC带来太多垃圾;

用简单的“for”循环代替“foreach”循环;

为所有游戏内的动态物体使用内存对象池,可以减少系统开销和内存碎片,复用对象实例,构建自己的内存管理模式,减少InstantiateDestory

尽量不使用LINQ命令,因为它们一般会分配中间缓器,而这很容易生成垃圾内存;

将引用本地缓存到元件中会减少每次在一个游戏对象中使用 “GetComponent” 获取一个元件引用的需求;

减少角色控制器移动命令的调用。移动角色控制器会同步发生,每次调用都会耗损较大的性能;

最小化碰撞检测请求(例如ray castssphere checks),尽量从每次检查中获得更多信息;

l AI逻辑通常会生成大量物理查询,建议让AI更新循环设置低于图像更新循环,以减少CPU负荷;

要尽量减少Unity回调函数,哪怕是空函数也不要留着;(例如空的UpdateFixedUpdate函数)

尽量少使用FindObjectsOfType函数,这个函数非常慢,尽量少用且一定不要在Update里调用;

千万一定要控制mono堆内存的大小;

转自:http://blog.csdn.net/wetest_tencent/article/details/51784782



==================================================================================================================

https://www.cnblogs.com/Yellow0-0River/p/4238748.html

对项目优化有很多,如:mesh合并,减少DrawCall和模型骨骼以及物理计算,合并材质球,优化代码等等。

优化:

  1. 更新不透明贴图的压缩格式为ETC 4bit,因为android市场的手机中的GPU有多种,每家的GPU支持不同的压缩格式,但他们都兼容ETC格式。
  2. 对于透明贴图,我们只能选择RGBA 16bit 或者RGBA 32bit。
  3. 减少FPS

在ProjectSetting-> Quality中的VSync Count 参数会影响你的FPS,EveryVBlank相当于FPS=60,EverySecondVBlank= 30;这两种情况都不符合游戏的FPS的话,我们需要手动调整FPS,首先关闭垂直同步这个功能,然后在代码的Awake方法里手动设置FPS(Application.targetFrameRate= 45;)

降低FPS的好处:

  • 省电,减少手机发热的情况;
  • 能都稳定游戏FPS,减少出现卡顿的情况。
  1. 当我们设置了FPS后,再调整下Fixed timestep这个参数,这个参数在ProjectSetting->Time中,目的是减少物理计算的次数,来提高游戏性能。
  2. 尽量少使用Update ,LateUpdate, FixedUpdate,这样也可以提升性能和节省电量。多使用事件(不是SendMessage,使用自己写的,或者C#中的事件委托)。
  3. 待机时,调整游戏的FPS为1,节省电量。
  4. 图集大小最好不要高于1024,否则游戏安装之后、低端机直接崩溃、原因是手机系统版本低于2.2、超过1000的图集无法读取、导致。2.2 以上没有遇见这个情况。注意手机的RAM 与 ROM、小于 512M的手机、直接放弃机型适配。
    1. VSCount 垂直同步

unity3d中新建一个场景空的时候,帧速率(FPS总是很低),大概在60~70之间。一直不太明白是怎么回事,现在基本上明白了。我在这里解释一下原因,如有错误,欢迎指正。

在Unity3D中当运行场景打开Profiler的时候,我们会看到VSync这一项占了很大的比重。这个是什么呢,这个就是垂直同步,稍后再做解释。我们可以关闭VSync来提高帧速率,选择edit->project settings->Quality。在右侧面板中可以找到VSyncCount,把它选成Don"t Sync。这就关闭了VSync(垂直同步),现在在运行场景看看,帧速率是不是提高很多。

现在来说说什么是垂直同步,要知道什么是垂直同步,必须要先明白显示器的工作原理,

显示器上的所有图像都是一线一线的扫描上去的,无论是隔行扫描还是逐行扫描,显示器都有两种同步参数——水平同步和垂直同步。

  • 什么叫水平同步?什么叫垂直同步?

    垂直和水平是CRT中两个基本的同步信号,水平同步信号决定了CRT画出一条横越屏幕线的时间,垂直同步信号决定了CRT从屏幕顶部画到底部,再返回原始位置的时间,而恰恰是垂直同步代表着CRT显示器的刷新率水平。

  • 为什么关闭垂直同步信号会影响游戏中的FPS数值?

    如果我们选择等待垂直同步信号(也就是我们平时所说的垂直同步打开),那么在游戏中或许强劲的显卡迅速的绘制完一屏的图像,但是没有垂直同步信号的到达,显卡无法绘制下一屏,只有等85单位的信号到达,才可以绘制。这样FPS自然要受到操作系统刷新率运行值的制约。而如果我们选择不等待垂直同步信号(也就是我们平时所说的关闭垂直同步),那么游戏中作完一屏画面,显卡和显示器无需等待垂直同步信号就可以开始下一屏图像的绘制,自然可以完全发挥显卡的实力。但是不要忘记,正是因为垂直同步的存在,才能使得游戏进程和显示器刷新率同步,使得画面更加平滑和稳定。取消了垂直同步信号,固然可以换来更快的速度,但是在图像的连续性上势必打折扣。这也正是很多朋友抱怨关闭垂直后发现画面不连续的理论原因。

  1. 合并材质球

unity 3d中每导入一次模型就多一个材质球,可我的这些模型都是共用一张贴图的就想共用一个材质球,所以每次都要删除再附上,很麻烦。怎么才能合并这些材质球?

采用TexturePacking吧

  • 遍历gameobject,取出material,并根据shader来将material分类
  • 调用Unity自带的PackTextures函数来合并每个shader分类中的material所对应的textures(PackTextures函数有缺陷,不过可以将就用)
  • 根据合并的大的texture来更新原有模型的texture、material已经uv坐标值。

需要注意的是:需要合并的纹理应该是物体在场景中距离相近的,如果物体在场景中的距离较远,则不建议合并纹理,因为这样做很有可能非但起不到优化的作用,反而降低了运行效率。

  1. mesh合并

分为2种方式合并

  1. 自带的合并必须勾选静态。所有被勾选了"Static"的GameObject,其中的Mesh Filter中的mesh都会被合并到 "Combined Mesha (root: scene)" 中
  2. 也可以用脚本来合并mesh 。

public class MyClass : MonoBehaviour

{

    void Start ()

    {

        MeshFilter []meshFilters = GetComponentsInChildren<MeshFilter> ();

        CombineInstance[]combine = new CombineInstance[meshFilters.Length];

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

            combine [i].mesh =meshFilters [i].sharedMesh;

            combine[i].transform = meshFilters [i].transform.localToWorldMatrix;

            meshFilters[i].gameObject.active = false;

        }

           transform.GetComponent<MeshFilter> ().mesh = new Mesh ();

           transform.GetComponent<MeshFilter> ().mesh.CombineMeshes(combine);

            transform.gameObject.active= true;

    }

}

  1. 先在 Unity 中建立 空物件 ( Empty )
  • 再创建2个 Cube 方块,并放入 空物件底下 (可以改成你自己的模型)
  • 把 MyClass 代码丟进 空物件上 。
  • (可选) 建立一个 Material材质,并且丢进 空物件上
  • 执行前后

    角色Material数量2-3个

    骨骼数量小于30个

    面片数量300-1500

    一般角色应该没有IK结点

    这是因为角色的动作大多数都是事先设定好的,并不需要经过IK操作来进行实时计算(Rogdoll除外),所以在模型导入时,不要将IK结点一起导入。

  1. 静态实体

不要附加AnimationComponent在静态实体上附加Animation部件虽然对结果没有影响,但却会增加一定的CPU开销来调用这一组件,所以尽量去掉该组件。

  1. 网格顶点数小于500
  2. UV值范围尽量不要超过(0,1)区间尽量保证UV值不越界,这对于将来的纹理拼合优化很有帮助。
    1. 地形

地形的分辨率大小

长宽均尽量小于257。这是因为地形太大,会造成大量顶点数据,给你的内存带宽造成一定的影响,在目前的ios设备中,内存带宽是非常有限的,需要尽量节省。同时,如果用Unity自带的地形,一定也要使用Occlusion Culling,因为Unity的刷地形工具虽然方便,但却是framekiller,刷过之后,你会发现drawcall增加的非常多。

  1. 纹理
  • 混合纹理数量不要超过4。地形的混合操作是很耗时的,应该尽量避免。能合并的纹理尽量合并。
  • 纹理格式建议png或tga。不用转成ios硬件支持的PVRTC格式,因为Unity在发布时会帮你自动转的。
  • 纹理尺寸,长宽小于1024。同时应该尽可能地小,够用就好,以保证纹理对内存带宽的影响达到最小。
  • 支持Mipmap,建议生成Mipmap。虽然这种做法会增加一些应用程序的大小,但在游戏运行时,系统会根据需求应用Mipmap来渲染,从而减少内存带宽。
  • 检查Alpha值 ,如果纹理的alpha通道均为1,则用RGB的24位纹理来代替RGBA的32位纹理。(据说Unity内部会进行自动检测)

    16.  光源

光源"Important"个数建议1个,一般为方向光。"Important"个数应该越小越少。个数越多,drawcall越多。

Pixel Light数目1-2个。

    17.  粒子特效

屏幕上的最大粒子数建议小于200个粒子。

每个粒子发射器发射的最大粒子数建议不超过50个。粒子大小如果可以的话,粒子的size应该尽可能地小。因为Unity的粒子系统的shader无论是alpha test还是alpha blending都是一笔不小的开销。同时,对于非常小的粒子,建议粒子纹理去掉alpha通道。

尽量不要开启粒子的碰撞功能。非常耗时。

    18.  音频

游戏中播放时间较长的音乐(如背景音乐)使用.ogg或.mp3的压缩格式。较短音乐(如枪声)使用.wav和.aif的未压缩音频格式。

    19.  相机

裁剪平面

将远平面设置成合适的距离。远平面过大会将一些不必要的物体加入渲染,降低效率。根据不同的物体设置不同的远裁剪平面

Unity提供了可以根据不同的layer来设置不同的viewdistance,所以我们可以实现将物体进行分层,大物体层设置的可视距离大些,而小物体层可以设置地小些,另外,一些开销比较大的实体(如粒子系统)可以设置得更小些等等。

    20.  碰撞

如果可以的话,尽量不用MeshCollider,以节省不必要的开销。如果不能避免的话,尽量用减少Mesh的面片数,或用较少面片的代理体来代替。

   21.  其他

  • 1) Drawcall

尽可能地减少Drawcall的数量。IOS设备上建议不超过100。减少的方法主要有如下几种:Frustum Culling,Occlusion Culling,Texture Packing。

  1. Frustum Culling是Unity内建的,我们需要做的就是寻求一个合适的远裁剪平面;
  2. Occlusion Culling,遮挡剔除,Unity内嵌了Umbra,一个非常好OC库。但OcclusionCulling也并不是放之四海而皆准的,有时候进行OC反而比不进行还要慢,建议在OC之前先确定自己的场景是否适合利用OC来优化;
  3. Texture Packing,或者叫TextureAtlasing,是将同种shader的纹理进行拼合,根据Unity的static batching的特性来减少draw call。建议使用,但也有弊端,那就是一定要将场景中距离相近的实体纹理进行拼合,否则,拼合后很可能会增加每帧渲染所需的纹理大小,加大内存带宽的负担。这也就是为什么会出现"DrawCall降了,渲染速度也变慢了"的原因。
  • 非运动物体尽量打上Static标签
  1. Unity在运行时会对static物体进行自动优化处理,所以应该尽可能将非运行实体勾上static标签。
  • 场景中尽可能地使用prefab
  1. 尽可能地使用prefab的实例化物体,以降低内存带宽的负担。检查实体的PrefabType,尽量将其变成PrefabInstance,而不是ModelPrefabInstance。
  2. 移动平台相对于PC机,具有体积小,计算弱,带宽少的特点。因此做手机游戏的开发,优化的方向,与力度对比PC游戏都有所区别。必须要做到优化流程,合理利用资源。
  3. 目前在手机上面,还不能够像PC游戏那样追求高质量渲染效果,为了让手机不那么容易发烫,还要控制cpu,gpu,不能让他们全速运算。

    22.材质方面:

  • 纹理方面,建议使用压缩纹理,
  1. Android上面使用ETC1,苹果上面使用PVRTC。
  • UV坐标控制在0到1之间,人物模型面数控制在1500内,骨骼控制在30个以内。
  • 场景中使用一个主光(不能再多了)。
  • 尽量减少alphaTest和alphaBlend材质的使用。在手机上,这是很杀效率的。

    23.骨骼动画方面:

在动画方面可以考虑不使用插值,固定的帧率的动画。如果要做插值,考虑使用四元数(表示旋转)和向量(表示位移)来做插值。四元数做插值速度比矩阵来的快,Slerp提供了平滑插值。

    24.优化的常规技巧

剖析你的游戏。不要花费时间来优化那些晦涩的代码或者缩减图形文件的大小,除非这是你游戏的瓶颈。第一次剖析你的游戏将会使你发现你游戏的瓶颈。Apple"s Shark是一个很好的用来剖析基于OpenGL的程序的工具。再次剖析你的游戏。优化之后不要忘记再剖析一次你的游戏,这样可以检查你所做的优化是否达到了预期的效果。当然,这样做也可能会使你发现更多的瓶颈。流程第一、性能第二。花费时间来使你游戏的创建尽可能地流畅。尽可能快地修正游戏中的错误将会使你后期更容易优化你的游戏。

    25. 在Scene View中测试场景。

这样做将会使你清楚了解这个场景中的物体或者附加在物体上的脚本是否降低了游戏性能。如果Scene View反应迟钝,那么有可能是图形方面的原因,如果Scene View反应不迟钝,那么瓶颈可能出在脚本或者物理系统上。

    26.禁用指定游戏物体。

在play模式下,尝试禁用并启用游戏物体来排查出游戏慢的原因。

    27.网格

  • 如果可能的话,把相邻的物体(网格)合并为一个只有一个材质的物体(网格)。比如,你的游戏中包含一个桌子,上面有一堆东西,你完全可以在3D程序中将它们合并在一起(这可能也需要你将这些物体的纹理合并为一个大的纹理集)。减少需要渲染的物体的数量可以极大地提高游戏性能。
  • 不要有不必要的网格。如果你的游戏场景中有一个人物,那么他应该是一个网格。如果你有一个船,那么它也应该只是一个网格。
  • 每一个网格只用一种材质。
  • 使用极少的面数的网格(比如500个多边形以下)。
  • 最好把你人物的三角面数量控制在1500-2000个之间。这个数量可以说是游戏质量和性能之间一个均衡值。如果你的模型有四边形,那么在导入模型的时候,引擎将会把每个四边形变为两个三角形。

    28.光照

  • 像素光。

像素光可以让你的游戏看起来效果很牛逼,但是不要使用过多的像素光。在你的游戏中可以使用质量管理器来调节像素光的数量来取得一个性能和质量的均衡点.

性能占用顺序:聚光灯>点光源>平行光。

一个好的点亮场景的方法就是先得到你想要的效果,然后看看哪些光更重要;在保持光效的前提下看看哪些光可以去掉。

  • 点光源和聚光灯只影响它们范围内的网格。

如果一个网格处于点光源或者聚光灯的照射范围之外,并且光源的attenuate开关是打开的,那么这个网格将不会被光源所影响,这样就可以节省性能开销。这样做理论上来讲可以使用很多小的点光源而且依然能有一个好的性能,因为这些光源只影响一小部分物体。

  • 一个网格在有8个以上光源影响的时候,只响应前8个最亮的光源。

8.        贴图

1)        在外观不变的前提下,贴图大小越小越好。

如果你的显卡的显存不够大的话,你游戏中的贴图将会被转存到系统内存中,在显卡调用它们的时候再传到显卡中。对于比较新的电脑来说,内存和显卡之间有足够的带宽来达到一个很好的性能;如果你很无耻地用了巨多的大图片的话,在低显存的电脑上运行你的游戏的时候,你的游戏必然会挂掉。倒是没有必要在图形编辑软件中调整贴图的大小。你可以在unity导入贴图的时候进行调整。

2)        不要使用低质量的图片。

在小播放界面的游戏中使用低质量的jpeg图片或者低色彩的png图片亦或是gif图片没什么问题。在发布游戏的时候,引擎会自动压缩这些图片,多重压缩和解压将会降低图片的质量,所以最好保持贴图文件的分辨率为原始分辨率。这样就会减少多重压缩和解压所导致的图片失真现象。

9.        Shaders

1)        多重效果的shader就比看起来样式很单一的shader要更耗费资源。同样在一个拥有贴图和光反射的物体上,使用VertexLit Diffuse shader无疑是最省资源的。

在美术制作场景的过程中,会使用到大量的粒子系统。比如场景中的火把。在我们的一个地下城场景中,美术们放置了大量的火把。整个场景中的各个地方,有100来个火把。unity中,在摄像机范围外的粒子系统虽然不会被绘制。但是update是一直持续的。这也就意味着,这100多个火把,不论是否可见都在更新。这个设计应该是很不合理的,在我看过的其他引擎中,都会有一个开关,来控制不可见的粒子系统是否需要update。有的粒子系统在不可见的时候需要更新,比如爆炸。有的不需要更新,比如火堆火把。为了避免不必要的update开销,尤其是最后游戏是要发布到页游平台(web player只能使用一个cpu的核)。于是写了一个脚本,控制不可见的粒子系统就不更新。

该脚本主要是用到了2个MonoBehaviour的函数。OnBecameInvisible() 当变为不可见和OnBecameVisible() 当变成可见。要这2个函数起作用的前提是,该GameObject绑定了MeshRender组件。所以,我们要在粒子系统的GameObject放置在一个GameObject  下,且给该GameObject绑定一个MeshRender与 MeshFilter。MeshFilter中的mesh可以随便找个cube。在Start() 的时候,把最GameObject的scale设置为很小,以保证该cube不被看见。其实遍历所有的child,把active设置为false。在OnBecameVisible 中遍历所有child,把active设置为true。在OnBecameInvisible中遍历所有child,把active设置为false。

10.    Unity 性能优化 DrawCall

Unity(或者说基本所有图形引擎)生成一帧画面的处理过程大致可以这样简化描述:引擎首先经过简单的可见性测试,确定摄像机可以看到的物体,然后把这些物体的顶点(包括本地位置、法线、UV等),索引(顶点如何组成三角形),变换(就是物体的位置、旋转、缩放、以及摄像机位置等),相关光源,纹理,渲染方式(由材质/Shader决定)等数据准备好,然后通知图形API——或者就简单地看作是通知GPU——开始绘制,GPU基于这些数据,经过一系列运算,在屏幕上画出成千上万的三角形,最终构成一幅图像。

在Unity中,每次引擎准备数据并通知GPU的过程称为一次DrawCall。这一过程是逐个物体进行的,对于每个物体,不只GPU的渲染,引擎重新设置材质/Shader也是一项非常耗时的操作。因此每帧的Draw Call次数是一项非常重要的性能指标,对于iOS来说应尽量控制在20次以内,这个值可以在编辑器的Statistic窗口看到。

Unity内置了Draw Call Batching技术,从名字就可以看出,它的主要目标就是在一次Draw Call中批量处理多个物体。只要物体的变换和材质相同,GPU就可以按完全相同的方式进行处理,即可以把它们放在一个Draw Call中。Draw Call Batching技术的核心就是在可见性测试之后,检查所有要绘制的物体的材质,把相同材质的分为一组(一个Batch),然后把它们组合成一个物体(统一变换),这样就可以在一个Draw Call中处理多个物体了(实际上是组合后的一个物体)。

但Draw CallBatching存在一个缺陷,就是它需要把一个Batch中的所有物体组合到一起,相当于创建了一个与这些物体加起来一样大的物体,与此同时就需要分配相应大小的内存。这不仅会消耗更多内存,还需要消耗CPU时间。特别是对于移动的物体,每一帧都得重新进行组合,这就需要进行一些权衡,否则得不偿失。但对于静止不动的物体来说,只需要进行一次组合,之后就可以一直使用,效率要高得多。

Unity提供了Dynamic Batching和StaticBatching两种方式。Dynamic Batching是完全自动进行的,不需要也无法进行任何干预,对于顶点数在300以内的可移动物体,只要使用相同的材质,就会组成Batch。Static Batching则需要把静止的物体标记为Static,然后无论大小,都会组成Batch。如前文所说,Static Batching显然比Dynamic Batching要高效得多,于是,Static Batching功能是收费的……

要有效利用DrawCall Batching,首先是尽量减少场景中使用的材质数量,即尽量共享材质,对于仅纹理不同的材质可以把纹理组合到一张更大的纹理中(称为Texture Atlasing)。然后是把不会移动的物体标记为Static。此外还可以通过CombineChildren脚本(Standard Assets/Scripts/Unity Scripts/CombineChildren)手动把物体组合在一起,但这个脚本会影响可见性测试,因为组合在一起的物体始终会被看作一个物体,从而会增加GPU要处理的几何体数量,因此要小心使用。

对于复杂的静态场景,还可以考虑自行设计遮挡剔除算法,减少可见的物体数量同时也可以减少Draw Call。

总之,理解DrawCall和Draw Call Batching原理,根据场景特点设计相应的方案来尽量减少Draw Call次数才是王道,其它方面亦然。

11.    Draw Call Batching (绘制调用批处理)

To draw an object on the screen, the engine has to issue adraw call to the graphics API (OpenGL ES in the case of iOS). Every single drawcall requires a significant amount of work on the part of the graphics API,causing significant performance overhead on the CPU side.

在屏幕上渲染物体,引擎需要发出一个绘制调用来访问图形API(iOS系统中为OpenGL ES)。每个绘制调用需要进行大量的工作来访问图形API,从而导致了CPU方面显著的性能开销。

Unity combines a number of objects at runtime and drawsthem together with a single draw call. This operation is called"batching". The more objects Unity can batch together, the betterrendering performance you will get.

Unity在运行时可以将一些物体进行合并,从而用一个绘制调用来渲染他们。这一操作,我们称之为"批处理"。一般来说,Unity批处理的物体越多,你就会得到越好的渲染性能。

Built-in batching support in Unity has significant benefitover simply combining geometry in the modeling tool (or usingtheCombineChildren script from the Standard Assets package). Batching in Unityhappensafter visibility determination step. The engine does culling on eachobject individually, and the amount of rendered geometry is going to be thesame as without batching. Combining geometry in the modeling tool, on the otherhand, prevents effecient culling and results in much higher amount of geometrybeing rendered.

Unity中内建的批处理机制所达到的效果要明显强于使用几何建模工具(或使用Standard Assets包中的CombineChildren脚本)的批处理效果。这是因为,Unity引擎的批处理操作是在物体的可视裁剪操作之后进行的。Unity先对每个物体进行裁剪,然后再进行批处理,这样可以使渲染的几何总量在批处理前后保持不变。但是,使用几何建模工具来拼合物体,会妨碍引擎对其进行有效的裁剪操作,从而导致引擎需要渲染更多的几何面片。

12.    Materials材质

Only objects sharing the same material can be batchedtogether. Therefore, if you want to achieve good batching, you need to share asmany materials among different objects as possible.

只有拥有相同材质的物体才可以进行批处理。因此,如果你想要得到良好的批处理效果,你需要在程序中尽可能地复用材质和物体。

If you have two identical materials which differ only intextures, you can combine those textures into a single big texture - a processoften calledtexture atlasing. Once textures are in the same atlas, you can usesingle material instead.

如果你的两个材质仅仅是纹理不同,那么你可以通过纹理拼合操作来将这两张纹理拼合成一张大的纹理。一旦纹理拼合在一起,你就可以使用这个单一材质来替代之前的两个材质了。

If you need to access shared material properties from thescripts, then it is important to note that modifyingRenderer.material willcreate a copy of the material. Instead, you should useRenderer.sharedMaterialto keep material shared.

如果你需要通过脚本来访问复用材质属性,那么值得注意的是改变Renderer.material将会造成一份材质的拷贝。因此,你应该使用Renderer.sharedMaterial来保证材质的共享状态。

13.    Dynamic Batching动态批处理

Unity can automatically batch moving objects into the samedraw call if they share the same material.

如果动态物体共用着相同的材质,那么Unity会自动对这些物体进行批处理。

Dynamic batching is done automatically and does not requireany additional effort on your side.

动态批处理操作是自动完成的,并不需要你进行额外的操作。

14.    Tips:提醒:

Batching dynamic objects has certain overheadper vertex, sobatching is applied only to meshes containing less than900 vertex attributes intotal.

批处理动态物体需要在每个顶点上进行一定的开销,所以动态批处理仅支持小于900顶点的网格物体。

If your shader is using Vertex Position, Normal and singleUV, then you can batch up to 300 verts and if your shader is using VertexPosition, Normal, UV0, UV1 and           Tangent, then only 180 verts. Please note: attribute count limit mightbe changed in future

如果你的着色器使用顶点位置,法线和UV值三种属性,那么你只能批处理300顶点以下的物体;如果你的着色器需要使用顶点位置,法线,UV0,UV1和切向量,那你只            能批处理180顶点以下的物体。请注意:属性数量的限制可能会在将来进行改变。

Don"t use scale. Objects with scale (1,1,1) and (2,2,2)won"t batch.

不要使用缩放尺度(scale)。分别拥有缩放尺度(1,1,1)和(2,2,2)的两个物体将不会进行批处理。

Uniformly scaled objects won"t be batched withnon-uniformly scaled ones.

统一缩放尺度的物体不会与非统一缩放尺度的物体进行批处理。

Objects with scale (1,1,1) and (1,2,1) won"t bebatched. On the other hand (1,2,1) and (1,3,1) will be.

使用缩放尺度(1,1,1)和 (1,2,1)的两个物体将不会进行批处理,但是使用缩放尺度(1,2,1)和(1,3,1)的两个物体将可以进行批处理。

Using different material instances will cause batching tofail.

使用不同材质的实例化物体(instance)将会导致批处理失败。

bjects with lightmaps have additional (hidden) materialparameter: offset/scale in lightmap, so lightmapped objects won"t bebatched (unless they point to same portions of lightmap)

拥有lightmap的物体含有额外(隐藏)的材质属性,比如:lightmap的偏移和缩放系数等。所以,拥有lightmap的物体将不会进行批处理(除非他们指向lightmap的同一部分)。

Multi-pass shaders will break batching. E.g. Almost allunity shaders supports several lights in forward rendering, effectively doingadditional pass for them

多通道的shader会妨碍批处理操作。比如,几乎unity中所有的着色器在前向渲染中都支持多个光源,并为它们有效地开辟多个通道。

Using instances of a prefab automatically are using thesame mesh and material.

预设体的实例会自动地使用相同的网格模型和材质。

15.    Static Batching静态批处理

Static batching, on the other hand, allows the engine to reducedraw calls for geometry of any size (provided it does not move and shares thesame material). Static batching is significantly more efficient than dynamicbatching. You should choose static batching as it will require less CPU power.

相对而言,静态批处理操作允许引擎对任意大小的几何物体进行批处理操作来降低绘制调用(只要这些物体不移动,并且拥有相同的材质)。因此,静态批处理比动态批处理更加有效,你应该尽量的使用它,因为它需要更少的CPU开销。

In order to take advantage of static batching, you needexplicitly specify that certain objects are static and willnot move, rotate orscale in the game. To do so, you can mark objects as static using the Staticcheckbox in the Inspector:

为了更好地使用静态批处理,你需要明确指出哪些物体是静止的,并且在游戏中永远不会移动、旋转和缩放。想完成这一步,你只需要在检测器(Inspector)中将Static复选框打勾即可。

Using static batching will require additional memory forstoring the combined geometry. If several objects shared the same geometrybefore static batching, then a copy of geometry will be created for eachobject, either in the Editor or at runtime. This might not always be a goodidea - sometimes you will have to sacrifice rendering performance by avoidingstatic batching for some objects to keep a smaller memory footprint. Forexample, marking trees as static in a dense forest level can have seriousmemory impact.

使用静态批处理操作需要额外的内存开销来储存合并后的几何数据。在静态批处理之前,如果一些物体共用了同样的几何数据,那么引擎会在编辑以及运行状态对每个物体创建一个几何数据的备份。这并不总是一个好的想法,因为有时候,你将不得不牺牲一点渲染性能来防止一些物体的静态批处理,从而保持较少的内存开销。比如,将浓密森里中树设为Static,会导致严重的内存开销。

Static batching is only available in Unity iOS Advanced.

静态批处理目前只支持Unity iOS Advanced。

备注:最近一直在研究Unity3D的性能优化问题,这段时间可能会多翻译这方面的文章。

前两天,MadFinger,就是当今iOS与Android上画质最牛逼闪闪的游戏之一——ShadowGun的开发商,令人惊异地放出了一个ShadowGun的样例关卡以及若干可免费使用的Shader,国外同行们的分享精神真的是令人赞叹不已。原文在这里,以下是我的一些摘录和笔记。

首先是一些优化常识。针对图形方面的优化主要包括三角形数量,纹理所占内存,以及Shader,前两项基本没什么好讲的,针对设备机能的限制制定相应的指标即可,所以Shader就成为了图形性能优化的关键。

16.    Alpha blending

在Unity官方文档中讲,由于硬件原因,在iOS设备上使用alpha-test会造成很大的性能开销,应尽量使用alpha-blend代替。这里提到,在同屏使用alpha-blend的面数,尤其是这些面所占屏幕面积的大小,对性能也会造成很大影响。原因是使用alpha-blend的面会造成overdraw的增加,这尤其对低性能设备的影响很大。不过没有购买Pro版,没有Occlusion Culling功能的话,就不必顾虑这一问题了,反正overdraw是必然的。

17.    复杂的Per-pixel shader

Per-pixel shader即Fragment shader,顾名思义是要对每个渲染到屏幕上的像素做处理的shader,如果per-pixel shader比较复杂且需要处理的像素很多时,也就是使用该shader的面占屏幕面积很大时,对性能的影响甚至要超过alpha blending。因此复杂的per-pixel shader只适用于小物体。

下面是对几个Shader的逐一讲解:

Specular map通常都是利用贴图的alpha通道来定义物体表面的光滑程度(反光度),这个shader的特点是per-vertex计算反光度的,有着相当不错的效果的同时比per-pixel的shader性能要高得多。这个shader很适用于关卡环境等占很大区域的模型。

经过优化的动态角色光照和阴影(Light probes和BRDF Shader)传统的Lightmaps无法支持动态物体,对此Unity提供了Light probes技术,预先把动态物体的光照信息保存在代理对象(即Light probes)中,运行时动态物体从距离最近的Probe中获取光照信息。

Unity本身还提供了一个效果非常棒的专为移动设备优化过的角色Shader,支持Diffuse、Specular和Normal maps,并通过一个特殊的脚本生成贴图用于模仿BRDF光照效果。最终产生的效果堪比次时代大作中的角色光影效果。

18.    雾和体积光(Shader Blinking Godrays)

目前在移动设备上要开启真正的雾效基本不可行,ShadowGun的方案是通过简单的网格+透明贴图(称为雾面)来模拟雾效。在玩家靠近时,雾面逐渐变淡,同时fog plane的顶点也会移开(即使完全透明的alpha面也会消耗很多渲染时间)。

使用这个Shader的网格需要经过处理:

顶点的alpha值用于决定顶点是否可以移动(在例子中0为不可动,1为可动)。顶点法线决定移动的方向然后Shader通过计算与观察者的距离来控制雾面的淡入/淡出。这个Shader还可以用来做体积光和其它一些alpha效果。

飞机坠毁的浓烟效果(Shader Scroll 2 Layers Sine Alpha-blended)通过粒子产生浓烟的代价太高,所以ShadowGun中使用了网格+贴图动画来制作这个效果。通过混合两层贴图并让它们交错移动来产生动画效果。其中顶点alpha值用于让网格的边缘看起来比较柔和,同时使用顶点颜色来模拟从火焰到烟雾的过渡效果。带动态效果的天空盒(Shader Scroll 2 Layers Multiplicative)

通过两张贴图的混合和移动产生云的动态效果。

旗帜和衣服的飘动效果(Shader Lightmap + Wind)

同样利用顶点alpha值决定哪些顶点可以移动,然后shader的参数用于调整摆动的方向和速度。

19.    程序方面

1)        务必删除脚本中为空或不需要的默认方法;

2)        只在一个脚本中使用OnGUI方法;

3)        避免在OnGUI中对变量、方法进行更新、赋值,输出变量建议在Update内;

4)        同一脚本中频繁使用的变量建议声明其为全局变量,脚本之间频繁调用的变量或方法建议声明为全局静态变量或方法;

5)        不要去频繁获取组件,将其声明为全局变量;

6)        数组、集合类元素优先使用Array,其次是List;

7)        脚本在不使用时脚本禁用之,需要时再启用;

8)        可以使用Ray来代替OnMouseXXX类方法;

9)        需要隐藏/显示或实例化来回切换的对象,尽量不要使用SetActiveRecursively或active,而使用将对象远远移出相机范围和移回原位的做法;

10)    尽量少用模运算和除法运算,比如a/5f,一定要写成a*0.2f;

11)    对于不经常调用或更改的变量或方法建议使用Coroutines & Yield;

12)    尽量直接声明脚本变量,而不使用GetComponent来获取脚本;

13)    尽量使用整数数字,因为iPhone的浮点数计算能力很差;

14)    不要使用原生的GUI方法;

15)    不要实例化(Instantiate)对象,事先建好对象池,并使用Translate"生成"对象;

20.    模型方面

1)        合并使用同贴图的材质球,合并使用相同材质球的Mesh;

2)        角色的贴图和材质球只要一个,若必须多个则将模型离分离为多个部分;

3)        骨骼系统不要使用太多;

4)        当使用多角色时,将动画单独分离出来;

5)        使用层距离来控制模型的显示距离;

6)        阴影其实包含两方面阴暗和影子,建议使用实时影子时把阴暗效果烘焙出来,不要使用灯光来调节光线阴暗。

7)        少用像素灯和使用像素灯的Shader;

8)        如果硬阴影可以解决问题就不要用软阴影,并且使用不影响效果的低分辨率阴影;

9)        实时阴影很耗性能,尽量减小产生阴影的距离;

10)    允许的话在大场景中使用线性雾,这样可以使远距离对象或阴影不易察觉,因此可以通过减小相机和阴影距离来提高性能;

11)    使用圆滑组来尽量减少模型的面数;

12)    项目中如果没有灯光或对象在移动那么就不要使用实时灯光;

13)    水面、镜子等实时反射/折射的效果单独放在Water图层中,并且根据其实时反射/折射的范围来调整;

14)    碰撞对效率的影响很小,但碰撞还是建议使用Box、Sphere碰撞体;

15)    建材质球时尽量考虑使用Substance;

16)    尽量将所有的实时反射/折射(如水面、镜子、地板等等)都集合成一个面;

17)    假反射/折射没有必要使用过大分辨率,一般64*64就可以,不建议超过256*256;

18)    需要更改的材质球,建议实例化一个,而不是使用公共的材质球;

19)    将不须射线或碰撞事件的对象置于IgnoreRaycast图层;

20)    将水面或类似效果置于Water图层

21)    将透明通道的对象置于TransparentFX图层;

22)    养成良好的标签(Tags)、层次(Hieratchy)和图层(Layer)的条理化习惯,将不同的对象置于不同的标签或图层,三者有效的结合将很方便的按名称、类别和属性来查找;

23)    通过Stats和Profile查看对效率影响最大的方面或对象,或者使用禁用部分模型的方式查看问题到底在哪儿;

24)    使用遮挡剔除(Occlusion Culling)处理大场景,一种较原生的类LOD技术,并且能够"分割"作为整体的一个模型。

21.    三、其它

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







©️2020 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值