每天一点UWA:第二十周

小结&新的开始

  • 从今年的7月29号发布第一周的笔记算起不知不觉已经二十周了,当时的想法也很简单就是下定决心读完当时算下来是168篇文章。5个月过去了,在写这篇的文章的时候基本上已经通读完了所有厚积薄发版块的关于unity的文章,真的感觉这是我这些年来做的最有毅力的一件事情。可以说在这二十周里面对于unity的理解尤其是图形学上学到了很多东西,而且也学会了很多优化的理论和别人实践的方法。
  • 理论已经学习完了,在后面的工作中就是一个实践的过程了。现在的项目会开始使用GOT工具进行优化的工作,真正的把优化这条路走下去。
  • 由于UWA出文章的速度不可能太快,而且自己的工作也很忙,所以每周的笔记可能会改成每月一篇了,继续加油。
  • 还有就是UWA都开始搞UE了,还有什么理由不开始学UE。

GamePlay

在UWA的测评结果中,我们的Mesh文件内存过高(使用UWA GOT测试最大的场景会达到200MB),大部分是由于场景的物件导致的。我们的场景物件是这样加载的:场景有一个基础的框架(地面、天空盒和个别大的物件等),在进入场景后,会根据位置来加载其他的物件,加载的物件在离开视野后,为了防止下次再加载,只将其隐藏了,并没有销毁,这样的话,人物如果在场景里跑了一圈,就相当于整个场景的物件,都会进入内存。请问,是不是将离开视野的场景物件销毁比较好呢,销毁后是不是要调用UnloadUnusedAssets才能彻底从内存中去除呢?

  • 如果题主做的是移动游戏,那么200MB确实太大了。
  • 建议题主建立一个Memory Pool来缓存场景中的物体,至少有以下两个规则:
    • Pool必须有一个上限,一般为容器的数量,超过最大阈值后即开始进行清理;
    • 为Pool中每个Object记录一个存储时间,当时间超过一定阈值后进行清理,或者当Pool满了后,将时间最长的进行Deactive Object进行清理。
  • 通过缓存池来进行销毁较长时间不再显示的的物体,同时,可以通过UnloadAsset API来卸载相关的资源,Resources.UnloadUnusedAssets API一般只建议在场景切换处进行使用。
  • PS:说两句,如果一个最长时间的物体正好在视线内就不好办了。所以这个答案有点太粗暴,个人感觉还是要根据摄像机位置进行动态的删除对象。

解析NavMesh.asset数据,用于服务器寻路。

ReflectionProbe在移动平台丢失部分高光数据

大批量地合入美术资源时,经常会出现构建出来的版本出现材质引用丢失、Animator Controller引用错误、贴图引用错误等资源引用错误的问题。

  • 出现这个问题可能是如下原因:
  • 是否使用代码混淆。在Prefab 上挂的脚本忘记添加到排除混淆的列表,导致序列化的字段被混淆,打完Bundle后的Prefab资源加载时候,挂接的脚本出现引用错误;
  • 资源导入都重载过OnPostProcess并处理了资源设置,这一步是否修改了什么不合理的地方,比如破坏了引用关系;
  • 打包AssetBundle时,在构建Bundle 之前有没有使用文件操作API(不是 Unity 的AssetDatabase的API)来直接修改了某个文件夹或者其他会破坏引用关系的行为,然后构建Bundle,完成后恢复文件夹名字(或者恢复资源原始状态),这样丢失引用关系的几率很大;
  • 有没有可能发布机器上,看着正常,但是Perforce里面已经存在一堆已修改的Meta文件;这种问题常出现于美术本地有两个A1,A2个相同资源在不同文件夹,A1受版本控制,但是由于某种操作,本地临时资源A2使用了原本A1的Guid,原本正确的 A1 被迫使用了不正确的新生成Guid(相当于两人交换),然后上传了A1的Meta,结果发布机器的下来的A1 Meta就会跟别人丢失引用,或者更新下来本地重新分配了新的Guid;美术策划最容易犯这个错误;
  • 我们是Unity 5.3.8p2,上周遇到一个疑似bug,美术多上传了一组相同的资源,我们更新下来都会重新生成Guid,但是很多挂起的Meta在Unity里重新导入后,在版本控制里神奇地消失了,但修改还在;
  • 如果都不是,只能尝试最小排除法了,删除项目绝大部分资源,一点点增加,然后打包,重现,排除原因;不行的话然后删除代码,一点点添加,打包,重现…有时候笨办法也是最容易接近真相的。

