UWA

每天一点UWA:第十九周

Posted by 蔡华的博客 on December 22, 2017

AssetBundle

在移动平台上打AssetBundle时,为了控制包体大小,会开启PlayerSettings中的Optimize Mesh Data,对网格文件使用范围比较多的资源有较好的优化效果。 然而这带来了一个问题,就是在部分使用网格的特效中,美术会对模型上一些顶点做特殊处理,以达到一种网格遮罩的效果,而这个设置需要用到网格的顶点色。然而在开启Optimize Mesh Data这一选项后,打出来包中Mesh上的顶点色会丢失。 请问有什么比较好的解决方法,既能使用此方法优化网格,又能保留网格的顶点色效果?

  • 一般来说,开启“Optimize Mesh Data”选项,引擎会在发布时遍历所有的网格数据,去除其多余数据,从而降低其数据量大小。需要注意的是,这里的“多余”数据是指Mesh数据中包含了渲染时Shader中所不需要的数据,即如果Mesh数据中含有Position、UV、Normal、Color、Tangent等顶点数据,但其渲染所用的Shader中仅需要Position、UV和Normal,则Mesh数据中的Color和Tangent则为“多余”数据,引擎在发布游戏时,会将这些数据自动去除。
  • 开启这个选项是一把双刃剑。对于在Runtime情况下有更换Shader需求的Mesh,建议研发团队对其特别注意。如果Runtime时需要为某一个GameObject更换更为复杂、需要访问更多顶点属性的Shader,则建议先将这些Shader挂载在相应的Prefab上再进行发布,以免引擎去除Runtime时会使用到的网格数据。

对于运行时动态加载的普通模型比如怪物,我们目前的打包策略是把它单独打一个AssetBundle包,通过AssetBundle加载并实例化的消耗。如下图所示,对于WWW、AssetBundle、GameObject,卸载方法分别为WWW.Dispose、assetBundle.Unload、Destroy/DestoyImmediat。但对于通过AssetBundle加载出来的Assets资源, 这块的资源用什么策略清理合适?

1)Resources.UnloadUnusedAssets,但该函数比较费时,一般只在切换场景时候使用;

2)assetBundle.Unload(true); 运行时Assetbundle在Instantiate prefab完成后也立即执行了unload(false),所以也不适用;

对实例化出来的GameObject在使用后即使执行了DestroyImmediate,模型引用的贴图还驻留在内存中,难道要遍历GameObject使用的Assets分别执行UnloadAsset么?

  • 如果是仅加载Prefab,那么随Prefab一起加载进来的资源是不太方便被“优雅”地卸载的,即便是对应的GameObject被Destroy了,那么它对应的资源会变成“游离”状态(没有Refcount),只能等到手动调用Resources.UnloadUnusedAssets或场景切换时被引擎卸载。 所以,一般建议研发团队尝试通过依赖关系进行打包,将资源和Prefab进行分离,这样可以将加载资源和加载Prefab分开,从而可以通过“显式”地方式加载资源并将其进行储存,这样当你想精准释放资源时,则可以直接通过Resources.UnloadAsset来进行卸载。
  • PS: 这个问题本身就很有内容。

Audio

将一个Unity 4.7.2的项目升级到Unity 5.5.3。打成安卓包,发现APK文件比之前大了200MB。然后分别对4.7.2打出来的APK包和5.5.3打出来的包分别进行解包,发现音频文件个数一样,但所占大小多了200MB左右,平均每个文件都大了几百KB。Unity 4.7.2的解包为Mp3格式,Unity 5.5.3的解包为Wav格式。

于是我在Unity 4.7.2和Unity 5.5.3下,分别新建一个空工程,将单独一个音频文件(ogg格式)放入Resources文件下,即两个Unity版本的空工程里只有一个Resources文件夹,文件夹里只有一个相同的Ogg格式音频文件,打成安卓包。使用解包工具解包,发现Unity 4.7.2的音频文件转成了mp3格式,大小几十kb;Unity 5.5.3的音频文件转成Wav格式,大小200多KB。

