当前位置:   article > 正文

项目的整理游戏优化(为自己)2017_gameobject.deactivate

gameobject.deactivate

 

Transform.SetParent

GameObject.Deactivate/Active

MipmapVisualization

 

Unity3D性能优化:ShaderLab内存占用

前言:

手机的内存占用一直是项目优化的重要部分。最近优化项目的内存占用时。发现了个占用比较恐怖的地方。

那就是ShaderLab

正文:

从上图(进入战斗场景时的内存快照)可以看出,ShaderLab占用居然达到42M(请忽略其他项数据,因为还没优化到=。=)。

为什么Shader的占用那么高呢?

 

 

由于当前项(ShaderLab)没有说明详细的shader占用信息。所以只能去另外找原因了,还好,在内存快照的Assets下的shader项中,有详细的使用信息。

然后我看到Standard的shader使用。那么我就开始迷惑了,因为项目里面根本没有地方使用过Standard这个shader,为什么会存在呢。

所以我感觉内存占用的大头就出在这了。

然后进行了一轮的排查,把部分使用到Standard的地方清除掉,测试并再拿一次内存快照。

 

清除了一部分后,ShaderLab降到27.6M(后来完全清除掉Standard,降到21M)。果然,主要原因就出在了Standard上。

那么问题又来了,为什么我没有使用到Standard,却会在内存中看到Standard的使用呢?

这里就要说下两个坑了,同样,也是排查这个问题的方法。

一.模型导入导致。

模型导入的时候,“Import Materials”是默认勾选的。所以当模型导入时,Unity会在同目录创建“Materials”目录,并创建相应的材质,而这个材质默认是使用Standard。

由于美术在制作过程中对Prefab中对模型另外赋予材质,所以实际上,默认创建的材质(Standard)是没有使用到的。可是当加载模型的时候,却又把默认创建的材质加载上了,并对着色器解析了。因此导致内存中有Standard。

那么解决方法也很简单,把“Import Materials”去掉,并把没有使用的默认材质删除。

 

注意:如果不把“Import Materials”去掉,导入到其他项目时,材质又会自动创建了。

由于实际项目中,Prefab改动次数比较大,相应的模型文件改动比较少,所以把项目中的模型和对应的Prefab分开打包成不同的AssetBundle。然后就出现一个很奇怪的情况。

没有勾选“Import Materials”的模型文件,在实例化Prefab时,ShaderLab会存在一份“Standard”的shader内存,而这个shader的引用是指向一

个“Default-Material”文件(可是这文件并不存在)。

  

但是,在模型和Prefab在相同的AssetBundle中,或者使用Resources加载时,却不会有“Standard”和“Default-Material”的出现。

暂时还还不确定是这是版本bug,还是Unity的特殊机制。(版本5.3.3)

临时解决方案:在需要模型与Prefab分开打包时,勾选“Import Materials”,直接使用和修改默认生成的材质。

二.默认模型(Cube、Sphere)创建导致的。

早期场景搭建时,为了方便定位和可视化,曾经使用Cube等系统默认的Mesh作为锚点,然后在启动游戏时禁用掉。

由于这些Cube不启用,性能消耗很轻微。所以就没有理会了。

可是,就因为是系统默认的Mesh,所以创建时,赋予的材质就是默认的材质“Default-Material”,而这个材质使用的着色器就恰恰是“Standard”。

所以“Standard”存在的并不冤了。

 

解决的方案也很简单,删掉这些mesh或者是替换材质。这样,这部分占用的“Standard”就不存在了。

总结:

由于Standard的变体太多了,所以当引用了Standard的时候,往往会存在多个Standard变体,占用大量的内存。如果,你发觉你的ShaderLab的内存过大,而又那么也不妨找找是不是上述的原因。

那ShaderLab占用内存过大是不是完全是Standard的原因呢?其实并不止的。像我优化完之后的27M(完全清除后是21M),肯定还有其他原因造成的。可是,优化的过程是砍大头,像上文那样,稍微优化一下,就能拿掉20多M,当然要立刻做,可是越往小的时候,优化效率就越来越低了,所以这时候就需要转移目标看一下“Objects”“Texture2D”这些大头了。所以,剩下ShaderLab的优化方向可能会在以后遇到的时候再补充。

  • 角色更新过高,后面更详细的分析。

  • 屏幕边缘图标和Wifi过高。优化方案:频繁设置UI文字和贴了可以优化。

  • 放技能开销非常高,每个子弹有独立弹道逻辑,技能释放频率比价高。优化方案:类对象使用内存池,delegate优化减少gc,更完善的预加载机制,逻辑优化等等。CMT的timer使用内存池。

  • 部分非技能的CMT没有预加载。

  • 召唤物没有预加载。

  • 部分道具有些没有预加载。

  • 消息处理非常卡。解决方案:使用类对象内存池,优化序列化方式不使用protobuff的。

  • 帧操作增减人会卡。解决方案:推测玩家行为预加载。

  • 飞行道具开销很高,特别是Deactive上。解决方案:可见性使用layer,使用内存池

  • 地图脚本开销较高。

  • Camera.main直接调用开销较高。解决方案:因为Unity会遍历所有的Camera,是一个线性时间。在Update中调用的话,缓存一下main
    camera。

