UWA

每天一点UWA:第一周

Posted by 蔡华的博客 on July 29, 2017

初衷

  • 首先UWA一直以来都是一个我很喜欢的网站,因为他们在unity的优化方面是非常专业的,而且他们对于技术的分享也是毫不吝啬的。在他们的网站上有很多的技术文章,很久以来都想系统的阅读一遍,但是一直没有去做。这周开始下定决心要从第一篇开始,把UWA所有的技术分享文章读完,这个系列的文章会对每周读到的技术做一个总结,但是因为文章涉及的方向比较杂,因此会简单的用技术进行一个分类。

CPU方面的优化(很有用)

  • 就目前的Unity移动游戏而言,CPU方面的性能开销主要可归结为两大类:引擎模块性能开销和自身代码性能开销。其中,引擎模块中又可细致划分为渲染模块、动画模块、物理模块、UI模块、粒子系统、加载模块和GC调用等等。

渲染模块

  • 减少draw call。减少dc的核心办法在unity里面就是减少材质球。因为如果多个物体共用一个材质球(共同的贴图和shader)那么对于一次draw call来说就是传递顶点数多少的问题。
  • 顶点数的多少引出了另一个问题,传输渲染数据的总线带宽。当大量GameObject满足批处理条件时(900个数据值),unity会合并mesh,但是一个超大的mesh数据传输起来也有瓶颈。因此关于draw call的优化不能是无脑的,需要平衡dc次数和总线带宽。
  • 简化资源
  • 渲染模块在CPU方面的优化方法还有很多,比如LOD、Occlusion Culling和Culling Distance等等

UI模块

  • 文中以NGUI为例进行了函数的分析,但是UGUI应该也有借鉴的意义。
  • 在NGUI的优化方面,UIPanel.LateUpdate为性能优化的重中之重,它是NGUI中CPU开销最大的函数,没有之一。对于UIPanel.LateUpdate的优化,主要着眼于UIPanel的布局,其原则如下:
    • 尽可能将动态UI元素和静态UI元素分离到不同的UIPanel中(UI的重建以UIPanel为单位),从而尽可能将因为变动的UI元素引起的重构控制在较小的范围内;
    • 尽可能让动态UI元素按照同步性进行划分,即运动频率不同的UI元素尽可能分离放在不同的UIPanel中;
    • 控制同一个UIPanel中动态UI元素的数量,数量越多,所创建的Mesh越大,从而使得重构的开销显著增加。比如,战斗过程中的HUD运动血条可能会出现较多,此时,建议研发团队将运动血条分离成不同的UIPanel,每组UIPanel下5~10个动态UI为宜。这种做法,其本质是从概率上尽可能降低单帧中UIPanel的重建开销。
  • PS:结合之前关于UI优化的文章,UI优化的核心在于拆分UI元素。始终不变的归为一类,动态变化的归为一类。对于UGUI来说就是拆分Canvas。
  • PS2:关于UI重建的说明。UI其实是一些3D的quad,这一下就能够理解为什么存在UI的减少drawcall了,这个和模型的减dc完全一个原理。多个UI在一个Canvas下就存在多个UI的mesh合并问题(批处理),但是像血条这样的UI一般用的是progress,它的原理就是默认模式下改变quad的顶点数据来实现进度大小的改变,fxxx模式下是顶点和UV一起变,只要改变了顶点位置,那么mesh必要每次dc的时候都要重新计算。和它一起的那些不变的UI的计算是浪费的。

加载模块

  • 目前加载模块性能开销主要在场景切换,分为前一场景的场景卸载和下一场景的场景加载。
  • 场景卸载:主要是Destroy和Resources.UnloadUnusedAssets两个函数,前者的消耗多少主要取决于事件函数中代码的功能多少。后者耗时开销主要取决于场景中Asset和Object的数量,数量越多,则耗时越慢。

  • 场景加载:主要是资源加载和Instantiate实例化。
    • 资源加载几乎占据了整个加载过程的90%时间以上,其加载效率主要取决于资源的加载方式(Resource.Load或AssetBundle加载)、加载量(纹理、网格、材质等资源数据的大小)和资源格式(纹理格式、音频格式等)等等。不同的加载方式、不同的资源格式,其加载效率可谓千差万别。
    • 在场景加载过程中,往往伴随着大量的Instantiate实例化操作,比如UI界面实例化、角色/怪物实例化、场景建筑实例化等等。在Instantiate实例化时,引擎底层会查看其相关的资源是否已经被加载,如果没有,则会先加载其相关资源,再进行实例化,这其实是大家遇到的大多数“Instantiate耗时问题”的根本原因,这也是为什么我们在之前的AssetBundle文章中所提倡的资源依赖关系打包并进行预加载,从而来缓解Instantiate实例化时的压力。
    • 另一方面,Instantiate实例化的性能开销还体现在脚本代码的序列化上,如果脚本中需要序列化的信息很多,则Instantiate实例化时的时间亦会很长。最直接的例子就是NGUI,其代码中存在很多SerializedField标识,从而在实例化时带来了较多的代码序列化开销。因此,在大家为代码增加序列化信息时,这一点是需要大家时刻关注的。
  • 以上是游戏项目中性能开销最大的三个模块,当然,游戏类型的不同、设计的不同,其他模块仍然会有较大的CPU占用。比如,ARPG游戏中的动画系统和物理系统,音乐休闲类游戏中的音频系统和粒子系统等。