请问这种问题怎么解决?针对音频,Unity 4.x和Unity 5.x在打包过程中,作了那些修改?

  • 引用Unity 5.0的release note:https://unity3d.com/unity/whats-new/unity-5.0 “Rewritten Audio asset pipeline and AudioClip backend.””Much improved audio formats in terms of memory and CPU usage.** The Format property no longer refers to a specific platform-specific file format, but to a encoding method** (Uncompressed, Compressed, ADPCM).”
  • 可以看到,在Unity 5.x中,不同平台的音频不再与特定的文件格式关联,而是与文件编码关联。文件格式不等同于音频编码,虽然同是Wav格式的文件,但其有不同的编码方法(PCM,ADPCM,Vorbis,Wav也可以用mp3编码),编码方法会影响到Apk的大小。选定编码方式后降低quality和sample rate也会降低apk大小。 image

Font

我们在iPhone5c上测试时发现Font.CacheFontForText会造成很严重的卡顿,看了UWA的Blog,了解到可能是因为创建FontTexture导致的开销。那是否可以认为如果我们一开始申请了足够大的Font Texture,后面即便有新的文字,开销也不会很大? 如果可以这么做,一开始申请使用的字符串是根据自己项目生成一个字典,还是直接使用网上3、4千字的常用字库更好?

  • 是的,如果Font.CacheFontForText开销较大,那么在无法降低字体数量的情况下,一开始增大Font Texture是较为可行的方法。建议根据自己项目的字体来撑大Font Texture,避免不必要的浪费。 同时,也建议看一下这篇关于字体的精简优化方法

GamePlay

我想了解下如何获取物体的最小包围盒?Collider.bounds.size 获取的是与世界坐标系坐标轴平行的包围盒(AABB),当物体旋转时包围盒大小会发生变化。那如何获取物体的OBB,使包围盒的大小不会随物体旋转而改变呢?

  • 获取 OBB 可以用 Mesh.bonuds(模型空间)。注意蒙皮Mesh这一类在Shader中修改顶点坐标的操作。

安卓客户端存在大量的模板数据需要配置,其中一些模板表甚至可能达到万级的数据条目,那么怎么对这些数据模板表进行打包和加载,可以兼顾加载速度和热更新表结构?一开始我们采用了ScriptableObject,把全部模板数据加载到内存并序列化为Asset的方式进行Assetbundle打包,该方案加载速度较为理想。但当我们通过Dll替换热更新安卓客户端时,发现这种方式不支持热更新,一旦Dll中修改了模板表结构,热更新替换后,ScriptableObject的AssetBundle就无法读取了,提示损坏的AssetBundle,目前的方案是采用Protobuf代替ScriptableObject进行序列化,可以实现热更新模板表结构,但是加载速度相对ScriptableObject有较大的差距,目前数据模板加载较慢便导致了玩家进入世界的时间比较久。因此想了解大家有什么好的建议呢?

  • 用ScriptableObject或者BinaryFormatter二进制存储然后反序列化成保存数据结构的对象,这两种方法应该是加载速度最快的。 我们实际没有采取这个方案,也使用的是Protobuf,是出于以下考虑:
    • 一份二进制数据,客户端和服务器可以通用。从服务器推数据很方便;
    • 策划习惯使用Excel编辑,有脚本可以把表格内容导出成Protobuf的二进制数据,另外,还有.cs/.go表结构描述文件需要重点考虑。也就是说,策划修改表结构、增减表,服务器和客户端的结构描述文件可以自动生成好;
  • 第一个问题:配置表存储格式 现在主流的数据存储基本分为三大类,各有优劣,需要根据实际情况选择:
    • ProtoBuf或类似序列化库,这种方式兼容性高,但是加载速度一般;
    • 自己实现二进制数据存储,兼容性差,需要精心设计达到较高的数据表达能力;
    • 采用Lua热更新方案的游戏,普遍直接把数据存储为Lua表。
  • 第二个问题:配置表数据与代码兼容 一般不建议大量修改数据结构,比如增删字段,如果实在无法避免,需要代码连同数据一起发布进行热更,做好版本管理即可。
  • 第三个问题:配置读取速度优化
    • 先从数据量上约减,减小数据冗余重复,数据存储设计优化,多次引用的字段多引用等等;
    • 采用多线程加载,避免使用Unity提供的API,在游戏启动时,并行加载配置表,充分利用多核优势;
    • 就我们自己项目而言,没有使用Lua的更新方案,但是我们依然采用Lua作为了数据存储,经过优化后加载速度也不错,可以参考 LuaTableOptimizer。

