Unity AssetBundle打包与资源更新

Unity的AssetBundle打包是一件让人头疼的事情,当我接手这项工做时,我觉得最多只用两个周就能够把整个打包和资源热更新的流程搞定,结果仍是花了一个月,期间踩坑无数,总结出来但愿可以节约别人的时间。html

(一)你的游戏项目是什么类型的?算法

在开始写打包的Editor脚本以前,你最好先详细考察一下大家的游戏项目是什么类型?是端游,手游仍是页游?由于这三者涉及到bundle包的资源管理策略大相径庭,若是大家是跨平台发布,那我建议你最好用宏来切换管理策略。缓存

 

(二)采用什么样的bundle包加载策略?多线程

AssetBundle加载有如下几种方式:异步

     (1)CreateFromMemory/CreateFromMemoryImmediateide

     这种方式直接从内存构建,可同步可异步,能够先经过C#的IO函数从磁盘加载进内存,再用这个API构建AssetBundle内存镜像,占用内存大。不只有构建出来的AssetBundle内存镜像,还有用来构建的bundle包的那部分托管堆内存byte[],要等待垃圾回收。函数

同步构建速度比较快,异步构建的速度很是慢,可是多个bundle包一块儿异步构建在Unity底层有优化,测试要快过一个个的构建。工具

      (2)WWW加载测试

这种方式为异步加载到内存,多个www对象有多线程优化。相比CreateFromMemory少掉了托管堆那部份内存。优化

      (3)WWW.LoadFromCacheOrDownload 

     这种方式占用内存小,是由于Unity会在硬盘上开辟一块空间,用于缓存解压后(时间主要浪费在解压这一步)的AssetBundle,而后再从这块硬盘缓存上构建AssetBundle包,这种方式占用内存较小,由于构建出来的AssetBundle包主要是对磁盘文件的引用,只有在实例化的时候才会分配资源占用的内存。可是磁盘缓存有上限的,超过了上限以后仍然会变成普通的www所有加载到内存。并且你要有个版本号文件管理传入的version参数,不然有可能加载到老的assetbundle。

(4)CreateFromFile

直接从硬盘构建,也是只构建引用,因此速度快且AssetBundle包自己占用内存最小。推荐这种方式,由于同步的代码比较好写,尤为是对于项目后期才引用bundle包机制的,把之前的全部资源加载都改为异步的逻辑工做量太大。

(三)从构建好的assetbundle里load资源

AssetBundle.Load/AssetBundle.LoadAssetAsync

在PC上纹理的上传就发生在这一步。我测试过一个1024*1024的纹理上传所花费的时间每每10倍于512*512的上传时间,因此减少纹理大小才是性价比最高的优化。对于2d mmorpg常用的大图2048*2048,若是你使用同步load一个多帧动画,能够明显的感受到卡一下。若是使用异步则彻底不掉帧,估计Unity是采用sub-image的方式一次锁定一小块区域的纹理上传显卡,可是比较慢,且没有方法调整哪一个参数来加速这个步骤。像大型2d微端这种须要在场景上实时加载不少大图的效果不能使人满意,能够采用切图的方式来优化。

还有unity对象的构建花费的时间也很长,不少游戏过关卡时间太长,主要就是prefab的构建和实例化。能够采用pool manager的方式将实例化出来的对象保存起来,过关卡时只卸载其占用内存较大的纹理音效等资源,下次须要时再加回来。这样能够大大减小过关卡的时间,可是这种方式却会给assetbundle的管理带来一些麻烦,我会在后面bundle包卸载那里提到。

(四)Assetbundle打包

(1)依赖打包

最头痛的就是这一步了,你要考虑怎样处理资源间的依赖,以免产生资源冗余。Unity提供了PushDependencies和PopDependencies来处理依赖包的共享资源问题,例如你有以下依赖关系

(A,B)->C->D

则打包脚本为