游戏对战时因为会频繁更换武器或释放技能(主要是网络玩家),会替换不同的动作,但是发现每次设置animatorOverride[name]=clip(每次设置3-6个),CPU占用50-180ms,请问这是什么原因?我按照官方的指导存了一个list(https://docs.unity3d.com/ScriptReference/AnimatorOverrideController.ApplyOverrides.html) ,然后每次LateUpdate的时候调用ApplyOverrides,发现占用更高了,每次400ms,请问这个要怎么解决?目前每个网络玩家玩家OverRidesCount是73,会不会是Clip过多的原因?

  • 在真机运行时发现animatorOverride[name]=clip的耗时会受到animatorcotrollor中的clip数目的影响,随数目的增多耗时会明显增加,70个以上的clip耗时确实能达到50ms(红米note2)以上。
  • 换为applyOverrides方法时耗时仍然很高,而且出现了堆内存分配,但并没有达到400ms那么高。
  • 尝试了整体替换AnimatorOverrideController,即不在现用的AnimatorOverrideController中替换clip,而是在另一OverrideController中换好clip后,再整体替换到runtimeAnimatorController,这样耗时就会降到很低了。

Texture

Unity 5.5.1版本下,Sprite Packer在iOS平台下RGBA PVRTC4打包图集失真非常严重(对单个的Sprite设置PVRTC4是正常的),参照了4.6.7版本是正常的,我想知道为什么呢?

  • unity的bug
  • 在 Windows 下尝试用 4.7 的 pvrtextool.exe 替换了 5.x 的,暂时解决了这个问题,建议也尝试一下。

异步纹理加载Asynchronous Texture Upload这个功能最后到底有没有实用?

image

  • 这个是默认就开启的,只是得开启多线程渲染才有效,因为要在 Render Thread 里上传纹理,还有文档里提到的各种限制,比如只针对关闭了 Read/Write 的纹理… 非多线程渲染时,异步加载纹理的时候,可以在 Loading.UpdatePreLoading 这个函数里看到较高的 Texture.AwakeFromLoad。但多线程渲染时,这部分的开销就会变得很小,因为这部分开销被放到 Render Thread 去了,如下图中的 Gfx.UploadTexture。
  • 但需要注意的是,这个地方的异步并不是流式的加载,也就是说,当加载一个大纹理的时候,还是一次性完成的,并不会分帧去做,如下图加载了一张2048的纹理,图中绿色峰值是Gfx.WaitForPresent,也就是主线程在等待 Render Thread。
  • 综上,这个功能实际上就是把某些满足要求的纹理的加载时间从主线程移到了渲染线程。在某些情况下,确实是可以提高总体的加载速度的。但这个功能并不能真正地“解决异步加载时纹理却只能同步加载”的问题,只是把卡顿放到渲染线程去了(如果耗时高了,主线程还是要等的)。
  • 非常好的问题,建议直接看答案。 https://blog.uwa4d.com/archives/TechSharing_84.html Q3

在目前的Unity版本中,使用多维材质是否会对效率产生影响,用多维材质和把模型分块哪个更好?

  • 制作美术资源时,应该尽量避免使用多维材质。
  • 先举例说明多维材质的一个优点是拆分模型做不到的: 一块大石头有石头和草地两个材质,做成一个整体的模型,使用多维材质,作为一个物件进行烘焙,在Lightmap上的UV分布是一个整体,不会出现拆分为两个模型烘焙产生的接缝问题。
  • 除了上面这种情况,其他情况下推荐将模型拆分成多个模型赋予不同的材质。
  • 优点是:
    • 1、多个模型会作为多个MeshObject参与到裁剪、静态批次等优化中 ;
    • 2、拆分的模型和贴图可以进行材质合并,程序才能进行下一步的优化,如果本身是多维材质,就无法进行合并DrawCall的优化;
  • 第一个优点就能带来很大的收益。制作上应该按照一个模型对应一个贴图的做法进行,如果模型是一个整体,比如房子由底座、墙体、屋顶组成,建议将他们多选导出成一个FBX。
  • 另外,不同材质的模型建议做成多个模型,再多选导出成一个FBX,仍然是一个模型对应一个贴图的规范进行制作。
  • PS: 对于人物模型可能不考虑剪裁,但是至少减少了drawcall。

UI

我在UGUI 使用一个UIRoot,类型使用Screen Space - Camera,使用摄像机的 Culling Mask, 如果UIRoot可视,下面的UI子物体设置不可视Layer,是不是不会被裁掉?有没有什么解决办法吗?

  • UGUI的网格合并是以Canvas为单位的,所以只能改Canvas的Layer才有效。
  • 如果只是个别UI元素要快速隐藏和显示,可以考虑用Scale为0来做,Scale为0时UI的顶点信息会被清空,所以隐藏时就不会参与网格重建了。
  • PS:顶点清空的第一帧应该会出现rebuild,后续帧不参与rebuild。

在UI中,有两个字体控件每帧都要显示不同的数字,第一个有8个字,第二个有6~7个字,需要开outline增加辨识度,从而引起了较高的性能开销。 把这两个控件单独放到另外独立的Canvas,该问题依然存在。现已确定该开销是由这两个字体控件每帧的文本更改引起的。

  • 提到一个是8个字,一个是6到7个字,都开启了outline。 首先建议再做一次确认,看是不是禁用这两个之后,SendWill就确实没开销了。因为这个字数看上去并不应该造成很明显的开销;
  • 其次,如果确实是这两个造成的,那么就需要先考虑下能不能转成带outline效果的静态字体,不行的话再考虑下能不能降低更新的频率了,是不是确实要每帧都变;
  • 如果要再降低,就只能考虑静态字体了,如果文本是一些比较固定的内容就很好处理,但如果是像聊天一样,内容无法预知的话似乎就没什么办法了。或者就是低端机用shadow来做。

现在用Resources.Load()加载资源时,UI动画会出现卡顿的现象,如果想消除卡顿,是不是只能通过异步加载,分拆Prefab和优化材质网格动画之类的操作来实现?还是说UI动画可以在另外的线程播放之类呢?另外我想问下,异步速度是不是一定比同步慢呢?

  • 不直接贴答案了
  • 总结一下就是遇到这样的卡顿的解决办法
    • 分析卡顿原因
    • 使用异步方法
    • 拆分功能、预加载

工具

Unity中的Frame Debugger中的渲染顺序以及数量和Profiler中看到的Draw Call数量,以及在高通的调试工具里看到的DrawCall数,它们之间有什么关系呢?哪个数值是影响渲染的重要指标?

  • 简单来说,Unity引擎中Total Batches是我们建议最需要关注的指标。
  • 1个Setpass Call或者Batch,相当于是一次Render State的切换,而1个Draw Call则是CPU让GPU去进行渲染某一个Object的1次操作。在当前的移动设备中,1次Render State的切换要1个Draw Call本身要耗时。
  • 所以,Total Batches是我们较为建议的关注指标,也是UWA性能报告中所提供的Draw Call查看指标。而Frame Debugger中,其数量是与Total Batches相一致的,即查看的是每一个Batch的渲染物体。 更为详细一些的说明,可以查看https://answer.uwa4d.com/question/58d29b8b5a5050b366a6b6ae
  • 而Unity Profiler中的Draw Call,其理论上对应的则是glDrawElements的调用次数,其与高通或其他第三方工具所返回的Gl Trace信息操作数量不太一致,但应该与其中的glDrawElements API的调用次数基本一致,题主可以自行检测看看。

我用UWA GOT进行本地性能测试,在CPU的数据分析中发现,某些帧StackTraceUtility耗时特别高,这是什么原因导致的呢?

  • StackTraceUtility.XXX是Unity引擎的Log输出,可能是本身Debug.Log/LogError的调用输出,也可能是使用过程中引擎端出现了Warning/Error等信息而自动输出的。

我在Profiler中观察性能曲线,发现某一帧AssetBundle加载中,LockPersistentManager耗时比较大。请问这部分能否优化?

  • 这说明当前帧或前几帧中存在较大量的资源在通过LoadAsync来进行加载,其本质是所加载的资源过大所致,对自身资源进行合理优化可降低Loading.LockPersistentManager的开销。
  • 另外,将异步加载换成同步加载,LockPersistentManager就不会出现了,但其总加载耗时是没有变化的,因为总加载量没变。

我在安卓真机上跑游戏,发现Profiler下的合批数据和PC或者iOS下的不一样,因此不确定Android的合批是否有效。经过分析,Android上不能设置Graphics Jobs(在Player Settings里面),也是不断打包测试发现这个问题,想了解一下具体是什么原因呢?

  • Graphics Job目前在Android平台上是为Vulkan而设计的,也就是只有支持Vulkan设备的才会真正起作用。按照Unity原厂的说法,该选项在不支持Vulkan的Android设备上应该是没有效果的。
  • 另外,Graphics Job和MultiThread Rendering并不建议同时使用,而且以目前的Android设备来说,建议只开启MultiThread Rendering一项即可。

模型&动画

在Unity3d的官方文档中,animationclip.framerate的解释如下:

“Frame rate at which keyframes are sampled. (Read Only) This is the frame rate that was used in the animation program you used to create the animation or model.”

我对此有些疑问:

  • 1)这个是否为动画系统每一秒钟更新动画的次数?如果是,当这个值高于Application.targetFrameRate时,一秒钟更新动画的次数依据哪个为准

  • 2)这个是否只是调节动画播放速度的一个参数,而每秒钟动画状态更新的次数不受影响?

  • A: 动画采样的时候是根据当前播放的时间去找对应的前后两个关键帧做插值。所以动画本身得告诉Unity两个关键帧(没做keyframe reduction时)的间隔时间是多少,或者一秒有几帧,这个值就是framerate。所以这个是只读的,导出时就应该是确定的。因此这个值也不会影响运行时动画的更新次数,默认情况下动画就是每帧更新一次的。

