Unity5 怎样做资源管理和增量更新
2017-07-20 08:09 tlnshuju 阅读(2153) 评论(0) 编辑 收藏 举报工具
Unity 中的资源来源有三个途径:一个是Unity自己主动打包资源。一个是Resources。一个是AssetBundle。
- Unity自己主动打包资源是指在Unity场景中直接使用到的资源会随着场景被自己主动打包到游戏中。这些资源会在场景载入的时候由unity自己主动载入。这些资源仅仅要放置在Unityproject目录的Assets目录下即可,程序不须要关心他们的打包和载入,这也意味着这些资源都是静态载入的。
但在实际的游戏开发中我们一般都是会动态创建GameObject。资源是动态载入的,因此这样的资源事实上不多。
- Resources资源是指在Unityproject的Assets目录以下能够建一个Resources目录,在这个目录以下放置的全部资源,不论是否被场景用到,都会被打包到游戏中,并且能够通过Resources.Load方法动态载入。
这是平时开发是经常使用的资源载入方式,可是缺点是资源都直接打包到游戏包中了,没法做增量更新。
- AssetBundle资源是指我们能够通过编辑器脚本来将资源打包成多个独立的AssetBundle。这些AssetBundle和游戏包是分离的。能够通过WWW类来载入。AssetBundle的使用非常灵活:能够用来做分包公布。比如大多数页游资源是随着游戏的过程增量下载的,或者有些手游资源过大,渠道要求公布的包限制在100M以内,那仅仅能把一開始玩不到的内容做成增量包。等玩家玩到的时候通过网络下载。
AssetBundle 也能够用来做我们以下讨论的自己主动增量更新。
Unity5相比之前的版本号,AssetsBundle的打包过程有所简化。之前打包须要通过代码来设置须要打入包的资源并自己建立包的依赖关系,Unity5能够通过每一个资源Inspector底部的AssetBundle下拉来指定该资源要打入哪个包,不指定就是不打包。
打包过程仅仅须要BuildPipeline.BuildAssetBundles一句话即可了,Unity5会依据依赖关系自己主动生成全部的包。每一个包还会生成一个manifest文件,这个文件描写叙述了包大小、crc验证、包之间的依赖关系等等,通过这个manifest打包工具在下次打包的时候能够推断哪些包中的资源有改变,仅仅打包资源改变的包。加快了打包速度。manifest仅仅是打包工具自己用的,公布包的时候并不须要。
关于自己主动生成依赖关系这个有必要提下,Unity确实会自己主动给你建立依赖关系,前提是你依赖的资源必须已经在Inspector中设置了BundleName。假设没有,Unity会把这个公用的资源反复打到多个用到的包中。由于这个公用资源不在一个独立的包中,Unity也不会智能地给你发现它是公用的。然后生成一个公用包。更深的坑在于,假设你公用的是一个FBX模型,你仅仅给这个模型设置BundleName还不行,它用到的贴图。材质都要设,否则模型是公用了,贴图没有公用。结果贴图还是被打包到多个包中了。所以设置BundleName这个工作不妨由编辑器脚本来完毕。
方案
Unity提供的就这些了,以下就自己发挥:怎样做一个方便的资源管理方案,既能够开发时方便。又能够方便公布更新包呢?开发过程全用AssetsBundle是不合适的。由于开发中资源经常加入和更新,每次加入或者更新都生成一下AssetsBundle才干执行是非常麻烦的。
并且我们要做的是自己主动更新而不是分包下载。这也就是说在公布游戏的时候这些资源应该都是在游戏包中的,所以他们也不该从AssetsBundle载入。
分析完需求,方案也就出来了:资源还是放在Resources以下,可是这些资源同一时候也会打包到AssetBundle中。
代码中全部载入资源的地方都通过自己的ResourceManager来载入。由ResourceMananger来决定是调用Resources.Load来载入资源还是从AssetsBundle载入。
在开发环境下(Editor)这些资源显然是直接从Resources载入的。公布的完整安装包资源也是从Resources载入,仅仅有当有一个增量版本号时,游戏主程序才会去server把增量的AssetBundle下载下来。然后从AssetBundle载入资源。
实现
实现中我们首先要考虑的是AssetBundle的粒度。即每一个AssetBundle包括多少资源。
增量包的最小粒度就是AsssetBundle。 假设单个AssetBundle过大,仅仅要这个AssetBundle中有一个资源改变了就须要又一次下载整个AssetBundle,浪费流量和玩家的等待时间。假设单个AssetBundle过小,极端情况是每一个资源一个AssetBundle,尽管实现了更新最小化,可是带来了额外开销:AssetBundle本身也是有大小的,并且查找载入AssetBundle也是须要时间的。大家都往U盘里面拷过东西,拷一个1G的文件比拷1千个1M的文件要快非常多。比較合理的做法是依据逻辑来,比如每一个角色能够有独立的AssetBundle。公用的一些UI资源能够打到一个AssetBundle里面。每一个场景独立的UI资源能够打成独立的AssetBundle。
这样做资源预载入的时候也方便,每一个场景须要用到几个Bundle就载入几个Bundle,无关的资源不会被载入。
以下要考虑的是怎样来确定一个资源是从Resources载入还是AssetBundle载入。
为此我们须要一个配置文件resourcesinfo。这个文件随打包过程自己主动生成。里面包括了资源版本号号version。全部包的名字,每一个包的HashCode以及每一个包里面包括的资源的名字。
HashCode直接能够从Unity生成的manifest中得到(AssetBundleManifest.GetAssetBundleHash)。用来检查包的内容是否发生变化。这个resourceinfo每次打包AssetBundle时都会生成一个,公布增量时将它和新的Bundle一起全部拷贝到server上。
同一时候在Resources目录下也存一份,随完整安装包公布,这就保证了新安装游戏的玩家手机上也有一份完整的资源配置文件,记录了这个完整包包括的资源。
当游戏启动时。首先请求server检查版本号号。前端用的版本号号就是Resources以下的这个resourcesinfo中的version。
server比对这个版本号号来告诉前端是否须要更新。假设须要更新。前端就去获取server端的新resourcesinfo。然后比对里面每一个bundle的HashCode,把HashCode不同的bundle记录下来。然后通过WWW类来下载这些发生改变的bundle,当然假设server版的resourcesinfo中包括了本地resourceinfo中所没有的Bundle,这些Bundle就是新增的。也须要下载下来。全部下载完毕后。前端将这个新的resourceinfo保存到本地存储中。后面前端的全部操作都将以这个resourceinfo为准而不再是Resources以下的resourceinfo了。Resources下的resourceinfo能够退出历史舞台了。除非一种情况:本地存储的resourceinfo被觉得删除了。
手机端玩家清理应用的数据就会造成下载的bundle以及resourceinfo被删除。没关系,这时候前端由于找不到外部的resourceinfo了,还会使用Resources以下的resourceinfo和server比对。把新的bundle又一次下载下来。
如今从哪里载入资源就非常明白了:ResourceMananger先读取resourcesinfo。知道了游戏中全部的Bundle和每一个Bundle包括的资源,然后去外部存储查找这些Bundle是否存在,假设存在,就记录下这个Bundle的资源应该从外部的AssetBundle载入,假设不存在,就从内部的Resources载入。在开发过程(Editor)中,由于不存在外部存储的bundle,资源自然都是从Resources载入的,达到了我们开发方便的目的。这个过程隐含了一点:不是全部的资源都须要有BundleName而被打包到AssetBundle中,游戏内不须要兴许更新的资源就不要设置BundleName。它们不会被打包更新,这样的资源ResourceManager在resourceinfo中是找不到的,直接去Resources目录以下读取即可了。
载入AssetBundle,我们直接使用WWW类而不用WWW.LoadFromCacheOrDownload, 由于我们的资源在游戏開始的时候已经下载到外部存储了。不要再Download也不要再Cache。注意WWW类载入是异步的。在游戏中我们须要同步载入资源的地方就要注意把资源预载入好存在ResourceManager中,不然等用的时候载入肯定要写异步代码了。大部分时候我们应该在一个场景初始化时就预载入好全部资源,用的时候直接从ResourceManager的缓存取就能够了。
资源载入卸载
最后简单说下资源的载入卸载。这个网上也有非常多文章介绍。
从我理解来看Resources是一个缺省自己主动打包的特殊AssetBundle。
不管从WWW还是AssetBundle.CreateFromFile创建AssetBundle事实上是创建了一个文件内存镜像。
这时候是没有Asset的。
AssetBundle.LoadAsset 和Resource.Load才真正创建出了Asset。而Instaniate复制了这个Asset。注意这个复制有两种,学C++的都知道浅拷贝和深拷贝,这里的复制有的是正真的复制,有的是引用。为什么要这样呢?由于有些游戏资源是仅仅读的,像贴图Texture,这么大并且仅仅读,当然不须要再去全然复制一份。但像GameObject这样的资源它的属性是能够通过脚本改变的,必须要复制一份。
所以一个资源从AssetBundle到场景中被实例化。事实上有3块内存被创建,这3快内存的释放是有不同方法的。
- 文件内存镜像是通过AssetBundle.Unload(false)来释放的。
- Instaniate出来的Object内存通过Object.Destory来释放。
- AssetBundle.Unload(true)不单会释放文件内存镜像,还会释放AssetBundle.Load创建的Assets。这种方法是不安全的,除非你能保证这些Assets没有Object在引用,否则就出问题了。
- Resources.UnloadAsset和Resources.UnloadUnusedAssets能够用来释放Asset。
以下这个图非常直观:
这是老Unity的图,Unity5已经把AssetBundle.Load 改成了AssetBundle.LoadAsset。
这个修改让我们更明白了Load出来的是Asset这块内存区域。
什么时候把Resource.Load也改了吧。
注意事项(坑)
- Resources.Load方法传入的资源路径需是从Resources目录下一级開始的相对路径且不能包括扩展名。而AssetBundle.LoadAsset方法传入的资源名需是从Assets文件開始的全路径且要包括扩展名。路径不区分大写和小写,建议全用小写。由于AssetBundle.GetAllAssetNames方法返回的资源名都是小写的。
- Unity5打包AssetBundle时会自己主动处理依赖关系。可是在执行时载入的时候却不会,程序须要自己处理,先载入依赖包。
- AssetBundle.CreateFromFile不能载入压缩过的AssetBundle。所以我们仅仅能用WWW来异步载入AssetBundle。
- 眼下我用的Unity5.0.2f1的Resources.Load方法在手机端比原来慢了非常多。假设曾经能够不缓存每次用的时候都调用Resource.Load如今就不行了。
频繁的调用会导致明显的性能开销,不知道是不是Bug。