必读!ILRuntime来实现热更新的优与劣!

必读!ILRuntime来实现热更新的优与劣!

本期聚集话题:使用ILRuntime实现热更新的优劣分析、手游里昼夜交替的实现方法、AssetBundle Diff Patch 方案的可行性、控制fbx导出模型时顶点数量...


这是第103篇UWA技术知识分享的推送。今天我们继续为大家精选了若干和开发、优化相关的问题,建议阅读时间15分钟,认真读完必有收获。

UWA 问答社区:answer.uwa4d.com
UWA QQ群:793972859(仅限技术交流)


热更新

Q1:使用ILRuntime来实现热更新的可行度有多高,大家有没有使用经验分享?

一般热更新分两块,代码+资源,资源热更几乎都是通过AssetBundle来搞,代码热更可以用某种解释器+解释执行的语言来搞,可供选择的有Lua、as3、python、js、C#等,据我所知,主流是Lua,次主流是C#。

热更新的选择上,我们项目最初所有代码都是C#写,不考虑热更新。因为团队没人会Unity3D,全是边学边做,”做出来”是第一要务。上线后自然顶不住运营的压力:在中国很多Android渠道的情况下,运营的这个需求是合理的,否则每次版本更新,各个渠道审核和上线的时间同步是非常困难的。

然后我们首先用XLua打补丁修Bug:成本最低,后来运营要求能通过热更加功能,而不仅仅是改Bug,那么要么用Lua写所有可能会被热更新的代码,这需要把现有的大量C#代码翻译过来;要么想办法让C#能热更新。于是很自然的就选择了后者+ILRuntime.

最后的技术方案是这样:

1. 基本限制
热更部分的代码都不继承MonoBehaviour,也就是都不挂脚本,非热更部分随意:热更对MonoBehaviour这种比较特殊东西的支持都挺麻烦,要么不用,要么只是做个不可热更的消息转发层;要么开发时挂脚本,打包时用某种特殊的方式把它变成代码里动态AddComponent。

2. Android
不用任何第三方的热更方案,用C#反射执行DLL,性能和代码写法和纯C#基本一样。
Google Pay强制要求在2019年8月之前App都支持64位,Unity的应对方案是Android IL2cpp,暂时没有支持mono backend 64位的打算。所以到时候只能是IL2CPP + ILRuntime的方式,性能会差一大截,主要慢在ILRuntime上。

3. iOS
ILRuntime + DLL 解释执行,当然是在IL2CPP下。