使用Unity的FrameDebugger简单分析

  • 俯视角开启了天空球渲染,其实没必要

使用Unity MemoryProfiler分析内存占用和内存泄漏

通过memoryprofile抓帧分析,内存占用不高,但是RenderTexture占用非常高63.5M

  • 大厅场景RenderTexture 63.5m。解决方案:因为使用了景深,扰动,屏幕校色,实时阴影,等后期处理效果所致,我们根据玩家手机配置做了自适应和限定,并且再图形配置选项允许玩家做一些调整。景深开销非常高因此关闭了实时景深效果使用其他方法代替。
  • 战斗场景Texture 33.7m Mesh 14.9m animationclip 3.7m Audiomanager 4.1M。解决方案:贴图压缩,使用shader减少换色贴图,修正一些贴图引用不释放导致的内存泄漏。
  • LockstepMnanager.FixedUpdate函数 每帧分配10kb以上内存。解决方案:各种GC相关的优化。
  • SceneLoader.update函数(315 bytes) MainLoop.Update函数(202bytes), NetManager.update函数(186bs) CharacterActor.LateUpdate函数(392 bytes)皆为每帧都分配。GC的分配每帧超过200b就算大了。解决方案:各种内存优化,消息使用内存池等等。
  • MainLoop.OnGUI函数每帧分配内存 300b。解决方案:删掉空函数。
  • 大厅场景Camera.Render cpu等待gnu渲染,造成时间帧率抖动 25ms-> 43ms 体现为Camera.Render self时间大幅变动 (优化方法 降低场景面数,拆分Gm_Cabin模型)
  • Loading贴图存在伪内存泄漏,因为图集和按钮放在一起了被按钮引用。
  • Unity技术支持提供了脚本插件可以分析代码中所有静态的引用。发现是lua的LuaScriptMgr中会对一些贴图有引用,需要注意使用Lua导致的内存问题。

使用XCode做详尽的性能优化

XCode也可以做GPU的性能分析。编译xcode工程需要Development版,启动,GUP的优化点需要在Run的面板里设置成metal否则有些堆栈看不到。点FPS,然后点相机Capture一帧,可以看到帧率,CPUGPU的占比,渲染时每个函数的开销。点Shader里面的看更详细堆栈,可以看到每个shader的每行代码的开销占比。

  • 这个版本大厅很简单但是非常非常卡,看到UI和景深开销占比比较高,UI占了25,景深占了30。看到UI费是采样一张贴图占了50但是这张贴图并不大,景深费是因为shader里lerp了很多次导致开销很高。在FrameDebugger里可以看到UI最后渲染了一个全屏的面,可以看到每次渲染了占用的时间比,这个面实际没用。关闭了这个UI的全屏面,关了景深和后处理,基本就没开销了,连UI的渲染也只有2ms了因为景深把总线带宽给占满了,其中最费的还是景深效果。

渲染线程开销也较高

  • 版本机出的版本在XCode中会报错APPLE MACH-O Linker Error,无法做性能分析。解决方法:改成il2cpp了。
  • 修复字符串拼接的量级,使用StringBuilder等。
  • OnGUI,FixedUpdate,Update等空函数也会有gc开销,因为会产生从C++到C#层调用的开销。最好都去掉。
  • FMOD音效模块开销过高。我们没使用官方的FMOD使用了FMODStudio,测试版资源为了效果和开发方便使用了一些事件内的效果器。优化后音效占用10-15%。

  • 动画开销较高,使用了动作融合,动画状态机较复杂。解决方案:不可见的角色不更新动画,一些顶点受骨骼影响数可以设置很小,小怪等简单角色使用Animation,动画的一些优化等。

  • UI,粒子,动作开销较高,并且多线程开销也高。

  • 可以看到每个模块开销比较平均,已经做了很多优化了没有明显瓶颈,只能一个模块一个模块的抠了

其他一些测试以及和Unity官方支持的问答:

  • Unity的物理比射线省,因为会优化做空间划分,只计算较小范围。Navmesh最优化。
  • 迷雾和可以性检测可见性预缓存,预缓存分块加载,迷雾进行分块划分优化,分担计算量到多帧执行等等
  • Resource目录资源多会导致Unity进游戏时间特别长,因为会把Resource下所有资源做一次检索,导致启动慢。建议使用Assertbundle,也支持压缩。另外提升进入游戏速度的就是,第一个场景足够简单足够小。解决方案:开发了热更的资源管理,每个资源有一个唯一ID,可以存在Resource下也可以存在Assertbundle中。
  • Assetbundle的新算法是按Chunk加载,不会把整个Assertbundle都加载进来但是压缩比小,压缩的时候要选择ChunkBase。如果bundle加载好之后资源的引用时放在bundle上,如果a界面打开bundle包读取贴图,a界面关闭则贴图会释放,如果a没关闭,b界面读取贴图b界面会对应到同一张贴图上。
  • 建议bundle放在StreamingAssets目录下。
  • Profile分析建议:Overhead,是Unity没统计到的时间,用总计时间减去剩余的时间,一般包括C++到C#层的调用开销,场景复杂度,垂直同步等。大厅因为Overhead卡是因为CPU在等GPU,建议用XCode看。
  • 通过Profile.BeginSample自己插代码对怀疑的代码进行内存和性能调试。
  • Unity5.3.5p8修改了Foreach装箱拆箱导致的内存开销。但是没有修改mono的gc。实际测试可以用Foreach了。
  • 部分Android设备总线太烂导致GC会特别卡
  • OnGUI,FixedUpdate,Update等空函数也会有gc开销,因为会产生从C++到C#层调用的开销。最好都去掉
  • 部分3G的Android设备,实际可用内存可能只有500M。Unity启动时最高能占到80M,一般在40-50M
  • unload切场景自动调,gc也会自动调
  • 编辑器下看到的资源引用计数不准确,得看真机。实际测试的时候老发现很多资源释放不掉,但是到真机测试发现实际已经释放掉了,因为编辑器模式会缓存一些资源,所有的测试都应该以真机为准。
  • lua导致gc太多,没有比较好的解决办法,把lua的代码放到C# 层
  • IOS上如果限45帧对IOS来说就是限30帧,低于30帧才是真实帧率
  • CPU优化:ulua的计时器开销很大。其中PreloadManager是Unity的各个模块的预加载。
  • GPU优化:Mask会很费。

