UWA问答 | 博客 | 游戏及VR应用性能优化记录分享 | 侑虎科技 - 资源管理
本期聚集话题：RTS游戏的寻路系统、AssetBundle打包后md5不同、UGUI中3D血条优化问题、如何快速改变场景里一部分物件的外观效果... 这是第107篇UWA技术知识分享的推送。今天我们继续为大家精选了若干和开发、优化相关的问题，建议阅读时间15分钟，认真读完必有收获。文末，我们的互动话题是：能耗发热怎么办？期待你的灼见！ UWA 问答社区：answer.uwa4d.com UWA QQ群：793972859（仅限技术交流） 本文封面图来源：RTS手游《战地指挥官》 http://zdzhg.53mayi.com 寻路 Q1：我们在做一个RTS游戏，开始用的是Unity自带的NavMesh的寻路，但发现这个并不适合RTS多人寻路，因为总会出现阻挡和闪跳的问题。看Asset Store上的A* path插件评论说在碰撞上有问题，不知道大家是否能提供比较成熟的解决方案？ 我们上一个项目是RTS项目，刚开始寻路问题解决方案过程中遇到过和你相似的疑惑，修改探索和调教了很长时间，分享几点供题主参考下。 先上结论：我们采用了两层结构 A Start Pathfinding Project Pro + Unity NavMesh。 1）主体寻路方案使用 A Start Pathfinding Project Pro 这个插件中传统的网格形式，并进行了深度修改扩展，主要是满足寻路过程的通路性需要考虑到单位半径大小，这样寻路精度高； 2）使用Unity NavMesh系统作为寻路过程中的碰撞系统，因为你说的很对，这个寻路插件的碰撞有问题，稍后解释。 以上两部分组成“寻路算法+精确碰撞”。接下来说下以上方案的原因和细节。 1、NavMesh寻路速度快但不够精确。RTS还是适合使用传统的四方形NavGrid格子寻路，而且现有的任何寻路算法默认都是不支持考虑寻路单位半径大小对寻路过程的影响，但我们策划绝不允许1米宽的缝隙被一个两米宽的胖子挤过去，如果使用Navmesh并且动态生成阻挡会出现寻路路径是有了，可是由于碰撞问题大的单位会卡在缝隙这里来回“发抖”的情况；这个只能自己动手修改，所以我们买了该插件然后自己从里到外修改，具体修改算法原理和修改后的示意图，你可以参考这里：http://www.cnblogs.com/yaukey/p/rts_unit_traverse_size_based_path_finding.html。 修改完后，每次寻路会将单位的直径大小当做寻路参数之一传入进行寻路。 使用这个插件开启两个线程左右来寻路是我后来调教出的、一个比较理想的结果。然后是地图中各种单位会出生和死亡，当单位站定不动，需要生成阻挡；移动了需要移除阻挡恢复此处的通路性，通过再修改下插件中的DynamicObscule这个组件（原生功能太简单）来实现；我们具体是每个单位都有一个对应的大小的CapsuleCollider空对象挂上这个脚本，单位站定将该物体放置到单位的位置并激活它，单位移动隐藏该空物体即可，这个脚本会引起地图通路性的更新，这个更新是多线程的，这一点是要注意的，通过回调事件来做响应的事情，切记不要在同一帧或者下一帧去做，因为即时性不能保证。这样就能让整个地图一直根据单位来更新通路性。 2、寻路的通路性问题解决了，现在来解决碰撞问题。这个插件自带了碰撞算法，使用的rvo算法，但是据我当初各种调教实验，它的碰撞都达不到我们的要求，单位多了拥挤的时候总是会有些单位站立后变得重叠了，最不能接受的是两个单位完全重叠。在这里我花了相当多的时间都不能修改出的满意的效果。后来想起来recastnavigation作者亲口说过虽然Unity的NavMesh基于此，但是Detour系统经过深度的改写，Detour包含了巡航和碰撞，而实际使用也发现NavMeshAgent之间的碰撞是很精确的，于是我想了个取巧的办法，给地表再铺一层跟AStar插件的寻路面积一样大的NavMesh，但是没有任何的阻挡（理解为一片纯蓝色），同时给寻路单位添加一个NavMeshAgent，然而这个东西只用它的碰撞，而不使用寻路（没有阻挡，任何寻路都是两点直线，可以考虑几乎无开销），这样最后实际的碰撞效果才算达到我们满意的效果。 以上就是我所说的两层含义，我再说下坑和问题。 1）当初开发Unity版本是Unity 5.3.7-5.3.8，插件版本5.x，寻路插件中底层会有Physics.CheckCapsule调用，但是多线程大量调用，会导致底层PhysX物理引擎崩溃，游戏闪退，不必现但是大量的话很容易一下就挂，插件官方论坛也有这个Bug讨论，后来我将此换成了两个 Physics.CheckSphere 调用，目前2017版本没有测试过； 2）我在1中使用的方法，在UWA性能评测中，每次都是Dynamic Collider超标，这个需要自己取舍了，但是总体我们游戏的瓶颈不在寻路一块，消耗在这些测试中看来比较理想； 3）插件中由于是多线程更新，有很多的完成事件回调，基本都是利用这些链接游戏系统中，不够了自己加，否则依然是容易在未更新完成时得到错误的结果； 4）有可能的话再优化下内部的一些堆内存分配，并且和游戏系统结合后注意释放和断开引用，否则容易内存泄漏，造成寻路插件被引用以至于后面引用很长一串对象都不得释放； 5）寻路精度和碰撞精度在游戏中的表现，需要花比较多的时间去调教参数，有时候表现不好未必是方案不好，然后如果你们对实时同步性要求较高，这个方案还必须再进一步完善和扩展下。 我们使用这个寻路方案的项目是：《战地指挥官》，你可以下载看看寻路是否符合你的要求，苹果安卓各平台都有。以上是我暂时想起来的一些心得，希望对题主有所帮助：） 感谢Yaukey@UWA问答社区提供了回答，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ad0553b910222635366dd34 渲染 Q2：我想要快速改变场景里一部分物件的外观效果，考虑到对DrawCall的影响、修改材质的耗时等等，怎样做是最高效的呢？ 如果只是修改材质属性，那么可以用 MaterialPropertyBlock，如修改Color : MaterialPropertyBlock mpb = new MaterialPropertyBlock(); mpb.SetColor("_FillColor", Color.red); GetComponent<MeshRenderer>().SetPropertyBlock(mpb); 因为 MaterialPropertyBlock 的修改效率高，另外还可以配合 GPU Instacing合并 DrawCall。可以参考这篇文章：https://blog.uwa4d.com/archives/1983.html 感谢ice@UWA问答社区提供了回答，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5acc75e8425802635474fc88 资源管理 Q3：我们在Unity 5.6.5版本下写了一个AssetBundle打包工具。用了最新的Pipeline那个API，就是带manifest的。举个例子，我们打包一个Perfab，这个Prefab有Texture、Shader和脚本。问题来了，我们在不同的电脑，都在Android平台下，打出的AssetBundle不一样，用UnityStudio分析了一下，是Shader的md5不一样导致的，请问大家有什么建议吗？ Unity一直有这个问题的，就算是同一台电脑，删掉Library再打也会不一样。我分享一下我们打补丁的工具的对比流程： 1）首先基于SVN找出两个版本差异的AssetBundle，缩小范围； 2）然后对比对应AB的manifest文件，看AssetHash是否真的变化了，过滤掉没有变化的。 同时检查了一下TypeTreeHash，如果发生了变化给出警告。 顺带吐槽一下这个API： https://docs.unity3d.com/ScriptReference/BuildPipeline.GetHashForAssetBundle.html 它的实现就是去直接读同名的manifest里的AssetHash值，很容易让人误以为这API是去计算hash。 感谢littlesome@UWA问答社区提供了回答 UWA：MD5的问题一直存在，建议直接通过AssetBundle的Hashid来进行判断。 相关链接： https://docs.unity3d.com/ScriptReference/BuildAssetBundleOptions.AppendHashToAssetBundleName.html 感谢欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ace1d68425802635474fc9e UI Q4：我们使用UGUI WorldSpace Canvas 制作了小兵（小兵会经常移动、数量在几十个左右） 3D UI血条，血条UI会自动根据距离相机的远近形成合理的渲染顺序。但是这样的方式导致每个血条都需要一个Canvas， 一个Canvas 就是一个DrawCall。所以我们把所有的血条整合进同一个WorldSpace Canvas下面，这样所有的血条UI只有一个DrawCall，但是这样就无法形成合理的遮挡关系。 于是我们通过每隔几帧计算所有血条UI距离相机的距离进行排序，更改其transform.SetSiblingIndex 来模拟实现遮挡顺序问题，但是这样一来会增加CPU的计算。所以想问问大家对于数量较多的3D UI血条的优化是怎么处理的？ 我们上述的优化方案是否可靠？感谢指点。 UWA：首先UGUI里要改变遮挡确实只能改SiblingIndex，如果需要排序和修改的血条量比较大的话确实容易带来明显开销。但“几十个”算不算大确实不好说，建议先测试下这种做法在低端机上的表现，主要关注的就是算距离、排序、改SiblingIndex的开销总和，如果也就1~3ms那还是可以接受的，毕竟是隔几帧做一次的。 但如果开销非常明显，甚至引起降帧了，那么UWA建议改用Unity2D的Sprite试试，这个是天然用相机距离排序的，不需要自己来做了。只是Unity2D在某些版本上还不支持Sliced等模式（2017.2是支持的），可能视觉效果上会有影响。 PS：接触下来，其实不少游戏是不对血条排序的，可能是因为不太容易被关注到，所以偷懒的办法就是低端机就不排了… 该回答由UWA提供，欢迎大家转至社区进行进一步交流： https://answer.uwa4d.com/question/5ace15b7425802635474fc9c 今天的分享就到这里。当然，生有涯而知无涯。在漫漫的开发周期中，您看到的这些问答也许都只是冰山一角，像这样实用的知识点在UWA问答平台（answer.uwa4d.com）上还有一打！UWA欢迎热爱进步的你加入，也许你的方法恰能解别人的燃眉之急；而他山之“石”，也能攻你之“玉”。
本期我们聚集了这些话题：资源管理方式、新版本下UI相机粒子的性能开销、Camera下开启HDR对性能的影响... UWA一下，你就知道~ 我们将从日常技术交流中精选若干个开发相关的问题，建议阅读时间15分钟，认真读完必有收获。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。 UWA QQ群：793972859 UWA 问答社区：answer.uwa4d.com 渲染 Q1：我看到很多炫酷的效果都是用Unity的后处理来实现的，要求Camera开启HDR。但是我不知道开了这个选项会不会很耗，在中低端机上能跑起来吗？另外，使用HDR之后，Camera的Rendering Path是不是不能选Forward? 使用HDR可以选Forward，和Rendering Path的选择并不会相互影响。 在手机上单纯开启HDR(fp16)性能的影响并不会很大，各种后处理Pass的开销是要优化的重点。因为一般不会单纯开启HDR而不使用后处理，综合考虑到发热量等因素，不建议低端机器使用HDR和后处理，中高端再开启HDR并配合后处理，并对后处理做基础优化：比如适当降低后处理分辨率，尽量合并多个后处理流程到一个Pass等。 该问题来自UWA问答社区，感谢贺甲提供了回答，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/59f810692f7f63497e0f6baf 渲染 Q2：Unity5.5 运行中，材质球的GI选项在烘焙的时候，代码修改模型Renderer组件所引用的Material属性，SetColor(“_EmissionColor”)，没有实时刷新效果。 代码如下： Renderer[] renderers = (Renderer[])mesh.GetComponentsInChildren<Renderer>(); foreach(Renderer r in renderers) { r.sharedMaterial.SetColor("_EmissionColor", new Vector4(0.5f, 0, 0, 1)); } 测试中发现，必须要Editor中选择这个材质球才能刷新。 经测试发现将材质中的GI设成“Baked“之后，如果_EmissionColor的初始值是0，则Unity会直接关闭Standard Shader的“_EMISSION”Keyword，导致设置_EmissionColor对渲染结果无影响。解决方法是设置同时打开“_EMISSION”Keyword，即加上： r.sharedMaterial.EnableKeyword("_EMISSION"); 该问题来自UWA问答社区，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/59f5917ceecc29a735c1321d 渲染 Q3：下面两张图是mate9+OpenGL运行我们项目最高画质时的Proflier截图，Unity 5.6.0p4，可以观察到下图一中第二个Camera.Render出现了一个非常长的等待，这个是我们的UI相机。从图中可以观察到它在等待其他相机完成渲染线程的任务后才开始执行自己的主线程渲染任务。 而下图是关闭了大部分UI，UI相机依然工作，但并没有出现这个异常的等待，我们观察到上图中第二个Camera.Render的首个任务是WaitForJogGroup，并且Job Thread在相同时刻出现了Particle Job，我们怀疑只要这个相机出现了particle system就会出现这种情况，但暂未确定，不知道各位是否有遇到这种情况，以及Unity是不是存在这个问题，如果是5.6的问题，那么后续的版本是否有解决呢？ 这种情况在Unity 5.6版本中经常出现，不管是Unity 5.6.0还是最新的5.6.3，这种情况都是普遍存在的，下图是我们刚刚优化过的一个项目中的Timeline截图（Unity 5.6.3版本），而这种情况在5.6版本之前是很少出现的。 这种问题在我们来看，主要是有两点原因： （1）Unity 5.6版本中将更多的渲染计算从主线程中搬到了渲染子线程中处理，而主线程还需要等待子线程的操作结果才能继续进行，从而造成了问题图二的主线程“等待”现象，即Camera.Render的Self开销； （2）本质原因还是单帧渲染量太大，如题主遇到的UI界面渲染、粒子系统渲染以及蒙皮网格计算等，如果量大都是有可能造成子线程压力过大的，所以虽然很多项目中开启了多线程渲染功能，但还是需要对渲染量时刻保持关注。 目前我们还没有接触过过多的Unity 2017项目，所以目前暂时无法给出统计数据。建议题主可以尝试先回退到Unity5.5的最新版本中进行一个尝试，看看问题是否有好转。 题主补充： 最后深入测试证明2017.1依然是有问题的，只是wait的时机换了：render时不再wait了，但particlesystem.deactivate的时候会进行wait，所以可能会出现这样的timeline： 2017.2未测试但估计也不乐观。 该问题来自UWA问答社区，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/59f9c05ba740da296dd0367f/ 加载 Q4：如下图，释放了资源，但纹理还在内存中，且引用数为0，要如何销毁呢？ 纹理所在的UI的GameObject是通过Destroy销毁的，并且执行了Resources.UnloadUnusedAssets(); Ref Count为0应该就是可以通过Resources.UnloadUnusedAssets或Resources.UnloadAsset来进行卸载了。出现这种情况建议如下： （1）尝试将Resources.UnloadUnusedAssets在Destroy稍后几帧执行； （2）在加载时尝试直接获取这些资源，然后在Destroy后通过Resources.UnloadAsset来卸载指定资源。 建议先从（1）来进行尝试。 该问题来自UWA问答社区，如您对该问题仍有疑问，可以转至社区进行进一步交流。 https://answer.uwa4d.com/question/59f311950558dee424da9d77/ 今天的分享就到这里。当然，生有涯而知无涯。在漫漫的开发周期中，您看到的这些问题也许都只是冰山一角，我们早已在UWA问答网站（answer.uwa4d.com）上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入，也许你的方法恰能解别人的燃眉之急；而他山之“石”，也能攻你之“玉”。 官网：www.uwa4d.com 官方技术博客：blog.uwa4d.com 官方问答社区：answer.uwa4d.com 官方技术QQ群：793972859（仅限技术交流）
接上期UWA 发布的2017 Unity手游体检蓝皮书MMORPG篇，本期我们将深度剖析ARPG的游戏性能发展情况。此份蓝皮书收集了提交到www.uwa4d.com上的所有项目，我们从总体性能数据、引擎各模块开销、内存占用等方面进行了汇总分析，呈现Unity手游行业现状。 这是侑虎科技第275篇原创文章，欢迎转发分享，未经官方授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 报告目录： 一、ARPG手游总体性能开销分析 二、ARPG手游CPU模块性能开销分析 三、ARPG手游内存模块性能开销分析 四、ARPG手游资源管理分析 五、UWA对于ARPG手游研发团队的建议 一、ARPG手游总体性能开销分析 iOS设备的CPU性能普遍高于Android设备，Android设备的CPU均值主体范围为12.4~66.2 ms，iOS设备的CPU均值主体范围为7.0~57.5 ms。 我们选择了测评次数最多的四款机型来进行统计，这四款机型分别是红米2、红米Note2、华为6Plus和三星S6。 1）大部分ARPG手游在中低端设备的性能开销较高，在Android设备上，CPU耗时集中在17~50ms区间，而在iOS设备上，CPU耗时集中在33ms以内。 2）相较2016年同期，2017 Q2 ARPG手游在低端设备上的CPU开销均值增长5.8 ms。 无论是高、中、低端的Android设备，渲染、UI、加载/代码都是需要研发团队重点关注的，它们的消耗总和占了总开销的80%。 下面我们就来具体分析这些主流模块的开销情况。 二、ARPG手游CPU模块性能开销分析 1、渲染模块 严重程度：地狱 为了能够更好地反映出各个性能参数的整体使用情况，我们统计了每种性能参数的主体使用范围，其范围区间是[5%~95%]，以下数据中P5代表5%，P95代表95%。 在Draw Call使用上，ARPG手游的使用程度略低于MMORPG手游，且对Draw Call的控制同样较高，UWA建议Draw Call占用的P95 < 200，76.3%的项目可以将其控制在这个范围内。 1）ARPG手游的场景渲染三角形面片数量普遍较高。且设备性能越高，数量峰值越高。经过统计，在中低端设备中，46.5%的项目能将渲染三角形面片数控制在100K以下。 2）ARPG游戏的场景复杂程度明显低于MMORPG游戏，渲染面片峰值要比MMORPG手游平均低50K。同时，虽然2016 Q4和2017 Q1渲染面片数量大幅上升，但2017 Q2开始回落趋势较为明显。 Graphics.Bilt操作平均每次调用的CPU耗时主要分布在: 0.1~8.5ms。该项CPU开销在低端设备上的上升趋势较为明显，但在2017 Q1大幅下降。这主要是因为，在2017 Q1后，ARPG项目普遍开启了多线程渲染功能。 2、UI模块 严重程度：地狱 使用NGUI作为UI解决方案的项目占据了较高的比例，但从趋势上来看，UGUI从2016 Q3开始，增速非常明显。通过进一步分析，我们发现近大半年来的新ARPG手游中，使用UGUI的项目显著增加，从而大幅提升了UGUI在ARPG项目中的使用占比。 下面我们将分别说明NGUI和UGUI的CPU耗时和堆内存占用情况。 UI模块的性能开销依然很高，也是我们统计中的第二大性能杀手。在中低端设备上，超过90%的研发项目在UI端都面临较为严重的性能问题，主要体现方面与MMORPG游戏类似，功能界面复杂、同一时刻需要更新的Panel多等。但ARPG的平均耗时要略低于MMORPG，这是因为ARPG主要仍以推图形式为主，其在战斗副本中的玩家是预先固定的，大量玩家同屏打怪的情况很少出现。 经统计，NGUI中堆内存每10000帧分配的主体范围为 6.0~81.7 MB，UGUI中堆内存每10000帧分配的主体范围在12.3 MB以内。从堆内存分配来看，UGUI远低于NGUI。 3、逻辑代码 严重程度：噩梦 1）GC触发频率很高，是造成卡顿的主要原因之一，目前只有12.5%的项目可以将GC的触发频率控制在1000帧/次以上。 2）在2017年GC平均耗时趋势虽有所上升，但相比于2016年已经大幅降低。 1）Instantiate实例化操作平均每次调用的CPU耗时主要分布在: 1.2~43.5 ms，且主要集中在10ms以内。 2）Instantiate实例化耗时在2017 Q2之后大幅下降，这说明越来越多的研发团队开始重视项目运行时的资源管理。 Shader.Parse 操作是比较突出的性能杀手，目前平均每次调用的CPU耗时主要分布在 4.4~145.2ms。但随着越来越多团队了解该项的耗时特性后，该项的CPU耗时呈现出较为明显的下降趋势。 4、动画模块 严重程度：噩梦 1）Animator.Update/Animation.Update的开销在中低端设备上的耗时普遍较高，但整体耗时在2017年上半年有明显的下降趋势。 2）ARPG游戏的MeshSkinning.Update的耗时明显高于MMORPG游戏。这主要体现在角色模型的面片数和骨骼数方面，ARPG更加偏向于动作表现力上。 5、粒子模块 严重程度：普通 1）粒子系统的CPU开销普遍较低，且近一年来下降趋势较为明显。 2）粒子系统的数量使用仍然较大，这样会造成较高的内存占用。UWA建议将粒子系统的使用数量峰值控制在400以下，目前仅21.8%的项目达到。 6、物理模块 严重程度：普通 物理模块的性能普遍较好，但呈现出较为明显的上升趋势，因此在中低端机上依然需要研发团队关注。 三、ARPG手游内存模块开销分析 内存泄露是研发过程中经常面临的严峻问题，依然有37%的项目存在不同程度的内存泄露。我们可以看到从2016 Q2开始，内存泄露占比大幅下降，但随着项目新功能的开发以及新项目的涌入，其占比又开始大幅攀升回来。虽然整体趋势有所下降，但内存泄露问题的有效防范和解决，依然任重道远。 1、总体内存 严重程度：地狱 1）总体内存峰值主要分布在95.6~573.9 MB区间，且均值在300MB以上。 2）与MMORPG游戏相比，ARPG内存占用的上线起伏更加明显。经过进一步分析，我们发现，2016 Q4的内存占用在大多数老项目中的主要资源（纹理、网格和动画片段等）都得到了较大的内存优化；但随着2017 Q1新项目不断涌入，内存占用又大幅回升；而2017 Q2内存的下降则是因为AssetBundle内存的大幅下降，大多数研发团队开始通过LoadFromFile来加载LZ4格式的AssetBundle，从而让内存又迅速回落。 2、总体堆内存 严重程度：地狱 1）总体堆内存峰值主要分布为 9.7~109.6 MB，仅37.7%的项目可以把总体堆内存峰值控制在40MB以内。 2）堆内存使用上升趋势明显，建议研发团队重点关注。对此，UWA建议重点关注以下几点： 严格关注配置文件的使用，避免一次性加载过大的配置文件而撑大堆内存； 不少项目的堆内存增大是由于热更新所致。因此，对于过大的数据Data的使用，建议通过分帧进行，并及时进行GC回收； 避免不必要的代码堆内存分配，虽然是老生常谈，但从趋势上可以看出，大多数研发团队仍需加强重视。 项目的内存占用很大一部分来自于资源的使用，下面我们将对项目中主流资源的使用情况进行分析。 3、纹理资源内存 严重程度：地狱 1）纹理内存峰值主要分布为 22.4~198.8 MB，且主要分布在50~100MB区间。 2）内存峰值占用在2016 Q4大幅下降，但在今年后又开始逐步上升，建议研发团队密切关注自身项目的纹理资源使用情况。 4、网格资源内存 严重程度：噩梦 1）网格资源内存峰值主要分布为 1.0~ 47.5 MB，且主要集中在40MB以内。 2）ARPG游戏的场景较之MMORPG游戏普遍较小，场景相对轻量和简单，因此，其网格资源量明显低于MMORPG游戏。 5、动画资源内存 严重程度：噩梦 动画资源内存峰值主要分布在38.7 MB以内，且主要集中在15MB以内。 6、Shader资源内存 严重程度：噩梦 1）Shader资源内存峰值主要分布在6.1 MB之内，且主要集中在1MB之内。 2）从内存趋势上来看，近一年来内存占用下降较为明显。 7、RenderTexture资源内存 严重程度：地狱 1）RenderTexture资源内存峰值主要控制在67.7 MB以内，且在各个区间都较为平均。 2）内存使用上升趋势明显，建议研发团队对其在自身项目中的使用情况特别关注！ 8、粒子系统资源内存 严重程度：噩梦 1）粒子系统资源内存峰值主要分布在 33.7 MB以内，且主要集中在20MB以内。 2）粒子系统的内存占用一直较为平稳，虽然在2011年Q1期间高端机上的内存占用较高，但在2017年Q2则大幅回落到之前水平。 3）目前，粒子系统的内存占用仍然较为“虚高”，即游戏运行期间Active数量远小于内存中的总数量，在大部分项目中，Active的粒子系统数量占比不到10%，即粒子系统的内存占用峰值应该在5MB左右。 四、ARPG手游资源管理分析 1）在资源加载方面，AssetBundle.Load和Resources.Load是项目中的主要加载方式。 2）通过调用趋势可以看出，研发团队对于Resources.Load的使用仍然有明显的增长趋势。对此，UWA强烈建议研发团队尽可能使用AssetBundle作为游戏资源的主要加载方式。 1）LoadFromFile(Async)是ARPG项目中AssetBundle的主要加载方式。 2）LoadFromFile(Async)的使用在2017 Q2上大幅增长了46%，这是在UWA DAY 2017上我们非常建议的AssetBundle加载方式(Unity 5.3版本后)，研发团队可以通过UWA Blog(blog.uwa4d.com)的相关文章查看具体的性能对比。 Instantiate/Destroy和Active/Deactive调用次数较高，需要研发团队根据自身项目情况时刻注意： 1）由于Active/Deactive每次调用的CPU开销不大，且不会造成崩溃、闪退等问题，所以研发团队极易忽视这两项操作所带来的性能问题，因而出现了游戏运行1万帧Active/Deactive上万次调用的情况。因此，此处存在着很大的性能浪费； 2）Instantiate/Destroy的调用次数同样较高，虽然不如Active/Deactive频繁，但每3帧调用1次Instantiate的频率依然不容忽视，建议研发团队坚持加强项目的资源管理，避免不必要的频繁Instantiate/Destroy操作发生。 五、UWA 对于ARPG手游研发团队的建议 一、警惕渲染模块和UI模块这两大性能杀手！随着精品化ARPG项目的日益增多，在日后很长一段时间内，这两项依然将是研发团队在性能优化方面的重中之重！ 渲染方面，Draw Call的控制技术已被越来越多的团队所熟练掌握，并且随着设备的发展，Draw Call的影响将越来越小，反观渲染Triangle数量将逐步成为渲染模块的新的性能瓶颈，越来越大的渲染Triangle量会给GPU、耗电和发热方面带来巨大的压力； UI方面，UI网格重建开销在未来相当长的一段时间内仍然是研发团队头痛的问题，UI优化的原则非常简单：动静分离，但真实操作起来则是非常困难。90%项目中的UI性能问题是由于动静元素没有分离导致，但没有任何两个项目的具体原因是一致的。这是UI性能优化方面非常困难的主要原因。 二、逻辑代码方面，Instantiate实例化开销虽然在2017 Q2呈现较为明显的下降趋势，但仍需要研发团队持续对资源管理进行监控。同时，GC开销的上升趋势明显，需要研发团队加强对Mono堆内存的优化意识； 三、在内存优化方面，与MMORPG游戏类似，Mono和RenderTexture内存占用在2017年的上升趋势非常明显，对此，建议研发团队在接下来的项目研发中密切关注这两项的内存使用； 四、在动画模块方面，ARPG游戏的CPU耗时明显高于MMORPG游戏。对此，我们的建议如下： 尝试动画片段的精度缩减技术，该技术在UWA Blog和UWA问答上（answer.uwa4d.com）都有详细的讲解和代码分享； 由于ARPG游戏对于动作表现力的要求很高，因此，相比于Humanoid模式，Generic模式更加适合于ARPG项目，它可以让你的动画表现与3DMax、Maya等制作表现更加一致； 随着ARPG中怪物角色的数量持续增加，GPU Instancing技术会受到更多游戏团队的青睐，具体做法可参加UWA Blog相关文章。但同样需要注意的是，研发团队需要控制同屏的渲染面片数。 五、资源加载方面，Resources.Load依然是比较主流的资源加载方式。但UWA建议尽可能尝试通过AssetBundle.Load来加载相关资源。在2017 Q2以后，LoadFromFile(Async)已经成为ARPG研发团队的主流AssetBundle加载方式，对于仍然没有使用该方式的研发团队，建议查看UWA的相关直播信息，并找到对应的性能比较和分析； 六、以上数据是我们对于ARPG手游性能数据的整体研究和趋势分析，旨在从宏观上为大家展现项目中普遍存在的性能瓶颈和研发团队容易忽视的潜在性能问题。但同样需要说明的是，优化是一个“抽丝剥茧”的过程，需要研发团队花费大量的时间和耐心去完成。UWA建议：多做测试，以数据说话，空杯心态，将经验归零。 以上就是UWA发布的2016-2017年ARPG手游性能测评汇总和分析。此次，我们从性能参数上增加了资源加载、资源卸载等数据扩充了报告的完整性，在测评参数上加入了机型、历史数据等方便研发团队能及时进行横向和纵向的比较。我们深信数据是会说话的，总有规律值得我们追溯求实、鉴往知来。同时我们也会不断细化评析的标准、划分，以使得这些数据更有代表性和说服力。 关于UWA 由侑虎科技开发的游戏/VR应用性能优化平台，目前提供 1）性能检测与优化 2）资源检测与分析 3）UWA GOT 三大工具。同时，我们也会为大家开发更省心的功能，希望通过它们可以减少开发者反复测试定位问题的时间，从而将更多的精力集中在项目开发和解决问题中，能为大家项目研发省下的任何一分一秒，都是UWA团队的骄傲。
几乎所有的Unity开发者都曾为AssetBundle管理机制填过“坑”。如何巧妙合理地设计AssetBundle的依赖关系来尽可能减小包体的大小？怎样针对不同的资源选择不同的API来提高加载的效率？想必你也肯定有过类似的疑问，今天我们将结合UWA性能检测与分析报告，为大家分享针对资源管理中的优化方式，从而高效地定位资源漏洞并优化解决。 我们将针对资源管理分别介绍优秀级和大师级优化方式。（没错，今天就是为那些将要晋升为CTO的工程师准备的福利！） 一、优秀的优化方式 1. 查看资源加载（卸载） 在UWA性能测评报告的资源管理页面中，研发团队可以方便地查看项目运行时资源的加载情况，如通过Resources.Load/LoadAsync、AssetBundle加载（卸载）的具体资源信息。 我们提供了三种模式供大家自由地查看：查看所有资源、查看指定场景所用资源和查看指定帧所用资源。 通过该界面，研发团队可以明确： （1）项目运行过程中，哪些资源是通过Resources.Load/LoadAsync来加载的； （2）项目运行过程中，哪些资源存在频繁加载的情况； （3）项目运行过程中，资源加载的具体时间点； （4）项目运行过程中，资源加载的具体耗时。 建议大家通过以上四点，来快速定位资源加载存在的问题和漏洞，并及时对加载逻辑代码进行完善。 2.关注AssetBundle文件加载/卸载方式 在UWA性能测评报告中，大家可以直接查看项目运行时AssetBundle文件的加载方式。目前关于Unity AssetBundle的主流加载方式有以下四种：New WWW、WWW.LoadFromCacheOrDownload、AssetBundle.Load/CreateFromFile和AssetBundle.Load/CreateFromMemory。 点击上图中每个Widget，就可以看到该加载方式的具体使用情况。下图为New WWW在游戏运行过程中的整体调用情况； 建议研发团队对LoadFromMemory谨慎使用。由于LoadFromMemory的加载效率相较其他的接口而言，耗时明显增大，因此我们不建议大规模使用。如果希望对AssetBundle文件加密，那么建议仅对重要的配置文件、代码等进行加密，对纹理、网格等资源文件则无需进行加密。因为目前市面上已经存在一些工具可以从更底层的方式来获取和导出渲染相关的资源，如纹理、网格等，因此，对于这部分的资源加密并不是十分的必要性。 3.关注AssetBundle的具体使用情况 在UWA性能测评报告中，研发团队可以从以下几个方面直接查看AssetBundle文件的具体使用情况。 通过该界面，研发团队可以明确： （1） AssetBundle文件在项目运行过程中的具体加载时间点； （2） 具体加载的AssetBundle文件名称； （3） 具体AssetBundle文件的存放位置； （4） 每个AssetBundle文件的加载次数。 注意：UWA建议研发团队时刻关注AssetBundle文件的加载次数，查看是否存在同一AssetBundle被持续加载的情况，如上图中蓝色方框1和2所示。 二、大师级优化方式 除了以上提及的优化方式，下文我们将介绍性能优化中省时高效的黑科技，如何在极短时间内快速定位到问题的瓶颈，节省优化的时间，这些分析利器你千万不可错过！ 1.查泄露、查驻留，一个都不能少 在UWA性能测评报告的资源管理页面中，研发团队可以方便地查看项目运行时AssetBundle在内存中的驻留情况，从而快速看到AssetBundle是否存在泄露的问题。 上图的AssetBundle使用数量在不断上升，很可能存在AssetBundle资源泄露问题。 同时，研发团队也可以查看指定AssetBundle文件在项目运行过程中的的加载和卸载情况。点击上图的某一帧或某一场景，我们就能查看对应的具体AssetBundle信息，包括具体名称、存放的位置、加载方式、内存中的驻留时长和由该AB加载出的资源数量。 除此之外，UWA报告也提供资源内存的泄漏分析，通过资源的“场景比较”如同种类型场景或同一场景进行比较、不同类型场景进行比较等功能，从而更快速地找到存在“泄露”问题的资源。 2.时刻关注GameObject的Instantiate/Destroy情况 在UWA性能测评报告的资源管理页面中，研发团队不仅可以查看到项目运行时GameObject的Instantiate和Destory调用次数，还可以详细查看每次调用时的具体GameObject信息。 建议研发团队对表格中操作次数大于50的GameObject进行详细分析，避免同一GameObject频繁Instantiate和Destory的操作出现。 3.关注GameObject的Active/Deactive情况 此外，UWA的性能报告界面中还提供了项目在运行时，GameObject的具体Active和Deactive情况。 很多使用过的用户都反馈“大吃一惊”！因为在项目运行1~2万帧的时间里，GameObject Active/Deactive执行了几万甚至几十万次。 如果大家的项目中也存在上述过量的Active/Deactive调用，那么UWA建议可以直接查看页面下方的表格，了解到底是哪些GameObject存在大量的调用操作，然后检测与其相关的脚本，即可快速完善和修复这一问题。 当然，要想驾驭好这些优化利器，还需要有扎实的引擎原理基础，在此小编为大家送上UWA的几篇干货，相信能为大家在资源管理方面的优化事半功倍！ 你应该知道的AssetBundle管理机制 Unity 5.x AssetBundle 工作流程详解 Unity 4.x AssetBundle 工作流程详解 关于UWA 由侑虎科技开发的游戏/VR应用性能优化平台，目前提供1）性能检测与优化和 2）资源检测与分析 两大工具。 同时，我们也会为大家开发更省心的功能，希望通过它们可以减少开发者反复测试定位问题的时间，从而将更多的精力集中在 项目开发和解决问题中，能为大家项目研发省下的任何一分一秒，都是UWA团队的骄傲。
给调皮的AssetBundle加上面向对象式加载调试管理
相信每一个使用Unity引擎的老司机都会被它的一个功能所困扰过——AssetBundle。本文从加载、调试、异步、垃圾回收、路由5个方面，介绍了基于面向对象思想设计的用来进行AssetBundle加载的ResourceModule管理器。 一、变味的易用性 Unity是一款主打易用性的游戏引擎。它支持开发者可以低门槛、快速容易地使用Unity开发游戏，所以Unity在最初以类似JavaScript、类似Python的脚本语言作为主要开发语言。目前，Unity几乎已经成为手游开发的首选方案了，其自身的功能和各种围绕它的技术生态日趋完善，C#语言也当仁不让地成为首选开发语言。 但是，她的骨子里依旧还是那个标榜易用性的游戏引擎，由于她“易用”这个特点，开发团队如果在开发之初按照Unity标准的易用方式来制作游戏，到了后期就免不了出现各种各样的坑，需要花费大量的时间去重新重构和代码维护。最典型的情况就是： 一个开头快速功能迭代开发的游戏，直到中后期才萌生热更新的需求，而在Unity里做资源热更（AssetBundle）和代码热更（Lua）是一个不小的工作，需要耗费相当多的精力。 怎么办？加班或延期呗。 回想使用Unity这5年的搬砖经历，Unity里最令我沮丧的功能是它的AssetBundle——不论是打包还是加载，都要花费开发人员大量的时间成本去研究和应用。你以为它可以像Resource.Load那样轻松加载资源？不，它还要手工码代码打包；你以为打包完了就能直接加载调用了？不，它还要在加载时注意处理依赖关系。 二、从Unity的资源方式说起 如前面所说，Unity是一款主打易用性的游戏引擎。它的资源打包方式有两种。 Unity资源方式 一种是使用Resource模式，这种模式更像是端游时代的资源打包方式，把所有的游戏资源打包成一整块文件，然后通过索引文件去记录索引，各个资源文件散落在不同文件里的不同索引位置。比如说，暴雪公司的魔兽世界、魔兽争霸的mpq文件就是这样的一个思路。 这种方式完全体现了Unity的易用性，比如一个图片，不管它是PNG、TGA还是PSD，只要丢到Unity里，都会被统一转化成Unity的Texture格式，简单、傻瓜，非常适合小游戏的开发。但是它也有缺点，就是每一次发布最终编译包的时候，都会重新对资源进行一次打包，速度非常慢，这也反映出它的致命问题: 由于资源全部堆砌在一块了，要替换其中的资源就变得困难——难以进行资源热更新。 另一个种模式，AssetBundle模式。相比较之下，这个模式看上去就像后来迭代版本的时候加出来的一个功能——基于原有Resource模式的不足，提供一个对资源控制更自由的方式。 在Resource模式中，开发者几乎不用操心他们资源管理的技术细节。直接使用编辑器进行资源编辑，用完以后直接打包最终程序就可以了。而Asset Bundle模式则需要自己进行资源的打包加载管理。 Unity AssetBundle打包方式进化 在Unity 5.x之前的版本——3.x和4.x中，AssetBundle是一个非常难用的功能。你不仅要操心资源的管理规范，还要写大量的代码控制它们的打包，更要命的是，除了打包速度慢，还有数之不尽的坑。相信不少开发团队都在AssetBundle上花费过不少精力和时间。 我经历过了4个不同的中大型游戏规模的Asset Bundle打包，躺过其中相当多的坑，逐渐开始掌握它的特征。回头仔细一想，其实很多坑完全是没有必要的，但是前提是在设计之初就给予高度重视，提炼统一的方案，这样才不会导致后期失控的状况。 在Unity 5.x里，官方推出了一个全新的打包方案，对于程序员而言，可以仅用一行代码打包所有的AssetBundle。尽管它里面还是有一些坑，可是却大大减轻了开发团队的工作。打包方式变简单了，大家可以集中精力研究怎么更好地去把这些AssetBundle加载起来了。 三、更好的方式去加载AssetBundle AssetBundle加载资源的API非常简单，核心其实只是两类函数，一类同步，一类异步。 相信每一个游戏开发团队都在官方的这些AssetBundle加载API基础上，封装出自己的加载管理类，这几乎是必须的。封装的方式千奇百怪，怎么样去封装会比较好？ 接下来我所讲述的是一种模仿面向对象的AssetBundle加载管理类封装方式，实现方便加载的同时，又可以更容易地进行实时调试。 这种基于面向对象的方式来设计的AssetBundle加载管理器，我们给他一个名字叫ResourceModule，方便下文讲述。它的主要目的是为了让开发者在方便加载资源的同时，提供方便的实时调试功能，并且你会在这个过程中了解到资源文件的热更新策略。 下面将会分成五个部分来介绍ResourceModule：加载、调试、异步、垃圾回收、路由。 3.1 加载器 —— 基于追踪对象 在Unity的标准AssetBundle加载接口中，同步加载返回了行为结果，异步加载则返回了行为追踪对象。具体来说，同步加载直接就返回了资源的AssetBundle；异步加载则返回了异步加载的追踪对象AssetBundleCreateRequest。追踪对象，用于之后进行资源异步加载情况跟踪，被协程判断是否已经异步加载完毕，若完成了可从追踪对象里获取加载资源。 由于同步和异步的加载API不一样，在项目实际应用时，往往没有统一的加载接口。要避免这种情况，可以统一加载行为，返回追踪对象。这也是ResourceModule加载方式的核心。 3.1.1 函数式接口 在ResourceModule里，提供了两个最简化的加载AssetBundle API，看上去就跟Resources.Load一样简单。 这里的函数式接口跟官方不一样的地方：ResourceModule的函数式接口的返回值，将始终是一个AbstractResourceLoader对象，也就是“追踪对象”——对于异步加载，使用追踪对象，可以判断异步加载的进度并获取加载后的资源；对于同步加载，使用追踪对象立即获取资源；它也提供错误处理信息；并且后续所讲及的实时调试，也是基于这个追踪对象。 两个接口的返回值类型是一样的。 看上去，AssetBundle的加载接口很简单。但本质来说，这只是接下来Loader对象式加载的一个使用简化。 3.1.2 Loader对象 ResourceModule.LoadBundle的本质，是使用AssetFileLoader进行加载行为，并把自己作为追踪对象返回。AssetFileLoader本身是对UnityEngine.Object进行处理，它自身可以通过配置，修改成使用Resources.Load模式或AssetBundle模式。 当AssetFileLoader配置成AssetBundle加载模式，它就会调用AssetBundleLoader进行AssetBundle加载行为，而AssetBundle本身则使用HotBytesLoader进行AssetBundle文件字节码进行加载。 HotBytesLoader是一个热更新桥接器——根据资源“相对路径”和“热更新资源目录”，当热更新资源目录存在对应路径的文件时，使用热更新目录的资源。 所以说，一次加载行为会有4个Loader产生，它们之间形成链式关系。即AssetFileLoader -> AssetBundleLoader -> HotBytesLoader -> WWWLoader。 如前所说，ResourceModule函数式加载其实Loader对象式加载的一个简化。每一次加载行为都会对应一个Loader对象。那么基于AssetFileLoader，由于它是一个单独的解耦对象，我们还可以针对它一些特定需求的功能扩展： Loaders 在不同类型的资源加载中，不同的行为被划分成不同的Loader对象，来给资源加载代码赋予更好的维护性和可读性。同时，由于链式关系的存在，指定的AssetBundle文件永远只会被加载一次——从而避免一些项目中常见的AssetBundle文件被重复加载问题。 每一个Loader对象，都有一个静态.Load函数，这是一个工厂函数，每一个Loader对象通过自身的Load静态函数生成Loader，来确保引用计数、状态的正确。 3.2 对象式调试 Profiler中的AssetBundle Unity的Profiler可以方便地提供各种Unity运行时资源的调试功能。它采用快照的方式，捕捉当前运行时状态。只要你对Profiler足够熟悉，大部分运行性能问题都能从中发现。 对于AssetBundle加载，Profiler是不能实时获取动态的，即使发现了AssetBundle残留，也难以发现具体是哪部分代码残留了。而这类调试的事情，我们可以通过游戏里的统一加载接口来更易发现。 由于加载所使用的每一次行为都对应着一个Loader追踪对象，所以我们要对资源加载行为进行实时调试，简单来说就是对这些追踪对象进行监视。这里用了一个偷懒的方式：Unity引擎编辑器本身就是基于游戏对象的。 那好吧，我们把加载对象以游戏对象的方式显示在编辑器上，每创建一个Loader，就紧跟着一个GameObject，达到可视化实时调试的目的。 调试追踪对象和加载后的资源 从上图可知，每一个Loader追踪对象（加载行为）都被一个静态的全局列表包存起来，因此可以很方便地在Unity编辑器上显示它们的具体数量，我们把这些称为“调试对象”，点击后右边还能显示其引用计数和资源路径。 3.3 异步风格 Unity的协程是一个非常好用的单线程异步编程方式，让普通开发者在没有线程编程的基础下，也可以方便地进行异步编程。 另一种常见的单线程异步编程方式是回调Callback风格，是非阻塞IO语言NodeJS的主要异步方式。 无论是协程还是或者回调，它们都有一个共同的特点，都可以做到是基于单线程进行了异步编程。关于异步编程这个话题可以引申出很长的篇幅，这里就不多介绍了。 Unity开发中，两者各有优点。协程可以让看起来同步的代码实现了异步，但在Unity中它有一个蹩脚的地方是需要另写一个IEnumerator ()函数； 而Callback风格则由于C#中强大的匿名函数语法，使得让异步代码写起来更加的方便。 ResourceModule中两种异步风格并存，可以根据喜好使用。 3.3.1 协程式 这种协程可谓在Unity中最为常见、舒服的异步方式了。使用起来跟Unity原生的WWW差不多。 3.3.2 回调式 相比而言，匿名函数回调的异步风格，可以写更好的代码，并且调用代码更紧密连接。 3.4 垃圾回收——基于引用计数 我们都知道Java/C#语言的核心是面向对象，他们之所以那么的强大还有一个杀手锏，就是完全自动垃圾回收机制。因其基于对象的设计，所有对象的生命期都是可以被监视和管理的。做过iOS开发的同学也知道，Objective-C语言的内存管理使用引用计数的方式来实现的。 由于ResourceModule的加载行为都是基于对象，多个Loader对象有互相引用的关系，ResourceModule模仿了Objective-C引用计数的方式来实现AssetBundle对象的管理。 调试对象的信息 点开调试对象的GameObject，就能看到调试对象的引用计数信息和加载所耗费的时间。 3.4.1 资源的释放 要对加载Loader追踪对象进行引用计数递减。 当一个Loader的引用计数为0时，它就会进入到释放队列，待几秒后释放。 为什么不像java那样能全自动的判断对象是否无用自动释放？ ResourceModule的加载器需要手工释放引用计数。因为没法捕捉GameObject对象删除事件，Unity并没有提供这样的事件出来监视游戏对象的删除事件，所以无法捕捉到什么时候去把这一个对象的引用递减，所以只能手动去进行引用计数的管理。 3.4.2 延迟清理 当一个加载对象被引用计数减为0的时候，他不会被立刻释放。因为存在这样一种场景：当引用变成0的同一时间，同样的资源又被创建一份新的，引用计数立刻变回1。所以如果说当他引用计数为0时候，立刻就被清理了，同时又被创建，这里就会造成了重复的对这份内存资源创建和释放。 3.5 路由——管理资源加载的路径 Unity是一个跨平台的游戏引擎，每一个平台都会有它特殊的处理资源的路径方式，在Unity中一般我们常见的是StreamingAssets和PersistentDataPath两种路径。可是这里面也隐含有不少的坑，比如说在windows平台里面，路径URL，斜杠必须得3个///。安卓平台下，StreamingAssets目录是不能同步读取的（APK内目录），但是包括iOS在内的其他所有平台都是可以通过同步File.ReadAllBytes读取的。 不仅如此，由于AssetBundle的打包是平台定向性的：打出Android的AssetBundle，不能在iOS下使用；反之亦然。因此，AssetBundleLoader加载器在实际运行时，需要一个路由管理器来告诉它什么样的平台，使用哪里的AssetBundle目录。 ResourceModule路由 所以在ResourceModule中，路由管理器做了很多路径的识别工作来统筹各种不同平台下的资源路径，让AssetBundle模块开箱即用。 四、热更新 我们使用AssetBundle，无非最想解决的就是一个需求——热更新。 资源路由管理器除了平台差异化路径处理，另外的核心功能就是热更新路径处理了——即此前所说的，优先判断PersistentDataPath路径是否存在指定的热更文件。 五、后记 以上我们分别从加载、调试、异步、垃圾回收、路由5个方面，介绍了这种基于面向对象的思想，来进行AssetBundle加载的ResourceModule管理器。它的代码开源放在Github ResourceModule 。它是Unity开发框架KSFramework的核心部分。对于很多使用者来说，ResourceModule就像一个黑箱子，虽然一直能用，但是一直不好理解它的内部构思，所以就有了本文。它的本质是将行为进行对象化。概括来说就是把加载行为以对象的方式保存起来。 ResourceModule管理器开源代码： https://github.com/mr-kelly/KEngine/tree/master/KEngine.UnityProject/Assets/KEngine/CoreModules/ResourceModule 这是侑虎科技第208篇原创文章，作者陈霈霖，欢迎转发分享，未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们，一起探讨。（QQ群：793972859） 博客：http://www.jianshu.com/u/674f1a626944，作者也是U Sparkle活动参与者哦，UWA欢迎更多开发朋友加入 U Sparkle开发者计划，这个舞台有你更精彩！
关于Unity加载优化，你可能遇到这些问题
关键字 资源加载、卸载 Instantiate 实例化 资源管理方式 一、资源加载 Q1：将Shader独立打包，如果我在启动游戏的时候加载一次，那么之后切换场景是不是就不用每次都加载了？ 确切地说，要达到后续Shader都不出现加载开销，需要满足以下两个条件： （1）包含Shader的AssetBundle文件常驻内存； （2）Shader已经全Load加载好； 只要满足这两个条件，后续加载好的GameObject，但凡依赖于这些Shader的，都会直接拿来进行使用，而不会再有加载和解析开销。 Q2：假设有两个界面Panel A和Panel B，它们均依赖一个共享Atlas C。AssetBundle打包时，如果我们将其分别打包的话，那么在使用时是否不能将AssetBundle C进行卸载？如果在加载Panel A和B之前，将Atlas C加载到内存中，但是将AssetBundle C进行卸载，那么在加载和实例化Panel A和B时，是否会出问题？ 是的，不仅是Panel和Atlas，只要是A或B依赖于C，就必须保证在加载或实例化A或B时，C的AssetBundle一定存在于内存中。如果之前将C对应的AssetBundle进行卸载，则加载或实例化A和B时，引擎将无法自动将C绑定给A和B进行使用。 关于AssetBundle依赖性打包，我们已在之前的分享【揭开AssetBundle庐山真面目】中有所提及。 Q3：请问这个Loading.UpdatePreloading是什么东西，为什么会突然那么高？一般情况下有没有什么优化的办法？ 这是Unity引擎最主要的加载函数。该项一般在切换场景时或主动动态加载资源时较大。 一般来说，加载资源越多、越复杂，则其反映的Loading.UpdatePreloading耗时则越大。 优化之前，必须先定位该函数的CPU占用瓶颈。下图则为我们的案例项目，Loading.UpdatePreloading函数在UWA测评报告中的总体CPU分配情况。通过这个堆栈信息，开发团队就可以对函数的耗时分配一目了然，从而有的放矢地进行优化。 Q4：关于Loading.ReadObject耗费比较高，有什么推荐的方法吗？ Loading.ReadObject是Unity引擎的资源加载函数，一般出现在切换场景和加载API调用时，这其中包括纹理、网格、Material、Shader、AnimationClip等资源。如果你发现该值过高，建议去大力优化加载的相关资源。对于每种资源的加载，我们正在以专题的方式进行总结和分享，建议大家可以先查看以下内容： 加载模块之纹理 加载模块之网格 加载模块之Shader 加载模块之动画片段 Q5：我使用Shader.WarmupAllShaders操作，在后续加载资源还是有CreateGPUProgram出现。(Shader都在一个AssetBundle文件中，都是常驻内存的，不会删掉)是必须使用ShaderVariantCollection来加载Shader吗？ 以上这种问题的较大可能是：Shader被打包到不同AssetBundle中了，WarmupAllShaders仅能对当前内存中的Shader进行warm up。后续如果又有Shader加载进来，则仍然会出现CreateGPUProgram操作。 建议开发团队使用UWA的资源检测工具，检测下AssetBundle中Shader的具体打包情况，查看是否出现了冗余打包的问题。 Q6：我们在UWA上进行了性能测试，发现安卓上同步加载AssetBundle资源会非常耗CPU, 所以近期对资源加载方式做了比较大的调整，绝大部分的资源使用异步加载的形式。 不过这里有个疑问想咨询下: AssetBundle.LoadAsync 和 WWW加载方式都可以用来异步加载AssetBundle, 但是两者API特点也不同, 目前看WWW更耗内存一些, 请问这两种方式更建议使用哪一种 ? AssetBundle.LoadAsync 是在获取了 AssetBundle 对象之后加载其中的资源的；而 WWW 加载是在获取 AssetBundle，两者的作用是不同的。 开发者可能是希望了解异步加载 AssetBundle 的几个 API 之间的区别，相关的接口如下： new WWW WWW.LoadFromCacheOrDownload 这两种方式的具体区别可先参考《你应该知道的AssetBundle管理机制》中的“AssetBundle加载进阶”部分。 二、资源卸载 Q1：切换场景时，卸载上一个场景总感觉耗时，请问大家是否有什么推荐的方案？ 这是Unity在切换场景时调用Resources.UnloadUnusedAssets这个函数造成的，通常情况下其开销都是会比较大。建议开发团队通过UWA性能检测，在加载模块中进一步定位卸载场景的元凶。 Q2：Resources.UnloadUnusedAssets() 不仅局限于Resources.Load的资源，还包括AssetBundle.Load的资源，是吗? 是的，Resources.UnloadUnusedAssets同样可以卸载由AssetBundle.Load加载的资源，只是前提是其对应的AssetBundle已经调用Unload(false)，且并没有被引用。 Q3：我用UGUI做的一个界面中有一个背景图片，游戏中没有做任何处理，关闭销毁这个界面后这个图片还在内存中，但是我已经调用过了Resources.UnloadUnusedAssets，如下图所示。我的预设没有打成AssetBundle，是放在Resource路径加载的。请问是什么原因导致的呢？ 在使用 Resources.Load 加载 UI 界面的情况下，即使“关闭销毁这个界面”后，Resources.UnloadUnusedAssets 确实还是无法卸载对应的图集的。因为此时该图集依然被 Resources.Load 加载出来的 Prefab 引用。 对于这种情况，我们的建议是手动调用 Resources.UnloadAssets 来手动释放图集（可以通过 Sprite.texture 来找到对应的图集），在重新实例化该 UI 界面时，图集也会自动进行 Reload 的。 Q4：用Resources.UnloadAsset 释放未Instance的Object 会出现这样的错误 :Unload Assets may only be used on individual assets and can not be used on GameObject's/Components or AssetBundles.请问该如何解决？ Resources.UnloadAsset仅能释放非GameObject和Component的资源，比如Texture、Mesh等真正的资源。对于由Prefab加载出来的Object或Component，则不能通过该函数来进行释放。 Q5：我在Profiler中看到 GC.MarkDependencies 的CPU消耗有900ms+， 虽然在退出战斗的时候调用了Resources.UnloadUnusedAssets(); 可是卡顿还是很明显，请问有什么推荐的方案吗？ GC.MarkDependencies的消耗是由Resources.UnloadUnusedAssets引起的。该函数的主要作用是查找并卸载不再使用的资源。游戏场景越复杂、资源越多，该函数的开销越大，一般在300~2000 ms范围内。所以，我们在UWA报告中加入了对该函数的调用监测，以便让大家更好地掌控它的调用情况。 对于该函数的优化，我们建议一方面控制场景中不必要的资源量，同时通过UnloadAsset来及时卸载不再使用的资源，以减少Resources.UnloadUnusedAssets的耗时压力。 后续，我们会在加载模块的相关文章中为大家详细讲解Resources.UnloadUnusedAssets的性能问题，敬请期待。 Q6：我有一个UI预设，它使用了一个图集， 我在打包的时候把图集和UI一起打成了AssetBundle。我在加载生成了GameObject后立刻卸载了AssetBundle对象， 但是当我后面再销毁GameObject的时候发现图集依然存在，这是什么情况呢？ 这是很可能出现的。unload(false)卸载AssetBundle并不会销毁其加载的资源 ，是必须调用 Resources.UnloadUnusedAssets才行。关于AssetBundle加载的详细解释可以参考我们之前的文章：你应该知道的AssetBundle管理机制。 Q7：如果先Destroy Prefab ，然后将Prefab中用到的AssetBundle再进行Unload，这样的顺序是否会有问题 ？ 我在手机上测试时发现这样做内存中就会一直存在，不释放；如果反过来， 就可以释放。另外，我是在Destroy 的时候调用的Resources.UnloadUnusedAssets();，请问这会影响最终的结果吗？ 确实可能发生这种情况，在 Resources.UnloadUnusedAssets(); 时，如果还没有进行AssetBunlde的Unload 操作，那么从AssetBunlde中加载的资源依然会因为被AssetBunlde引用而无法被卸载。 开发团队可以尝试 Destory 后做 AssetBunlde的Unload，最后进行 Resources.UnloadUnusedAssets(); 。 三、管理方式 Q1：现在Unity还不能将场景和 NavMesh数据或者Lightmap数据分离，是吗？我是想先加载一个干净的场景，然后再动态切入不同的光照贴图和NavMesh网格数据，有什么办法吗？ Lightmap是可以与场景分离的，并且可以通过AssetBundle进行动态加载。建议将Lightmap作为普通的资源进行打包，动态加载后，通过LightmapSetting接口整体替换场景的Lightmap。目前，Navmesh确实是无法与场景分离的。但是在Unity 5.x版本以后，引擎已经允许通过LoadLevelAdditive加载多个场景的方式来加载NavMesh，因此，研发团队可以尝试预先将NavMesh在多个场景内烘焙好，然后通过LoadLevelAddtive或类似API来进行加载，从而达到动态加载NavMesh的效果。 Q2：从点击应用到出现游戏画面，这个时间长短是不是和Resource目录大小有关系？ 从点击应用到首次出现应用画面，其加载时间主要与两方面相关： 1、Resources文件夹中的资源数量。在游戏启动时，Unity引擎会为Resources文件夹下的资源建立一个查找树来存放与其对应的索引，便于后续资源的加载。一般来说，Resouces文件夹下资源数量越多，其构建时间越长，应用的启动也就越慢； 2、首场景的资源加载和相关代码的初始化工作。如果首场景的资源量较多，其脚本初始化的任务较重，则应用的启动时间也会越慢。 Q3：AssetBundle在使用时解压出来的资源会占用一定的内存。我们现在想尝试使用两种加载方式：（1）在AssetBundle加载相关的资源后，将资源进行缓存，并卸载AssetBundle文件；（2）对AssetBundle文件进行缓存，以后用到相关资源后再进行直接进行加载。请问这两种方式你们推荐哪一种比较好？ 对于Unity 5.3版本之前的项目，建议通过LoadFromCacheOrDownload或LoadFromFile的方式来加载AssetBundle，这样既可以降低Assetbundle对象的内存占用，又可以保持AssetBundle与资源之间的链接关系，从而方便后续具有依赖关系的Prefab的加载。 对于Unity 5.3版本之后的项目，可以从AssetBundle打包本身进行处理。即：使用LZ4格式的AssetBundle文件，而不使用默认的LZMA格式。同样可以达到上述的需求，稍有不足的是，LZ4格式的AssetBundle文件较之LZMA格式的文件占用稍大。 如果是依赖关系打包，对于被依赖的共享资源AssetBundle文件，我们还是建议将其缓存在内存中，虽有一定的内存增加，但可以通过上述办法极大地降低内存上的占用，缓解内存压力。 Q4：怎样动态加载Navmesh？ 目前Navmesh不支持动态加载，只能随场景一起加载， 因此可以考虑将带有Navmesh的场景打包成AssetBundle，然后使用LoadLevel加载AssetBundle中的场景。 Navmesh的动态加载已经在Unity的Roadmap中。而当前，Navmesh是和场景绑定的，也就是说目前只能通过LoadLevel（不支持> > LoadLevelAdditive的加载方式）来加载场景的同时，自动加载对应的Navmesh数据。替代方案是：将多个“场景Prefab”的Navmesh 合并到同一个场景中烘焙好（互不重叠），然后再将这些“场景Prefab”分离到各个单独的场景中去；在运行阶段，Navmesh随着第一个场景一次性加载，而对于其他的场景物件，再通过LoadLevelAdditive来加载对应的场景即可。缺点是为了使场景物件对齐Navmesh，在场景制作时就不能出现坐标上的重叠，因此仅供参考。 在Unity 5.x下，Lightmap的动态加载，需要通过脚本将烘焙时每个物件的Lightmapindex和Lightmapscaleoffset记录下，并在运行时动态加载后设置回去的方式来实现。因为目前Lightmapindex和Lightmapscaleoffset信息是和场景绑定在一起，储存在Lightmapsnap.assets 中，发布时也是放在场景信息中，因此不会记录在Prefab 上。 Q5：同一个纹理，有多个Prefab生成的实例，会有多份这个纹理的copy吗？ 该问题需要查看 Prefab 的具体加载方式。如果仅是通过 Resources.Load 进行加载，那么纹理是不会存在多份的，但如果是通过AssetBundle加载，每个 Prefab 均为一个 AssetBundle 且纹理没有进行依赖关系打包的话，那么纹理资源确实会在内存中存在多份。如果你发现了内存中存在多份相同纹理，且是通过 AssetBundle 文件来加载资源的，则建议将 AssetBundle 上传到UWA网站（www.uwa4d.com）中，其资源检测工具能协助开发团队高效检测并定位AssetBundle中的冗余资源。 四、Instantiate 实例化 Q1：我在第一次执行GameObject.Instantiate一些资源的时候会卡（当时加载当时就实例的情况），有的复杂资源甚至在第一次GameObject.Instantiate的时候会卡70多毫秒，造成明显的卡顿，请问有什么好的解决方案吗？ Instantiate的卡顿与三部分开销相关：相关资源加载、脚本组件的序列化和构造函数的执行，并且绝大部分原因均是相关资源加载导致。所以，我们的建议如下： 1、通过 Profiler 查看 Instantiate 具体的CPU分配情况； 2、如果是资源加载导致的性能瓶颈，则一方面通过简化资源来缓解CPU耗时压力，另一方面通过 AssetBundle 依赖关系打包将资源预先加载，即将此处 Instantiate 的总体耗时拆分，平摊到之前帧进行执行（比如切换场景处等），从而让 Instantiate 实例化操作的局部耗时更加平滑； 3、如果是脚本组件序列化导致的性能瓶颈，则可尝试减少脚本中的序列化信息； 4、如果是构造函数的执行导致的性能瓶颈，一般只能在策略上进行规避，比如降低 Instantiate 的调用频率等。 针对资源加载部分，我们近期正以专题的形式连载中，后续会对其Instantiate实例化的调用进行更为详细的分析，敬请期待。 加载模块之纹理 加载模块之网格 加载模块之Shader 加载模块之动画片段
重磅 | UWA性能诊断又添新利器，开启资源优化新篇章 ！
小编今天又要安利一个性能诊断的新利器啦—资源管理 强力来袭！ 通过该模块，大家可以对AssetBundle、Asset和GameObject的具体操作情况，以及加载、卸载、Instantiate和Active/Deactive等信息一览无遗，详细直观地查看并分析项目中资源的管理情况，从而高效地定位资源漏洞并优化解决。 目前，该新模块的主要功能如下： AssetBundle加载方式 通过该界面，用户可以快速地了解该项目在测试中其AssetBundle的加载方式、每种加载方式中的总体加载次数和具体AssetBundle加载等情况。 如上图我们可以了解CreateFromFile这个API在测试过程中被调用了729次，并且在下图中定位到该API调用了哪些资源、这些资源的文件位置以及加载次数。 同时，用户还可以通过上图右上角“查看所有资源”、“查看指定场景”和“查看指定帧”来进行详细查看，既能宏观把握AssetBundle使用情况，又可以细致掌控每一帧的AssetBundle加载内容。 此外，用户还可以选择每一个AssetBundle来查看它在项目检测时的具体加载点。 资源加载 资源加载主要提供用户查看项目中资源/AssetBundle文件同步/异步加载的具体使用情况，并通过统计数据来判定资源的加载频率和加载是否正确。 特别地，在报告结果中，我们也就对一些平日开发中忽视的细节进行了友情提示。如上图中的不存在，即表示开发团队希望加载的某资源在指定路径下并不存在，需要引起注意。 资源卸载 该页面提供AssetBundle.Unload和Resouces.UnloadAsset的具体使用情况。与资源加载页面一样，用户能迅速查看其资源和AssetBundle通过何种方式卸载以及其卸载频率。 Instantiate和Active（专业会员权限） 该选项显示GameObject在游戏运行时的Instantiate/Destroy和Active/Deactive的使用情况，重点查看其使用频率，从而帮助研发团队检验缓冲池的使用漏洞。 当然，我们也可以查看每个GameObject的具体使用情况。 以上报告数据来自Heroes of Skyrealm，一款拥有顶级颜值的次时代多人ARPG手游，其海外版已经上线，欢迎关注：http://heroesofskyrealm.com/ 。在此也特别感谢开发商厦门梦加的支持。 PS：一不小心又为大家安利了一款好手游，小编感觉萌萌哒~ 点击此处，抢先来一发UWA性能检测，从此开启资源管理新篇章!