4. 优点
语言(C#)\开发环境\工具链统一,随时可以变成不支持热更形式,如果苹果未来不允许任何解释执行的方式。

框架搭好后,满足一些限制条件(非硬性限制,主要是避免麻烦,限制主要是1个,可热更部分的代码不要继承不可热更的代码, 不继承MonoBehaviour是这个限制的子集),写逻辑的同学开发方式和原生C#开发完全一样,包括调试。

第三方插件直接可用(大部分插件都是基于C#写的).

5. 缺点
稳定性的坑还是有一些:通常发生于一些相对高级的语言特性组合,特别是各种反射代码。另外.net4.6的async\wait所支持的现在版本应该也还不够稳定,纯计算的性能弱于Lua,计算密集型的代码还是想办法放在不可热更新的部分吧。

历史短,Git贡献者少,项目考验少(据我的了解,上线的商业项目在x - 1x之间,具体的项目有MMO,SLG,休闲,也有棋牌),原理上大的优化空间没有,小的优化空间还是有一些;另外整合了各种常用Feature的框架也少 。

感谢UWA问答社区 @GX提供了回答,欢迎大家转至社区进行进一步交流:

https://answer.uwa4d.com/question/5a9fc420d35eb22c10a0a365


制作

Q2:我们知道Unity自建的Cube是24个顶点,而不是8个。我们在Max中自建一个Cube转换成Polygon,不加任何的UV处理,我们希望导出的顶点数量可控(可为8),但我们发现默认导出的FBX顶点数量一定是24个。为何? 如何精确控制其数量?

UWA:这个问题主要是看模型中“相邻面的顶点是否可以共用法线”。如果可以,那么只需要一个顶点来表示;如果不可以,则需要分为两个顶点(位置相同,法线不同)。而“是否可以共用法线”,在Max里可以通过光滑组来控制,而在Unity里可以通过Smoothing Angle来控制(类似于相邻面夹角小于多少的时候可以共用法线)。

所以,题主可以尝试在Max里添加光滑组并导出;也可以在导入Unity后,把Normal一项设为Calculate,通过Smoothing Angle来控制。为了更好地说明是否合并法线对光照效果的影响,左图是Smoothing Angle为0(基本不合并法线,顶点数最多,为7676),右图为1(最大程度合并法线,顶点数最少,为3260)。
请输入图片描述

该回答由UWA提供,欢迎大家转至社区进行进一步交流:
https://answer.uwa4d.com/question/5aa786ad0b827e2c0bfdd13b


制作

Q3:请问在手游中如何实现昼夜交替的方法呢?我考虑了动态加载Lightmap,但是渐变的过渡,会显得比较突兀。如果是实时光的话,性能压力又比较大。请问移动端昼夜交替的实现方法,大家有什么思路呢?万分感谢!

美术烘焙两套或者更多的Lightmap,然后游戏运行时通过RenderToTexture的方式来动态blend两张Lightmap,然后把结果拿去使用,混合过度完成后,切换正式的Lightmap,过渡Lightmap就可以丢了。

可能需要注意下Lightmap的编码格式需要在混合前解码一下,混合好了之后编码回去。总之,移动平台下压缩格式混合后有一些误差,不过能接受。

感谢UWA问答社区 @Lujian提供了回答

有一种山寨的做法,就是美术要烘出白天和黑夜的两套(如果有多种自然环境的可以烘多套)。

1)动态加载下一自然环境所要用到光照贴图当成全局贴图;

2)用时间做一个系数,光照贴图解码时,对当前的光照贴图和第1部加载的光照贴图进行插值就好了。(白天向黑夜转换),解码的地方在UnityGlobalIllumination这个函数里;

3)用时间做一个系数,适当再去调一下环境光。

这样做的前提是,你们的场景的光照贴图张数比较少,否则会比较尴尬。

感谢UWA问答社区 @kent提供了回答

我们项目也做了昼夜系统,不是我做的,邀请了对应的同事来回答,不知道有没有时间,我先说两句思路上的东西。

我们没有采用两套光照贴图的思路,利弊比较明显:好处就是多套光照贴图可以做较好的效果,点光源啊,GI啊,AO啊都可以有,坏处就是包体占用会大些,过度渐变有些麻烦。如果是ARPG那样的切换场景的比较合适,大世界的玩家如果一直在一个场景里的可能稍微麻烦些。

我们最初设计的场景里大都是野外场景,点光源之类的变动需求很小,因此昼夜系统的思路没有选择多套Lightmap的方案,而是使用修改材质参数的方案来做。

1)第一个版本用了lut,渐变就用两张lut贴图的过渡差值来做,消耗基本可以,但是因为是全屏的后处理,也没有HDR,特效的部分会导致也被压暗了,美术试用一段时间后不满意;

2)第二个版本基于材质参数的思路来做,美术烘焙的是白天的效果,晚上在PS里得出的结果里乘以一个调色值,场景和角色的Shader都要处理,特效的材质不处理,就可以实现整体变暗的效果,基本效果不算出彩,但是可以满足基本的移动设备上的需求,性能也可以接受。

3)天空盒、方向光亮度、环境光、角色反射Cube跟随昼夜做切换,主要对角色产生影响。

4)夜晚的时候,建筑窗户、灯笼自发光的部分调整强度,可以表现出更好的夜晚百家灯火的效果。

思路和切换光照贴图的思路不同,供参考,运行时消耗比切换光照贴图的思路要大一点,好处也就是节省美术的工足量,节省一些包体,过度也好做一些,方便做黄昏、早晨这样的效果,但是效果不是最好的,手游上基本够用。可能还有很多细节我不了解,回头看同事有时间再来补充吧。