CPU

  • Top10

使用Profile找到CPU占用最靠前的函数,从最高的开始依次分析优化。定位的方法有很多,Unity的Profile,UWA的性能测试工具,比较推荐的是使用XCode,可以抓取一段时间内函数的开销。更品均准确也可以看到更底层。

如过函数比较复杂可以使用BeginSample/EndSample拆分,我在项目里通过添加条件编译进行了封装类似:MDebug.BeginSample("Character.TakeDamage");

这样可以跟随意和高效的定位到关心的地方。下面列举部分比较通用的优化方案。

  • LOD,代码只在必要时才会运行

最常用的优化方式。例如屏幕外的角色不计算动画更新,不计算技能效果冒字血条等,屏幕外的角色休眠,只有主角才会冒字等等。还可以做一些LOD,例如AI可以做行为树lod,动画LOD,粒子特效LOD,更新频率LOD(更新频率随着游戏帧率,离主角的距离,重要程度,视锥,类似CSM的提醒分段,以及周围的角色个数动态调整。例如轩辕城擂台,同屏高配200人,未优化在68帧左右优化的后到94帧左右)

  • 限帧,负载均衡

为了降低耗电发热量我们会根据玩家机型进行限帧。另一方面我们将逻辑帧和渲染帧分开执行,逻辑代码以更低的帧率执行,部分逻辑也可以使用线程,负载均衡分段计算等方法提升性能。

  • 算法

一些代码本身的运算。例如优化物理运算,空间换时间使用查表预计算等方式对计算提速,减少频繁索引FindGetComponentd以及各种运算,优化遍历利用稀疏矩阵九宫四叉树等等。

  • 服务器计算或者客户端计算

根据不同类型的游戏也会调整一些运算是在服务器还是客户端,如果服务器性能强大可以让服务器计算物理,寻路,AI,战斗逻辑等复杂运算,客户端只要变现即可,特别是MMO的项目。而如果希望服务器开发较少提升开发效率和降低服务器性能要求,也可以将绝大部分运算放在客户端,例如帧同步的游戏我们就采取的这种方式。当然有时候也是两者相结合并没有绝对标准。

  • Unity接口
    • OnGUI,FixedUpdate,Update等空函数也会有gc开销,因为会产生从C++到C#层调用的开销。
    • MainCamera是一个遍历操作Camera比较多的时候不要频繁调用。
    • 尽量少使用GetComponent,AddComponent(还会产生GC),Find等操作。
    • 使用Unity5.6有个新函数SetPositionAndRotation。因为Transform的Position每次脏了会有一次消息,Rotation也会有,并且会开一个线程来做这个操作极大提升性能。所以最好每帧只设置一次,并且使用SetPositionAndRotation一次性设置可以提升一倍的性能开销。
    • 其他。

  • 物理

Unity使用PhysX作为物理引擎,本身优化还是很好的,会做空间划分。Unity官方建议碰撞对小于100,其实这个标准非常严苛,我们测试在300左右物理的开销还是蛮少。优化方面可以通过分层减少碰撞对,尽量使用BoxCollider而不是MeshCollider,UI界面不需要点击的控件不要打开Raycaster。因为我们只使用了最基本的射线检测,其他物理是自己实现的,主要的优化还是在物理算法上。如过在Profile中发现Physucs.Simulate开销比较大就是物理需要优化了。

  • IL2CPP & C++

把Unity编译设置成IL2CPP,编译成C++版运行效率会有较大提升。还可以把一些运算逻辑放到C++的库里,这样可以优化更极致减少gc。

  • 动画