MainCamera 渲染完场景后,在 Screen Depth Buffer 上记录了整个场景的深度信息。Camera2 在 MainCamera 后,将模型绘制到一个 RenderTexture 上,我想利用 Screen Depth Buffer + Camera2.RenderTexture.ColorBuffer 对 Camera2 绘制的内容进行深度剔除,这时 Unity 会提示 Screen Depth Buffer 和 RenderTexture.ColorBuffer 不能混用。这样的需求该怎么做呢?

  • 这是一个很有意思的问题,参考https://blog.uwa4d.com/archives/1945.html

请问怎么在inspector中显示Dictionary呢? 例如: Public Dictionary<string,string> dict;

  • Unity 无法序列化Dictionary类型,无法将其显示在inspector中。一个Workaround是将Dictionary的Key和Value拆到两个list中,list可以在Inspector中显示。利用API:ISerializationCallbackReceiver (https://docs.unity3d.com/ScriptReference/ISerializationCallbackReceiver.html 有示例代码)可以在序列化/反序列化时将Dictoionary中的内容与list内容同步。

Unity需要对float进行相等判定

  • Mathf.Approximately

通常UGUI中的Button点击事件在代码中是通过onClick.addListener()来添加的。而且这种按钮的点击事件通常只会执行一次,如果添加了多次就会出现点击一次、按钮调用多次的事件函数。如果该Button只会添加一种事件,可以通过引用来判断是否添加了重复事件,从而不再重复添加相同事件。但是如果Button添加了多种点击事件,那么引用的方式就无效了。C#中的Delegate中有一个函数GetInvocationListD()可以获取此委托中的事件列表,从而剔除相同事件。然而让人头痛的是,我查看了Button的API似乎并没有暴露出此Button添加了的事件列表,所以没有办法剔除相同事件。不知道是否有什么办法可以解决这个问题?

  • onClick是一个ButtonClickedEvent对象,而ButtonClickedEvent的基类是UnityEventBase。UnityEventBase里有一个InvokableCallList对象,里面应该就存了事件列表,但这个是private的,可能只能尝试通过反射,去取里面的值来对比了。看上去并没有比较简便的方法。
  • 可以考虑扩展下Button组件,封装一个AddUniqueListener的接口,保留一下已经onClick.addListener的UnityEvent,然后每次Add前都先和这些UnityEvent做个对比,没有重复再进行onClick.addListener。

如果场景中有大量物体,请问如何利用相机拾取获取不同范围内的物体?

  • 采用Physics.SphereCastAll函数来获取,类似下面这个场景: image
  • 在SphereCastAll这个API的参数中,第二radius可以控制获取半径,我们分别设置5和10的效果是这样的: radius=5: image
  • radius=10: image

如下图,蓝色部分为Unity视图,绿色为Android图片,我现在需要把Unity蓝色区域透视或者去除,iOS上已经透视成功,但Android一直失败,Unity版本为5.5.2,大家有什么建议呢?

  • 只记录下方案 https://blog.uwa4d.com/archives/TechSharing_79.html

改变GUILayout中ScrollView滑动条宽度

GUIStyle gs = GUI.skin.verticalScrollbar;
GUIStyle gs1 = GUI.skin.verticalScrollbarThumb;
        
gs.fixedWidth = 30;
gs1.fixedWidth = 30;
......
pos = GUILayout.BeginScrollView(pos);
......
GUILayout.EndScrollView();
......
gs.fixedWidth = 0;
gs1.fixedWidth = 0;

我这边做了个测试,把场景中的物件进行StaticBatch,用Lightmap烘焙,并且在烘焙完记录每个物件Renderer的lightmapindex and offset,然后在运行中,我删除了生成的LightingData.Asset,自己设置了Lightmapsetting,并且恢复了每个物体Renderer的Lightmapindex and offset, 结果我发现烘焙出来的场景Lightmap还是不对的。我怀疑是不是LightingData.Asset 还记录了别的东西,想问问看UWA有没有什么经验呢?

  • 这个问题还是出在Static Batching上。经过测试我们发现如果去掉场景中物件的“Static Batching”标记,换成手动Batching,即采用StaticBatchingUtility.Combine的方式结果就正确了。

image

  • 同时,我们发现使用LightingData.Asset时,开启Static Batching与不开启都正确,因此我们认为LightingData.Asset可能记录了与CombinedMesh有关的数据,导致如果仅仅使用gameobject的LightMap参数无法正确读取CombinedMesh的LightMap UV。另外一种可能是Unity的一个Bug。 最后,我们建议采用上述手动Batching的方式,即可解决此问题。

当使用Linear设置时,UI上图片透明度等也跟着变化,出来的效果与美术人员的需求不一样。请问有什么办法可以让UI不使用Linear或者让UI可以在Linear影响下也正常呢?

  • 值记录下做法:https://blog.uwa4d.com/archives/2066.html

Texture

请问在iOS上,图集如何设置成PVRTC的格式呢? 下图中的设置是否正确?

image

  • 图集的设置需要考虑到该图集的用法,如果是作为普通的Texture给3D物体用的,那么TextureType选择Texture,然后Format选择Compressed就可以,因为在iOS上,Unity会自动处理NPOT(把纹理拉伸为边长为2的幂次的正方形),并默认采用PVRTC。
  • 设置之后可以看一下下方的预览面板,会直接显示PVRTC的;如果是给UGUI用,那不同情况下,设置又不一样了;如果是给NGUI用,可以按照问题中截图里的设置即可,最终再确认下预览图上是不是PVRTC。

LoadSubsAsync和LoadSubs加载SpriteData,我想请问一下: 如果先LoadTexture加载纹理再加载LoadSubs,或者LoadSubsAsync函数调用加载SpriteData,纹理是否会加载两次,以及性能如何呢? 按照我的理解,LoadSubsAsync或者LoadSubs,它自身有Texture属性,我断点调试可以看到SpriteData内的Texture会随着SpriteData加载而出来。

  • LoadSub和LoadSubAsync是会同时加载Texture的。 而先Load再LoadSub,并不会造成冗余问题,所以Load过Texture后,LoadSub的耗时就很小了。 但如果Texture里的Sprite是打了图集的,那么LoadSub的时候还是会有个图集加载的耗时。

UI

Q1:众所周知,UGUI中有个Canvas.sendWillRender()函数在View打开的时候很耗时,所以通常的做法是把View移出屏幕外,或者Canvas.group,或者Scale等类似的处理机制。这样虽然能解决SendWillRender()消耗过高的问题,但也会有新的问题出现:

  1. EventSystem.Update()。由于各个View都是Active的状态,所以这个函数底下的Graphic.GetDepth()消耗很大,而且又由于不止一个GraphicRaycaster组件,所以这个函数下面的List.Sort()消耗巨大,更何况是每帧都在耗。
  1. 还有一个函数ScrollRect.LateUpdate(),它的开销也很高。而如果将Scale设置成0,其消耗更大,这种情况下,我还是倾向于用回Active/Deactive的处理机制,因为就界面打开的时候会卡一下,而不至于影响总体的帧率。
  • 界面的快速隐藏和显示,比较推荐的做法是:
    • 把界面单独作为一个Canvas,并绑定一个相机,同时在绑定相机的Culling Mask中设置一个不渲染的Layer;
    • 隐藏时,把Canvas移出相机范围,同时把Canvas的Layer改为不被渲染的Layer,禁用对应的GraphicRaycaster组件,把Canvas中所有的动态UI元素停止。
    • 显示时,移回Canvas,改回Layer,激活GraphicRaycaster组件。
  • 针对题主的问题1,禁用GraphicRaycaster组件后就没问题了,被禁用的GraphicRaycaster不会进行Raycast操作;
  • 针对题主的问题2,ScrollRect.LateUpdate中会通过EnsureLayoutHasRebuilt提前触发Canvas.SendWillRenderCanvases(),所以实际上还是Canvas.SendWillRenderCanvases()的开销。Canvas.SendWillRenderCanvases()开销高,是因为用Scale改为0的方式会清除顶点信息,在Scale改回1的时候还是需要重新创建顶点信息,开销肯定还是很高的,所以不推荐用Scale改为0来隐藏复杂的UI界面。

UI界面勾选Static是会节省一部分性能,但是如果我移动了某个设置为Static界面下的元素,这个界面还算是静态的么? 还是等运动的元素停下来后的界面才算静态的? 如果是这样,是不是我全部界面都设置静态就好了?

  • 如果题主所指的“勾选static”是指GameObject右上角的Static框,这个Static和UI是无关的,不会对UI有影响;如果是NGUI的UIPanel上的Static选项,勾上之后,子节点中的UI元素是无法移动的(即使改了Transform,其图标位置也不会更新)。

工具

Profiler里Assets和Scene Memory的区别是什么?比如Mesh这一项,在Scene Memory的Mesh中看到的只有2个合并的Mesh:CombinedMesh(root: scene),在Assets的Mesh里看到的有100多个,包括场景里的非合并Mesh,动态加载出来的角色Mesh等。并且Assets的Mesh中的某些点击后选择右边的Referenced By,在Hierarchy里会自动选中场景中的物体,怎么看也不像是单纯的模板,而是实例化出来的东西。

image image

  • Resources.Load/Assetbundle.Load出来的ParticleSystem都是放Assets下的,可以认为是模板资源,并不在场景里。Instantiate出来的放在Scene Memory下,是出现在场景里的。 所以两边都有是正常的。
  • 在Unity里资源至少分为两类:
    • 一类是可以被引用的,比如Mesh、Texture,如果要渲染多个相同的Mesh,并不需要对Mesh实例化,只需要在场景里多创建几份MeshRenderer/MeshFilter去引用它即可。所以Assets下的Mesh不应该被认为是实例化出来的东西,这些Mesh仅仅是通过AsssetBundle.Load/Resources.Load加载出来的,只是被场景里的东西引用了;
    • 但SceneMemory下的Mesh通常是通过new Mesh或者Instantiate创建的,这部分可以说是实例化出来的了,另外像CombinedMesh是Unity自己创建的,也可以算是实例化的。
  • 另一类是不可被引用的,通常是组件资源,比如ParticleSystem,如果要渲染多个相同的ParticleSystem,就需要实例化多份出来,ParticleSystem的模板在Assets下,而实例化出来的在SceneMemory下。

我在Editor下测试, 前一帧Mono还是67.2MB,下一帧突然自己掉到了51.5MB。然而并没有gc.collect()调用,这Mono是为什么减少?因为有不明原因的减少,所以担心也有不明原因的增多。

  • 如果题主是在Editor测的,这个Mono的回落就正常了。Editor本身(渲染窗口,处理交互事件等)就会造成内存分配,同时也会触发GC。
  • 但默认情况下,Editor本身的开销在Profiler里是看不到的,除非题主点击一下“Profile Editor”。 点击之后,就可以看到Editor本身函数的耗时和GC的触发了。

模型&动画

场景中放置一动画物体,原先正常进行动画,修改了物体Hierarchy某结点的名称,然后动画就异常了。打开Animation Window,发现动画曲线变为Missing。把物体名称改回后又恢复正常。我的疑问是动画Curve是与物体名称绑定的吗?如果是,如何修改动画物体的名称呢?

  • anim文件里有节点路径的: 比如:curve: serializedVersion: 2 m_Curve:
    • time: 0 value: {x: -0.187, y: -0.003, z: 0.062} inSlope: {x: 0, y: 0, z: 0} outSlope: {x: 0, y: 0, z: 0} tangentMode: 0
    • time: 0.93333334 value: {x: -0.187, y: -0.003, z: 0.062} inSlope: {x: 0, y: 0, z: 0} outSlope: {x: 0, y: 0, z: 0} tangentMode: 0 m_PreInfinity: 2 m_PostInfinity: 2 m_RotationOrder: 4 path: Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 Spine1/Bip001 Neck/Bip001 R Clavicle/Bip001 R UpperArm/Bip001 R Forearm/Bip001 R Hand/Bip001 R Finger1
  • Animation Curve与动画物体是名称绑定的(名称包含其Hierarchy的Path,除去Root);
  • Animation Window中可以修改Curve的绑定名,可以将其更新为改名后物体的Path;
  • 如李先生所说Animation文件包含结点路径,可以“ForceText”后用文本工具来修改,比较方便;

平时研究动画系统的时候有几个不太明白的问题:

  1. BlendTree(不论1D或者2D) 在采样的时候,如果BlendTree内的AnimationClip的长度不同,那么输出的动画长度是可变的。不同的BlendTree参数会导致不同的BlendTree输出的动画长度。问题是BlendTree最终输出的动画长度是如何计算出来的?采样AnimationClip的时候,对于动画的TimeScale做了什么调整? 图例:run是一个BlendTree2D,当它的参数Speed=300时,和Speed=900时的长度是不一样的。 image

2.Unity的BlendTree2D是如何根据两个动画参数确定该在哪几个AnimationClip中采样?这些AnimationClip各自的权重是多少? 图例:当Direction=-19,Speed=777的时候,黄箭头所指的动画实际上是由红箭头所指的4个动画混合而成的。那么混合权重分别是多少? image

  • 对于不同的Clip动画数据进行Blend时,Unity会把所有Clip的时间都归一化(展开为相同长度)后,再根据各个Clip的权重进行加权平均。
  • Blend后的时间长度与各Clip的权重相关,时间更接近权重大的Clip的时间(近似时间长度的加权平均)。BlendTree参数值用来决定当前Blend各Clip的权重值,参数不同导致权重不同,因此时间也就不同。参数值如何决定哪些Clip进行Blend,其权重则跟不同Clip的参数threshhold相关。
  • 2D时在参数空间中根据(x,y)的坐标位置对临近的Clips进行插值。不同2D Blend 类型使用的插值方法不同,而插值方法的实现也就决定了各Clip的权重。对于插值算法可以参考这里提供的pdf: http://answers.unity3d.com/questions/1206428/how-weights-of-2d-blending-are-calculated.html

性能综合

如下图,不理解ScriptMapper具体是什么,为什么会引用StandardShader,有什么办法可以彻底把StandardShader从内存里移除呢?

image

  • 这说明是有代码在索引这个Shader。建议题主尝试以下方法来定位Standard Shader的具体引用:
    • 在游戏运行时遍历所有的GameObject或者Material,然后获取其所使用的Shader信息,查找跟“Standard”名称相对应的Shader,然后就可以定位它是出自哪个Material或GameObject。
  • 上述方法应该适用于大部分情况,但无法适用于以下情况:
    • Shader被加载后直接被缓存在代码脚本中;
    • Shader是通过AssetBundle.LoadAll加载的;
    • Shader通过Preload Shader加载的。

Unity对GameObject.active()这个底层具体做了什么?因为在优化项目的时候看见有些界面active()这个函数造成的开销比较高,我采取的办法是移除摄像机的范围,这是参照UWA以前文章找到的办法,但是我还是想知道Unity对Active底层具体做了哪些操作,否则会有点困惑。

  • 至少SetActive后,如果GameObject被激活,那么会调用该GameObject和所有子GameObject上的所有组件的OnEnable函数。而各种不同的UI组件的OnEnable中也有各种不同的操作。 如果题主希望深入研究下,可以看一下NGUI或者UGUI的源码,OnEnable具体做了什么都可以看到。同时,针对UI界面而言,还会在同一帧里触发其他的相关函数,出现后续的开销,如UGUI里的SendWillRenderCanvases和NGUI里的UIPanel.LateUpdate。