在手机上测试LoadLevelAsync和LoadLevel的加载速度,同一个场景,LoadLevelAsync要比LoadLevel多花费40%左右的时间,请问这是正常的么?LoadLevel会有卡顿,导致Loading进度条不平滑,但是LoadLevelAsync好像又会增加Loading的时间?我项目中场景动态加载的做法是,是把物件做成Prefab,然后根据主角的位置做动态加载相应的Prefab,用的是Resources.LoadAsync方法。现在加载一些比较大的物件时,在红米2等低端机上,仍然比较卡,要消耗200ms以上。请问有什么好的方式,能平滑这个加载过程么,谢谢!

  • LoadLevelAsync一般情况下是要比LoadLevel慢的,但是否要慢40%,这个其实是根据每个场景所要加载的不同量而定的,并不是确数。
  • LoadLevel和LoadLevelAsync其实最本质的区别,是前者一定要在下一帧结束前完成加载操作,所以当加载场景较大时,其随后的单帧开销就会很大,而后者则没有这个限制,引擎可以根据当前的使用情况或者ThreadPriority(这个值是是否对LoadLevelAsync有影响,还没做过具体实验,但对于LoadAsync确实有影响 )来自行调控。
  • 还需要说明一点的是,不是说使用Async,它就一定是绝对平滑,下图红框是LoadLevel,绿框是LoadLevelAsync操作。