如过Prifile中发现Animator.Update或者MeshSkinning.Udpate开销比较大就说明动作可能需要优化。

    • 优化:打开Optimize
      GameObject,可以把一些无效的节点骨骼去掉,注意如果有自定义的节点需要拖到不被优化列表里。

    • 压缩:打开Keyframe
      Reduction,可以压缩很多不必要的关键帧,这个值越大压缩比率越高失真越严重。

    • BoneWeights:顶点受骨骼影响,对要求不高的环境可能一个骨骼就够了。可以每个模型设置,也可以实时全局改变。

    • BakeMesh:对于同屏需要显示大量模型可以使用SkinnedMeshRenderer.BakeMesh,把动画烘成模型,这样在渲染的时候可以合并(带动画不能合并)。可以大幅减少DC,省去蒙皮计算,不过缺点是内存增大,增加DynamicBatching的CPU开销,表现会差一些。
    • 不使用Animator:Animator的开销比Animaton比一个量级。
    • 不可见不更新设置CullCompletely,但是需要注意一些消息也会停掉如果对动画有依赖会出问题。
    • 其他:骨骼LOD,GPU Skinning(有些设备和情况会更慢),使用Bone代替CS等等。

  • UI

UI也是个开销大头,一般会占到30%-50%。UGUI对应Profile中Canvas.BuildBatch &
Canvas.SendWillRenderCanvases开销,类似NGUI的LastUpdate,UI的优化又很多文章这里也简单列举一下。

    • 动态静态分离:因为UI会合并。NGUI是按Panel进行重建的、UGUI是按Canvas进行重建的,防止动态UI触发合并导致静态UI也一起合并。
    • 预加载,常驻,即时释放:UI按类型划分,比较大的常用的UI在创建的时候会卡顿,可以进行预加载。主城到战斗场景,在保证峰值内存的情况下,将英雄界面工会界面等常驻内存,可加快Loading速度,实测优化后提升一倍以上loading速度。其他不常用界面拆分成小界面,使用即时加载,关闭时卸载节省内存。需要注意的是,UI节点过多也会导致加载缓慢,我们曾经Loading要10秒,其中序列化UI占了一半左右的时间(贴图预先加载测试),减少UI节点数,太大了拆开。
    • 图集:合理拆分UI图集,区分公共图集(常驻)和非公共图集。太大容易造成冗余加载,容易导致内存占用过大,导致内存显存交换开销。太小有容易导致显存碎片影响效率。规则很复杂。
    • 内存池:UI冒字等频繁创建的UI使用内存池减少创建的时间和内存碎片。
    • Active/Deactive:不推荐通过Active/Deactive来频繁切换UI界面,因为会触发UI合并操作,可以通过移到屏幕外的做法或者设置Layer。但需要注意移到屏幕外还是会被合并渲染,如果是长时间不显示的还是Deactive比较好需要视情况而定。
    • UISprite来代替UITexture:Texture不会合并。
    • 不移动不可见的UI不更新:例如血条名字等。
    • layout group, canvas group组件,任何子节点变了父节点都会用getcompent找到laygroup。这是Unity的UGUI的两大坑。
    • 检查不需要拾取的Raycast target是否关了。
    • 资源预加载:例如前面介绍的UI预加载,内存允许的情况下所有资源都应该预加载,结合内存池。我们游戏中所有变现逻辑,角色,怪物,道具,UI都会做预加载,并且有一套池膨胀和回收的策略。
    • Shader预加载。
    • 等等。

  • GC

GC是一个非常高开销的系统调用也,是大部分卡顿的主要原因,不能完全控制。因此我们要尽量减少代码堆内存分配过量防止频繁触发GC,同时也可以在Loading或者对性能不敏感的时候主动GC。

    • 升级Unity:Unity5.6修改了粒子系统的源码减少了lamda表达式的gc。
    • 减少一些字符串拼接,使用StringBuilder代替string减少GC开销,不要使用富文本改变Text组件的颜色直接通过修改Text组件颜色来改。
    • 内存池:前面说过GameObject的内存池,另外还有类对象的内存池。所有频繁反复创建删除的都应该使用。两个用途,减少加载创建释放的时间,减少内存碎片降低GC的频率。
    • Unity接口:AddComponent,OnGUI,UI合并频率,delegate,等(一些Foreach,协程等Unity已经优化)。
    • 逻辑优化:避免频繁创建开辟空间。
    • 插件的GC优化:对行为树,FMODStudio等一些插件的源码进行了修改减少GC。

GPU

  • DC

DrawCall实际上优化的CPU的时间,但因为DC的优化一般都是材质mesh合并,所以放到了GPU的部分。每次在准备数据并通知GPU渲染的过程称为一次Draw
Call。渲染一次拥有一个网格并携带一种材质的物体便会使用一次Draw Call。可以理解为调用一次DC就换一种画笔在画板上画一个物体。

  • 多线程

限于篇幅,虽然多线程是CPU的部分但都放在GPU这部分介绍。随着PC的主频达到瓶颈,手机也开始朝多核并行开发的方向前进了。所幸Unity这方面做的比较好,大部分开发者可以不用考虑多线程的开发方式。

    • 多线程渲染:虽然叫多线程渲染其实节约的也是CPU时间。虽然CPU和GPU是并行工作的,但是因为CPU提交了渲染数据和指令之后,需要等显卡同步会阻塞逻辑的执行。多线程渲染就是把这个等待时间和逻辑的执行并行了起来。Unity5.5版本在PC和IOS上都默认开启了多线程渲染,但在Android设备上单独提供了多线程选项,这是因为Android系统设备太多碎片化严重,开启多线程会导致一些设备出错闪退等。亲测我们游戏开启多线程渲染后,能提高10%的性能,所以我们是默认开启的。王者荣耀的解决方案比较完善,他们会针对每个机型做大量测试,还和硬件厂商有合作,只针对测试通过的设备开启多线程。所以也会看到一些比较有趣的广告,xx手机对王者荣耀做了优化支持多核,其实只是一个配置参数而已。
    • GPU骨骼更新:利用GPU计算骨骼降低CPU的开销,但是Android的一些设备因为GPU非常弱(广告只介绍CPU),所以开启之后很多设备性能反而下降了,这个配置默认关闭。
    • 网络多线程:我们是开了一个线程进行网络协议的收发处理,这也是比较常见的。
    • 多线程并行计算:Unity5.6的位置更新其实就是开了线程计算,所以新版本的Unity设置位置和旋转的性能大幅提升。此外音频的计算相对独立也都是在其他线程里计算的。优化时也可以考虑把一些运算量比较大又相对独立的部分放到线程里计算,例如AI。
  • 面数