push

    build D

    push

           build C

           push

                  build A

                  build B

            pop

     pop

pop

这是一个栈结构,后入栈的资源若是有包含先入栈的资源,则不会重复打包进去,而是依赖于这个包。加载时你要确保先加载被依赖的包,再加载最后的包才不会出错。可是被依赖的包是能够不分前后乱序加载的,若是你使用www加载,能够考虑几个www一块儿加。

还要你要搞清楚pushDependencies/popDependencies打包时设置的依赖关系和加载时的依赖关系其时是两码事,这也是一开始困惑个人地方。好比你有以下的依赖结构

A->(B1 B2 B3 B4)->C

D->(B3,B4,B5,B6)->G

则你的打包脚本应该是这样的

           push

                   build C,build G

                    push

                              build B1,B2,B3,B4,B5,B6

                               push

                                         build A,D

                                 pop

                      pop

                pop

看起来好像A和D都依赖于B1-B6了,其实否则,这样打包出来A包和D包仍是只会依赖于包含相同资源的那些包,好比加载D包的时候你也只须要加载B3-B4    只要你打包的参数设置正确,当B1,B2变更时,走这个流程打包出来的D包二进制仍然没有变化的。

(2) 打包时的参数设置

BuildAssetBundleOptions.DeterministicAssetBundle 

设置了这个参数每次打包出来的包才能确保二进制不变,只要被依赖的包不变,打包的流程不变。因此要作资源更新,这个参数不可少,不然在资源不变化的状况下重复打包出来的MD5都不同,怎么确保更新功能的正常?

BuildAssetBundleOptions.CollectDependencies

这个参数用来收集全部依赖的包,虽然咱们会手动收集依赖关系用于push/pop dependencies,可是仍然须要加上这个参数,由于你不会把一个包依赖的全部资源都收集完,你只会先push几个它依赖的资源,而后再用collectDependencies打最后这个包,确保这个包依赖的全部资源都打进去了。

BuildAssetBundleOptions.CompleteAssets

强制包含整个资源

BuildAssetBundleOptions.UncompressedAssetBundle

采用不压缩的方式打包一个bundle包

咱们打包的时候这四个参数都用了,只有最后一个参数视状况而定。

(3)收集依赖关系

打包前先使用AssetDatabase.CollectDependencies遍历全部资源收集他们间的依赖关系,在后面打包的时候按照每一个资源被依赖的深度进行分级,先打包级别较低的,如shader,script这些资源被其余资源依赖但不会依赖别的资源,级别最低。如prefab依赖前面的全部资源,级别最高,放在最后打包。通常是按照资源的类型(prefab,mesh,animator,texture,script…)进行分级。即便这样按类型分好级后还是不够的,由于同一级的资源也有可能产生相互依赖的关系。好比使用NGUI,一个面板prefab依赖于几个挂UIAtlas的prefab,这种同级的依赖须要用深度优先遍历对他们进行排序以肯定依赖关系。这个依赖关系使用序列化文件记录下来,供后面加载包的时候先加载全部被依赖的包使用。每次更新的时候这个依赖关系的序列化文件也要同其余资源一块儿更新。

(4)打包时可能遇到的一些问题

若是你使用www.LoadFromCacheOrDownload 请在调试的时候游戏开始时调用一次ClearCache。即便你的代码有动态更新LoadCache时传入的version参数的机制,调试的时候仍是要谨慎,若是BUG致使你传的version跟上次同样,相互依赖的包缓存的版本不匹配,就可能引发一些稀奇古怪的问题。

检查你的打包流程所记录的依赖结构是否稳定。这里的稳定是指,在CollectDependencies的时候有没有处理到的被依赖的资源,有可能在打包同级资源的时候出现相互吃资源的状况。好比

A->(B c)->D                     

E->(F c)->G

打包脚本

push

     build D,G

           push

                    build B,F

                    push

                             build A,E

                     pop

      