PS:关于场景加载在之前的文章中也有讲到,尤其是AssetBundle系列文章中。根据unity对资源的管理方式,Resources文件夹里面的asset会全部加载到PersistentManager,当然这个过程只是建立InstanceID的过程,但是仍旧消耗了时间。

代码效率

  • 一图胜千言 image

AssetBundle

  • 4.x的内容就不考虑写出来了。
  • AssetBundle5.x怎么制作我在之前的一个blog中写过,可以说5.x极大的简化了AssetBundle的打包流程。
  • 不考虑5.3前的情况,之后的版本建议使用LZ4打包,因为是按需解压加载的。
  • 新增的AppendHashToAssetBundleName选项很不错,可以在生成AssetBundle时加入hash值,便于判定是否有更新,AssetBundleBrowser中支持。
  • 几个注意的点:
    • 新机制打包无法指定Assetbundle.mainAsset,因此无法再通过mainAsset来直接获取资源。
    • 开启DisableWriteTypeTree可能造成AssetBundle对Unity版本的兼容问题,但会使Bundle更小,同时也会略微提高加载速度。
    • abName可通过脚本进行设置和清除,也可以通过构造一个AssetBundleBuild数组来打包。
    • Prefab之间不会建立依赖,即如果Prefab-A和Prefab-B引用了同一张纹理,而他们设置了不同的abName,而共享的纹理并未设置abName,那么Prefab-A和Prefab-B可视为分别打包,各自Bundle中都包含共享的纹理。因此在使用UGUI,开启Sprite Packer时,由于Atlas无法标记abName,在设置UI界面Prefab的abName时就需要注意这个问题。
    • 5.x中加入了Shader stripping功能,在打包时,默认情况下会根据当前场景的Lightmap及Fog设置对资源中的Shader进行代码剥离。这意味着,如果在一个空场景下进行打包,则Bundle中的Shader会失去对Lightmap和Fog的支持,从而出现运行时Lightmap和Fog丢失的情况.而通过将Edit->Project Settings->Graphics下shader Stripping中的modes改为Manual,并勾选相应的mode即可避免这一问题。
    • Shader被打包到不同AssetBundle中了,WarmupAllShaders仅能对当前内存中的Shader进行warm up。后续如果又有Shader加载进来,则仍然会出现CreateGPUProgram操作。
    • PS:这一条在2017中发现custom选项中是全选的。

纹理

  • 对于图片如果可以的话,建议直接将其制作成POT( power of two,即图片的size是2次幂,这个也是unity建议的)图片,而非进行二次转换。ToLarger确实可以将纹理拉伸成POT纹理,但如果是UI界面(开启Pixel Perfect)的话,可能显示时会有较大视觉损失。
  • Texture占用内存总是双倍:出现这种情况的原因有两种:一种是在真机运行时开启了Read&Write。另一种可能是Unity的Bug,目前的Unity 5.2.3 release note如下 : (735644) - OpenGL: Fixed texture memory usage reporting in profiler, was twice the actual size for most textures.
  • 纹理Atlas是合成一张2048(尺寸)的纹理还是四张1024的纹理在其他设置一致的情况下,这两种方式无论在加载还是渲染方面其实并没有实质上的差别。在我们接触到的大多数案例中,纹理资源方面的问题除了尺寸外,纹理格式、Mipmap设置和Read&Write功能同样是需要研发团队时刻关注的。
  • PS:看来纹理的Read&Write选项很重要

unity的工具

  • Overhead:
    • Overhead的计算方法是:Profiler当前帧统计的总耗时时间减去所有已知选项的累加时间,即引擎当前还无法识别模块的耗时时间。
    • Overhead数值理论上是趋向于0的,但是由于目前市面上的硬件设备、系统框架过于丰富,所以引擎可能无法识别所有模块的CPU开销。
  • unity的分析工具中的Reserved Total 和 Used Total为Unity引擎在内存方面的总体分配量和总体使用量。 一般来说,引擎在分配内存时并不是向操作系统 “即拿即用”,而是首先获取一定量的连续内存,然后供自己内部使用,待空余内存不够时,引擎才会向系统再次申请一定量的连续内存进行使用。