DC<200,面数小于10w是Unity建议。我在14年的测试,在红米1上,当dc超过100性能开始直线下降,面数超过6w面性能开始直线下降。

我们目前场景的标准是整个场景少于10个DC,由美术或插件合并输出,这样虽然不是十分灵活,但因为静态合并会增加Loading时间和内存,动态合并也会增加内存和合并CPU开销,所以还是采取这种优化方式。角色2000面左右,低配1个DC,高配3个DC(描边,实时阴影,主角还有额外的一个因为墙后半透效果)

  • LOD

对GPU的优化也可以通过LOD进行,可以通过模型LOD,骨骼LOD,粒子LOD,材质LOD的方式,地形LOD等等,例如不同配置开启不同的效果,开启后处理等。

  • 遮挡
      • 遮挡剔除:顾名思义就是被遮挡看不见的地方不渲染,例如墙后的物体。遮挡剔除可以CPU计算也可以GPU计算。Unity自带了OcclusiongCulling,但5.x不建议使用。
      • UI遮挡:例如全屏UI可以隐藏背景,节省电量。
      • 场景拆分:我们因为是俯视角,能遮挡的东西很少,只是采取了场景分割,小物件LOD的方式。
      • 入口:一般用于室内,早起的Quick使用了二叉树和入口的优化方式。
  • 半透明

相对不透明半透明开销巨大,在PC和手游上都是,还会破化渲染管线的优化。另外使用alpha通道的贴图压缩也很困难(特别是IOS上的PVR格式Alpha像素压缩之后损失巨大,ETC,DDS,PVR等格式Alpha一个通道的压缩比就等于其他3个通道了)。半透明物体不写Z无法做像素级遮挡,需要做混合操作,半透明需要单独排序。以下有一些比较通用的优化点;

    • 少用/减小面积:尽量少用,要用尽量减少占用屏幕的面积,减少像素填充率。
    • 这里要说的是PowerVR的平台AlphaTest比AlphaBlend开销更高。因为会影响HSR(Hidden Surface Removal)优化,类似EarlyZ,和其他平台不同。
  • Culling

这个其实也是优化的CPU。每个相机都会针对场景图做Culling,通过视锥和包围盒剔除这个摄像机看不见的物体,减少实际渲染物体个数。因为Culling操作比较耗时,也可以通过减少摄像机下对象的个数或者手工开关分层来做优化。这里需要注意的是,合并方式也会影响Culling,例如把整个游戏所有的树的都合并成一个DC,DC是下降了,但是只要有一棵树在摄像机里,所有合并的树模型都会被渲染,增大了渲染的带宽和负载需要权衡使用。例如手游穿越火线就是把整个场景优化成了3个DC,一个DC渲染所有地形,一个渲染所有的墙,一个渲染所有的箱子等。

    • 粒子

    • 减小屏幕覆盖面积
    • 避免使用Alpha
    • 合并材质和mesh
    • LOD:根据机型或者距离降低粒子发射器的个数和效果
    • 序列帧:对一些俯视角游戏使用序列帧做特效也能大幅提升效率。这个是一个典型的空间换时间的优化,会增大内存,使用受视角影响如果压缩较大会损失一些效果,要有选择的使用。之前在做一款3d战斗卡牌游戏时特地写了序列帧录制工具。
  • 其他

  • 渲染设置:阴影,雾,抗拒齿,垂直同步,各项异性,多线程渲染,GPU计算骨架,顶点受骨骼影响,软粒子等等。每个项目要求不同。
      • 降低渲染的分辨率:缩小Framebuff分辨率,减少ps开销和内存显存,但是会模糊,王者荣耀等很多主流游戏在Android上都降了分辨率。
      • 智能动态调节:根据玩家配置和游戏环境实时调整配置,低配设备或者战斗降低配置进行限帧,高配插电或者低开销场景,动态提升配置,提高限帧。有一篇文档有更详细的介绍。
      • 后处理:Unity里可以通过看Graphics.Blit性能了解后处理的开销。后处理一般是像素级别的计算,手机设备上分辨率有普遍比pc高,使用的时候更需要注意。
      • 不使用多维子材质材质:我在真机上实测,三个子材质,50个物体,使用多维自材质12帧,330个dc,343面。拆开之后20帧,154dc,134面。多维子材质Unity无法动态合并。

内存