在收集依赖关系的时候,c是咱们忽视的资源,打包时B和F放在同一级打包,A和E在同一级,因为使用了CollectDependencies,A包会把c给收进去,可是因为B包在同一级跟A一块儿打的,就会出现c打进A了就再也不打进B了,但你加载B的时候又没有加载A,因此B就工做不正常。

排查这个BUG的方式就是先打一两个角色或面板,备份,再打所有资源。把两份资源用二进制工具作比较(推荐BeyondCompare,能够对比目录),若是有不稳定的结构立马就能发现。

还有texture的宽高请使用2的倍数,我在测试不标准的图的时候发现Unity对于这种图会产生一个fmt-512*512(sprite)的临时资源,这个资源get他的硬盘地址时get不到,因此也没有记录进依赖关系文件。当有两张图不规范时,一张图的bundle包收录了临时资源另外一张图就没有,加载出来就会不正常。固然通常游戏项目为了优化使用的图都比较规范,不会遇到这个问题。

  在IOS真机调试时报Could not produce class with ID..这是由于你勾选了strip code,有些脚本类是被Resource下的资源引用的,打包后将Resource下的资源移除出去了,一些代码因为检测不到引用就被strip掉了,可是从AssetBundle里加载出来又须要根据ID打到对应代码。解决办法在这里http://docs.unity3d.com/Manual/ClassIDReference.html找到ID对应的class,而后在Assets目录下新建文件link.xml,把不应strip掉的类加进去就好了。个人link.xml文件

<?xml version="1.0" encoding="utf-8"?>
<linker>
    <assembly fullname="System">
        <type fullname="System.Net.HttpRequestCreator" preserve="all"/>        
    </assembly>

    <assembly fullname="UnityEngine">
        <type fullname="UnityEngine.CircleCollider2D" preserve="all"/>
    </assembly>
</linker>

有些类好比 AnimatorController(ID 91)属于Editor包里的,不能用link.xm加回来,能够在Resource下建一个空的prefab,在上面挂一个AnimatorController,打包时留下这个prefab就能够确保这个类不被strip掉了。

(五)更新机制

更新机制比较简单,收集全部bundle包的md5码和文件size,作成一个列表。进游戏时先比对游戏版本号提示更新游戏程序,再比对资源版本号,若是发现新版本号就开始下载md5列表,与本地的md5列表作对比,找出须要更新的资源用http下载就好了。

这个过程仍是有许多东西要考虑,好比你的http下载要有下载失败重试几回的机制,要有超时的检测,要知道在下载哪几个资源时整个更新流程卡住了,记录日志。即便遇到更新过程当中出错,对于已经更新的资源下次进不用再重复更新,因此最好每更新10条资源就写回一次md5,而不是全更完再写回。

md5列表的比较,之前有的PC游戏会在远端先作好与上一个版本的对比,而后生成一个ver x 到ver x+1 的资源更新文件。在更新的时候若是游戏的资源版本号是ver x-1 就先下载 ver x的资源更新文件更新,再从ver x更新到ver x+1。可是这一套用在Unity手机资源更新上有风险,假设有些手机的清理软件提示这个程序的资源占用过大,一不当心点了致使清掉了部分资源,但你的ver x文件还在,那你更新时被清掉的这部分资源就找不回来了。因此仍是每次在客户端对比全部md5比较稳妥,在提取本地的md5列表中的一项时同时检测本地是否存在这个资源文件,不存在的加入更新列表,这样即便被意外清掉的资源也能够找回。

(六)压缩bundle包

由于咱们使用的是CreateFomeFile的同步机制加载包,而CreateFromFile只能用BuildAssetBundleOptions.UncompressedAssetBundle,打包出来后本身压缩再在更新时解压。因此采用什么压缩算法就是一个值得商榷的问题。

压缩你要考虑两个方面:压缩率与解压时间。

(待续)

(十)AssetBundle包卸载

相关文章
相关标签/搜索