image

  • 关于真正“平滑”异步的加载方式,Unity引擎目前还是没有的。在遇到较为复杂的Prefab(比如大纹理、多AnimationClip等等)时,其加载依然会出现卡顿。如果想要缓解该问题,建议如下:定位资源->查看资源为何开销->采用预加载的方式处理这样的资源。

我使用了IL2CPP后是否还存在Mono内存呢?使用IL2CPP后,通过Profiler工具获取的managedObject(例如: int32[])是哪种内存?

  • 可以简单地认为,Il2CPP只是替换掉了Mono的虚拟机实现,所以该分配堆内存的地方还是会一样的分配(可能会有某些细节的地方不一样)。
  • IL2CPP在堆内存分配方面和Mono 最大的不同主要是Reserved Total 是可以下降的,而 Mono的 Reserved Total 只会上升不会下降。
  • PS:原来是这样,我一直以为是直接转成了CPP语言。

物理

我的游戏场景中有一个boxcollider2D,位置在(0.0f,0.0f) 到 (1.0f,1.0f);调用Physics2D.BoxCast(new Vector2(1.0f,1.702), Vector2(1.0f, 1.4f), 0, Vector2(0.0f,1.0f), 2.0f); 会产生碰撞,理论上碰撞的边界在1.700 ,我已经将位置放到1.702,应该是规避了浮点数误差的问题的,如果我将该值放大了1.706就不会产生碰撞了,请问哪里可以调整这个误差值的范围呢?

  • Unity 2017.1版本的Physics2DSettings里面有一个Default Contact Offset,表示当两个collider之间的距离小于它们的ContactOffset之和就会产生碰撞。把这个值设置小一点应该能解决题主的问题。
  • Unity 5.x中Contact Offset,默认是0.01,这也是为何1.702不行,而1.706可以的原因。

性能综合

释放了资源,但纹理还在内存中,且引用数为0,要如何销毁呢?纹理所在的UI的GameObject是通过Destroy销毁的,并且执行了Resources.UnloadUnusedAssets();

  • Ref Count为0应该就是可以通过Resources.UnloadUnusedAssets或Resources.UnloadAsset来进行卸载了。
  • 出现这种情况建议如下:
    • 尝试将Resources.UnloadUnusedAssets在Destroy稍后几帧执行;
    • 在加载时尝试直接获取这些资源,然后在Destroy后通过Resources.UnloadAsset来卸载指定资源。

Resources.UnloadUnusedAssets() 在卸载旧场景后加载新场景前调用好,还是在加载新场景后调用比较好呢?如果考虑内存峰值的话,我觉得是前者好,但是在UWA上看到有些文章说是加载场景后调用。

  • 如果是通过LoadLevel(Async)类似的方式来加载场景的话,那么Unity自身会在底层执行一次类似Resources.UnloadUnusedAssets的操作。所以,这时如果手动调用Resources.UnloadUnusedAssets操作,时间间隔很短,其实这个是有些重复的。因此才建议在新场景加载后再调用一次。
  • 但如果使用LoadLevelAdditive或其他类似的API来切换场景的话,那么Unity是不会调用Resources.UnloadUnusedAssets的,这时在旧场景卸载后调用,其实也是很不错的选择。
# UWA
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×