其实在手机上内存的优化才是最重要的,绝大部分闪退都是内存不足导致,都闪退了其他优化还有什么用呢?内存分析工具有很多,使用Unity的Profile,memoryprofile,XCode,,UWA。一般简单快速分析用Profile,具体内存资源使用memoryprofile和uwa,内存泄露代码级内存优化使用xcode。

    • Mono托管堆:逻辑代码的堆内存分配 ,一旦分配,不会返还给系统。代码级别比较难查。以前在PC上自己写过通过钩子的方式记录内存分配的工具,Unity推荐编译成IL2cpp使用XCode的工具分析,也可以自定义采样点逐帧查看或者通过二分法。
    • GfxDriver:显存资源。
    • FMOD音频资源:比较单纯,profile就可以看出来了。
    • Texture,Mesh,Animaiton,Audio等等:推荐使用memory或者uwa。

下面介绍一些具体的优化方式。

    • 压缩贴图ETC/PVR:贴图是占用资源最大的部分。我对3D贴图基本上都会压缩,2DUI贴图部分压缩。在Android上尽量使用etc格式,IOS上使用pvr格式,非半透有1/8的压缩比。这两种压缩格式类似DX的dds,可以直接被显卡渲染,即降低内存又能减少包大小,提升加载速度(JPG等格式虽然压缩比高但是需要解压成32位色再渲染,增加加了内存和显存还有额外的解压开销)。需要注意以下几点:
      • 如果压缩效果不好还可以减成16位色。
      • 关Mipmap:UI或者俯视角游戏不需要Mipmap可以关闭减小1/3的体积
      • 如果使用ETC,PVR,DDS压缩贴图必须是2的幂(因为区块颜色索引的压缩算法,如果不是2的幂压缩后也会补成2的幂),正方形(PVR还必须是正方形否则会自动补成正方形浪费空间)。处于渲染效率和显存碎片的考虑贴图的大小建议最大1024,最小64,最大不能超过2048。
    • 使用九宫,对称贴图:提高贴图复用率
    • Shader:利用Shader合并贴图通道,实现灰度图等。Alpha通道存alphatest和高光,贴图一个通道存阴影一个通道存ao等,alpha通道存在贴图的其他通道便于压缩等等。这点在做轩辕传奇的时候大幅使用。
    • 压缩动画减少关键帧:前面有介绍
    • 及时卸载在进出场景时,或者打开UI界面等对性能不敏感的时候,卸载资源并调用Resource.UnloadAsset清理引用资源和destroy,System.GC.Collect清理系统资源,前面有介绍。AssetBundle加载时生成、卸载时销毁,这也是比较大的一个坑:)
    • 在代码级别上避免不必要的堆内存分配:可以通过静态代码分析检查,另有一篇文章详细介绍。
      • 避免频繁New Class:使用内存池。
      • Constainer:新版本Unity Constainer已经优化。
      • 控制Log输出:我们使用条件限制,Release自动屏蔽一些不重要的日志
      • For代替Foreach:新版本Unity已经优化。
      • String连接:减少字符串拼接,使用StringBuilder等等。
      • delegate:因为内部的链表和装箱拆箱操作,使用频率较高时GC也很高。
      • 合理的使用Lambda表达式:例如Unity的粒子系统5.6版本以前GC较高就是这个原因。
      • 频繁的临时变量或者list生成,建议定义一个全局的list每次都用该list来计算。
      • 需要注意的是类申请在堆上,结构申请在栈上,有时可以使用结构。
      • 等等,这方面优化点和文章较多不再累述。
    • 内存泄露: Unity是基于引用计数的,一般内存泄漏是资源被Hold住无法释放,内存增长趋势明显、反复切换场景内存膨胀。针对这种情况可以自己写工具输出每个场景的资源日志。也可以使用XCode分析一段时间的内存。
    • 表数据。表一般不会卸载,15年帮一个项目做优化,发现光表数据就占用40M。建议使用二进制反序列化使用不要直接使用字符串,并且不在内存中做多份缓存,表数据非常巨大的情况下可以考虑使用完删除。
    • 冗余顶点数据:UI贴图Mesh把color,normal等不适用都导出,静态合并会导致内存增大。
    • 抗锯齿/Rendertexture:开了抗锯齿会增大内存,高分辨率会增大内存,后期处理可以交换使用Rendertexture不要创建多份。
    • GameObject数量:小于1w,节点书过多也会导致加载更新缓慢内存膨胀,可以写工具监控并作为测试时的一项性能指标
    • 其他

闪存

在PC上也就是硬盘。具体表现在包大小,资源加载速度等。闪存的优化和内存大部分共同,但是也有例外,例如使用jpg就是减少包大小增大内存消耗CPU的方法。

    • 压缩和内存一样。贴图,动画,导表等等。
    • 在移动平台上可以打开code strip功能来减少代码带来的内存和容量消耗。对于使用了反射的类,可以使用link.xml配置解决。code strip是Unity提供的一个优化功能,他会预判断代码的执行路径,将没有使用的函数去掉。

    • 部分SDK很大,可以和第三方协商减少重复包含的库。
    • l2cpp 中会包含 ARMv7 和 ARM64 位两个版本的代码,因此,会有两份代码的体积。
    • 动态下载:类似微端。但在手机上并不推荐,可能会导致使用玩家流量。
    • 冗余资源:导出包的时候利用插件或者自己写的工具分析资源。在prefab中搜索资源是否被引用。
    • 贴图mesh动态生成:对于一些规则画贴图和模型可以通过计算生成出来,例如以前使用过的著名的substance,还有之前端游优化地形的时候只保存地形的高度,通过多留的方式在GPU中还原成地形信息,可以减少1/3等等。
    • 其他:包体的优化相对CPU,GPU,内存没有那么重要,但是对游戏的推广非常重要,也值得花精力去优化。