感谢UWA问答社区 @贾伟昊提供了回答

欢迎大家转至社区进行进一步交流:
https://answer.uwa4d.com/question/5aa9d6f4d35eb22c10a0a3cf


资源管理

Q4:请教AssetBundle Diff Patch方案是否可行吗?我原先的更新方案是比对AssetBundle文件的hash值,直接替换整个AssetBundle和manifest来达到更新效果的,不知道有没有其它方案类似Diff patch,可以减小更新包体,如果可以做Diff patch,资源颗粒度是不是就可以忽略了。

基于AssetBundle的直接Diff更新目前没有看到成熟的方案,不过只把完整的Resources库中的部分资源挑出来更新的项目,已经是做了2个了。思路其实很简单:

1)出整包的时候,用ScriptableObject记录下AssetsDataBase中所有被发布资源的MD5码(不能用Unity自己的hash码);

2)用工具调出哪些assets是代码中动态Load的,分入一个Resource包中;找出这些资源的依赖资源,分入一个Share包中,并记录每个Asset所属的AssetBundle包;

3)出补丁时,对比当前AssetsDataBase中哪些代码中动态Load的资源出现了增加和改动(删除可以无视),包括他们依赖的资源。仍然把代码中动态Load的资源放入ResPatch中,依赖放入SharePatch中。对于没有变化的资源,仍然维持原来的AssetBundle名字;

4)记录下此次补丁之后,更新每个Asset所属的AssetBundle包;

5)下一次补丁时的Resource和Share可能会依赖上一次的补丁的Share;

6)运行时,对所有补丁入的Res建立一个字典索引。Load的时候,优先判断这个Asset是否在补丁的Res包中,如果是,则读补丁的AssetBundle包,并按套路处理依赖包。否则就用二进制版本内建的包。

这种做法的好处是:
1)分利用版本中的已经发布的资源,以减小补丁包的体积;

2)补丁包的打包规则可以几乎无视完整包的分包规则,方便后续折腾。

当然缺点也是很明显的:
1)完整包里的大包真的好吗?如果所有资源打成一个AssetBundle,就和全部用Resources目录没有区别的,这个是Unity官方竭力阻止的。实测下来,一个300MB的Res包Open一下,iOS分配了接近40MB的内存…(其实这个问题和Diff更新不矛盾,现在我们二进制包里的资源也拆得比较小了,但补丁仍然可以使用上述规则)。

2)工具啊工具,开发维护资源MD5数据库、补丁生成规则打包器,肝了5天5夜才搞对啊;

3)也是最致命的问题,对于被非常多资源依赖的Assets,一旦变化,更新包里就会找出一堆躺枪的Resources,例如UIAtlas相关的东西、Shader全家、字体什么的…

对于问题3,几乎必须要用整包的替换的思路,把这些易变的Assets挑出来,单独打AB包。后续什么Res什么Shared全都依赖它们。如果它们中有一个变了,就覆盖更新掉这整个基础AssetBundle包,而依赖它们的Res不视为发生了变化。

其实差异更新和整包替换到底哪个更好,我也一直在纠结。当初用差异更新的最主要原因,其实是项目里的代码异步化处理得不好,频繁Load小的AssetBundle包很卡,所以才打大包,被迫上Diff方案的。

另外,无论哪种打包方案,千万要控制好AssetBundle包中的冗余资源量【不管大包还是Diff包,都可以解决好的】,我的底线是2%实际容量以内。这一点上UWA的在线AssetBundle包检测工具能帮大忙,谢谢!

感谢UWA问答社区 @郑昊提供了回答

我是来劝退的。整包Diff的,Flashyiyi写过一个基于bsdiff的方案
https://zhuanlan.zhihu.com/p/31810166

bsdiff我在Cocos2d年代用来做CPP的更新,缩小so的体积,确实十分有效,但是为什么我还要劝退呢?