网络

    • 减小包体和压缩:在二进制上进行一些重用和合并减小包体,对协议包做压缩。
    • 合包:按一定频率进行合包操作,把几个包合并在一起降低发送频率,减小包头。MMORPG为了防止包过大可以采取裁包,当玩家同屏较多时抛弃一些不重要的协议。最近的项目为了防止超过MTU,会做一些负载均衡,当操作过多的时限制每帧的操作数,把一些操作放到后面的包广播。
    • 网络同步框架和策略。最近开发的一款射击MOBA游戏,就使用了帧同步。对于大量小兵的RTS和MOBA类游戏,帧同步的协议量可以比状态同步小n倍,我们的延迟和流量已经优化到和王者荣耀类似,而状态同步的全民超神比帧同步的王者荣耀高3倍左右。具体帧同步和状态同步的优缺点会写更详细的文章讨论。

耗电

耗电可能不会被大家所重视,但其实在手游上这是一个非常重要的指标。如果游戏打一两个小时就没电了,那出门在外谁敢玩呢?火遍全国的王者荣耀就在省电上做了非常多的优化,我们游戏也是。那么有哪些优化方案呢?

    • 降低CPU/GPU:CPU和GPU非常影响耗电,因为手机CPU和GPU封装一个SOC上的,因此主要体现在手机CPU占用上,当CPU低于25%会非常省电当CPU高于80%耗电就会从呈指数上升。和CPUGPU的优化是重合。
    • 内存闪存:内存闪存的使用频率也会影响耗电,和内存闪存的优化重合。
    • 网络:和网络的优化重合。
    • 限帧:限帧是比较常用的优化手段,手游一般会限帧30帧,早期苹果系统强制限帧30帧后来才开放的。王者荣耀已经把限帧作为一个广告手段了,哪个设备支持都会做一番宣传。但是限帧会降低手感需要权衡。
    • 省电模式:当检查到玩家没有插电的时候,配合限帧,降低画质和效果,降低更新频率和LOD等。

1. CPU
 

   A. WaitForTargetFPS: 
      Vsync(垂直同步)功能所,即显示当前帧的CPU等待时间 
   B. Overhead: 
      Profiler总体时间-所有单项的记录时间总和。用于记录尚不明确的时间消耗,以帮助进一步完善Profiler的统计。 
   C. Physics.Simulate: 
      当前帧物理模拟的CPU占用时间。 
   D. Camera.Render: 
      相机渲染准备工作的CPU占用量 
   E. RenderTexture.SetActive: 
      设置RenderTexture操作. 
      底层实现:1.比对当前帧与前一帧的ColorSurface和DepthSurface. 
              2.如果这两个Buffer一致则不生成新的RT,否则则生成新的RT,并设置与之相对应的Viewport和空间转换矩阵. 
  F. Monobehaviour.OnMouse_ : 
      用于检测鼠标的输入消息接收和反馈,主要包括:SendMouseEvents和DoSendMouseEvents。(只要Edtor开起来,这个就会存在) 
  G. HandleUtility.SetViewInfo: 
      仅用于Editor中,作用是将GUI和Editor中的显示看起来与发布版本的显示一致。 
  H. GUI.Repaint: 
      GUI的重绘(说明在有使用原生的OnGUI) 
   I. Event.Internal_MakeMasterEventCurrent: 
      负责GUI的消息传送 
  J. Cleanup Unused Cached Data: 
      清空无用的缓存数据,主要包括RenderBuffer的垃圾回收和TextRendering的垃圾回收。 
        1.RenderTexture.GarbageCollectTemporary:存在于RenderBuffer的垃圾回收中,清除临时的FreeTexture. 
        2.TextRendering.Cleanup:TextMesh的垃圾回收操作 
  K. Application.Integrate Assets in Background: 
      遍历预加载的线程队列并完成加载,同时,完成纹理的加载、Substance的Update等. 
  L. Application.LoadLevelAsync Integrate: 
      加载场景的CPU占用,通常如果此项时间长的话70%的可能是Texture过长导致. 
  M. UnloadScene: 
      卸载场景中的GameObjects、Component和GameManager,一般用在切换场景时. 
  N. CollectGameObjectObjects: 
      执行上面M项的同时,会将场景中的GameObject和Component聚集到一个Array中.然后执行下面的Destroy. 
  O. Destroy: 
      删除GameObject和Component的CPU占用. 
  P. AssetBundle.LoadAsync Integrate: 
      多线程加载AwakeQueue中的内容,即多线程执行资源的AwakeFromLoad函数. 
  Q. Loading.AwakeFromLoad: 
      在资源被加载后调用,对每种资源进行与其对应用处理.

2.GPU Usage

   A. Device.Present: 
      device.PresentFrame的耗时显示,该选项出现在发布版本中. 
   B. Graphics.PresentAndSync: 
      GPU上的显示和垂直同步耗时.该选项出现在发布版本中. 
   C. Mesh.DrawVBO: 
      GPU中关于Mesh的Vertex Buffer Object的渲染耗时. 
   D. Shader.Parse: 
      资源加入后引擎对Shader的解析过程. 
   E. Shader.CreateGPUProgram: 
      根据当前设备支持的图形库来建立GPU工程.

3. Memory Profiler

   A. Used Total: 
      当前帧的Unity内存、Mono内存、GfxDriver内存、Profiler内存的总和. 
   B. Reserved Total: 
      系统在当前帧的申请内存. 
   C. Total System Memory Usage: 
      当前帧的虚拟内存使用量.(通常是我们当前使用内存的1.5~3倍) 
   D. GameObjects in Scene: 
      当前帧场景中的GameObject数量. 
   E. Total Objects in Scene: 
      当前帧场景中的Object数量(除GameObject外,还有Component等). 
   F. Total Object Count: 
      Object数据 Asset数量.

4. Detail MemoryProfiler

   A. Assets: 
      Texture2d:记录当前帧内存中所使用的纹理资源情况,包括各种GameObject的纹理、天空盒纹理以及场景中所用的Lightmap资源. 
   B. Scene Memory: 
      记录当前场景中各个方面的内存占用情况,包括GameObject、所用资源、各种组件以及GameManager等(天般情况通过AssetBundle加载的不会显示在这里). 
   A. Other: 
      ManagedHeap.UseSize:代码在运行时造成的堆内存分配,表示上次GC到目前为止所分配的堆内存量. 
      SerializedFile(3): 
      WebStream:这个是由WWW来进行加载的内存占用. 
      System.ExecutableAndDlls:不同平台和不同硬件得到的值会不一样。

5. 优化重点

   A. CPU-GC Allow: 
      关注原则:1.检测任何一次性内存分配大于2KB的选项 2.检测每帧都具有20B以上内存分配的选项. 
   B. Time ms: 
      记录游戏运行时每帧CPU占用(特别注意占用5ms以上的). 
   C. Memory Profiler-Other: 
      1.ManagedHeap.UsedSize: 移动游戏建议不要超过20MB. 
      2.SerializedFile: 通过异步加载(LoadFromCache、WWW等)的时候留下的序列化文件,可监视是否被卸载. 
      3.WebStream: 通过异步WWW下载的资源文件在内存中的解压版本,比SerializedFile大几倍或几十倍,重点监视.**** 
   D. Memory Profiler-Assets: 
      1.Texture2D: 重点检查是否有重复资源和超大Memory是否需要压缩等. 
      2.AnimationClip: 重点检查是否有重复资源. 
      3.Mesh: 重点检查是否有重复资源.

6. 项目中可能遇到的问题

   A. Device.Present: 
      1.GPU的presentdevice确实非常耗时,一般出现在使用了非常复杂的shader. 
      2.GPU运行的非常快,而由于Vsync的原因,使得它需要等待较长的时间. 
      3.同样是Vsync的原因,但其他线程非常耗时,所以导致该等待时间很长,比如:过量AssetBundle加载时容易出现该问题. 
      4.Shader.CreateGPUProgram:Shader在runtime阶段(非预加载)会出现卡顿(华为K3V2芯片). 
   B. StackTraceUtility.PostprocessStacktrace()和StackTraceUtility.ExtractStackTrace(): 
      1.一般是由Debug.Log或类似API造成. 
      2.游戏发布后需将Debug API进行屏蔽.

   C. Overhead: 
      1.一般情况为Vsync所致. 
      2.通常出现在Android设备上. 
   D. GC.Collect: 
      原因: 1.代码分配内存过量(恶性的) 2.一定时间间隔由系统调用(良性的). 
      占用时间:1.与现有Garbage size相关 2.与剩余内存使用颗粒相关(比如场景物件过多,利用率低的情况下,GC释放后需要做内存重排) 
   E. GarbageCollectAssetsProfile: 
      1.引擎在执行UnloadUnusedAssets操作(该操作是比较耗时的,建议在切场景的时候进行). 
      2.尽可能地避免使用Unity内建GUI,避免GUI.Repaint过渡GCAllow. 
      3.if(other.tag == GearParent.MogoPlayerTag)改为other.CompareTag(GearParent.MogoPlayerTag).因为other.tag为产生180B的GCAllow. 
   F. 少用foreach,因为每次foreach为产生一个enumerator(约16B的内存分配),尽量改为for. 
   G. Lambda表达式,使用不当会产生内存泄漏. 
   H. 尽量少用LINQ: 
      1.部分功能无法在某些平台使用. 
      2.会分配大量GC Allow. 
   I. 控制StartCoroutine的次数: 
      1.开启一个Coroutine(协程),至少分配37B的内存. 
      2.Coroutine类的实例 —21B. 
      3.Enumerator —16B. 
   J. 使用StringBuilder替代字符串直接连接. 
   K. 缓存组件: 
      1.每次GetComponent均会分配一定的GC Allow. 
      2.每次Object.name都会分配39B的堆内存.

//

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号