1. 维护难度大
Diff是精准产生两个文件之间的差异,也就要求每一次补丁都要精准匹配上一版本和下一版本,并且在多版本时是一个严格线性的关系:必须逐个补丁按顺序安装合并。而这个严格线性关系是致命的缺点。

版本很多时,这个假如中间有一个版本丢失,或者中间一个补丁是有问题的,整个更新链就出问题。可靠的系统还是需要考虑出错的情况。

2. 合并补丁需要谨慎
替换文件的套路看起来补丁会大,但却天然做到了多个版本的补丁的重复改动是自动合并的,所以老玩家或者装apk更新到最新版本的时候下载总量不见得会很夸张。

但Diff方案就很麻烦,虽然可以进行Diff合并,但首先是你合并了也不能轻易删除合并前的补丁,否则更新到中间版本的玩家就会没有补丁可用。你得做很严谨的判断来跳过合并的补丁。也需要有靠谱的制作流程来防止合并补丁搞出问题。

3. 打一个大AssetBundle会导致合并缓慢
这是因为你要一个个地按顺序合并。相对地,云风写过一个解包Bundle,然后运行时再组装的方案(Blog,非源码)。祖龙的六龙争霸也是这个方案。并且他们也不是打一个大Bundle,而是小Bundle内精准替换。这个没有那么依赖版本的精准一致性,也能做到更新包非常小。

但又带来新的问题:官方格式一改,代码也要跟着改。而官方格式不开源,而是自行破解的(可以参考Unity Extractor之类的开源方案),你跟随的难度也就可见一斑。

所以,如果真的不是十分在意更新大小的,其实替换文件还是不错的……反正我们是在用。现在大厂的游戏,动辄也是几十MB补丁。

感谢UWA问答社区 @招文勇提供了回答

欢迎大家转至社区进行进一步交流:
https://answer.uwa4d.com/question/5aa5f267d35eb22c10a0a38e


其他

Q5:我们团队最近上线的产品用的是Unity 4.6版本,然后最新在筹划新项目,还是一款MMO产品。想请教一下,升级到Unity 5的必要性有多大呢。因为Demo切到5之后,出了一些解决起来比较耗时的问题,我们担心后续的坑比较多。我想大概所了解的Unity 5主要的新特性有:AssetBundle打包的优化、粒子从主线程中分离出去了、PBR渲染方式等,但是这几个东西暂时看对我们的影响不是太大。

UWA:如果是新项目,那么升级Unity 5很有必要。功能新特性上还是建议直接去看Unity 5的功能介绍吧,网上很多了。说说性能上提升的几个重要方面:

1)加载效率更高。即便都是同样的LZMA压缩打包,Unity 5.3之前版本关于同样内容的AssetBundle加载也一样提速(5%~15%)。另外,对于Unity 5.3之后的新LZ4 AB,加载速度较之之前是飞一般的提升;当然Unity 2017加载ms更快,我们将会在UWA Day 2018上进行总结和分享;

2)AssetBundle加载内存占用更小。Unity 5.3之后没有了Webstream,一大幸事。但需要注意5.4.5、5.5.4版本之前的0.5MB SerializedFile的“坑”;

3)除题主提到的粒子系统外,动画模块也同样多线程处理进行处理,效率提升明显(虽然有时候会出现主线程等待子线程的麻烦问题,但总体来说还是利大于弊);

4)UGUI在Unity 5.2版本以后趋于完善,且加入了多线程处理。在我们的趋势分析中,UGUI无论是性能还是使用成熟度方面赶超NGUI的趋势非常明显;

5)物理在Unity 5.4版本以后可根据项目的物理使用情况自行进行剥离,从而避免之前一直在不停空转的尴尬问题。

除上述之外,性能提升点还有很多,可以说Unity 5的性能提升还是多方面的,建议升级。最后,Unity 4已经不再维护了,如果以后万一出现了设备或系统的兼容性问题,咋办?

该回答由UWA提供,欢迎大家转至社区进行进一步交流:
https://answer.uwa4d.com/question/5aa686680b827e2c0bfdd117