Unity自动更新、AssetBundle整理

来源:互联网 发布:获取mac的最高权限 编辑:程序博客网 时间:2024/06/16 00:01

        这篇文章梳理了我现在所理解的Unity的AssetBundle、自动更新的流程和知识点。

一、关于AssetBundle

        如果不是为了自动更新的话,其实完全可以避免掉AssetBundle,全部资源放到Resources目录下或者在Scene中直接引用。这样可以避免掉很多坑。但是考虑到手游的自动更新,AssetBundle就非常有必要了。它相当于一个小的资源包,提供了在游戏完整包之外额外加载资源的能力。但是由于Unity内部资源加载和资源依赖引用的黑科技,Unity所提供的打包和使用AssetBundle的接口都非常蹩脚。关于AssetBundle基础使用和坑,我之前写文章分享过(http://blog.csdn.net/langresser_king/article/details/44208585),这里会对部分知识点进行修正,以及进行拾遗补缺。

        1、关于资源规划。

             如果项目资源非常大的情况下,可以把资源独立一个项目专门用于资源导出。这样程序在开发和调试的时候不会因为项目资源过大造成启动慢、卡顿的情况。程序的资源加载顺序分开发和实际发布版本两种情况,开发模式下应该优先加载Resources目录下的资源,如果没有的话,再加载AssetBundle的资源,这样如果有资源需要修改或者测试的话,直接把对应的资源拷贝到Resources目录下就可以了,不一定需要打包才能测试。而实际发布的版本则相反,优先加载AssetBundle,如果没有的话再加载Resources目录下的资源,这样可以保证自动更新的逻辑。

             之前我有考虑统一打包成AssetBundle然后放到StreamingAssets目录下,这样统一都用AssetBundle的逻辑来处理资源。后来发现其实没有太大必要。只要我们资源规划是合理的,那么无论是配置中,还是逻辑代码中是完全可以兼容Resources.Load和AssetBundle的加载方式的。而且就我现在的理解来看,Resources.Load的加载速度要比AssetBundle要快,因为AssetBundle是压缩的,而Resources.Load是未压缩的(当然这意味者iOS游戏安装后实际占用的文件大小会几倍于安装包,Android的游戏安装并不会解压apk包,所以不受影响)。

              上一段的结论尚未验证,我在Editor下测试了一下AssetBundle和Resource.Load的加载速度,反而是AssetBundle要快。实际结论如何要在手机平台上测试过才能确定。

              在我的规划中,所有带动画的、需要绑定脚本的、需要设置包围盒的模型都要创建prefab,然后给这个prefab设置AssetBundle的名字(即导出这个AssetBundle)。如果存在同一个模型可以替换多个纹理的情况(一种简单的换装),那么所有的纹理都要导出。除此之外的模型可以直接导出。

              总结一下AssetBundle的步骤就是,给需要的模型创建Prefab,设置AssetBundle的导出名字,导出AssetBundle。

         2、如何设置AssetBundle的导出名字

             之前我是直接修改meta文件的,后来发现完全可以在导入资源的时候就设置好。代码如下:

using UnityEngine;using UnityEditor;using System.Collections;using System.IO;using System.Linq;// 设置固定文件夹下面的assetbundle的名字public class AssetBundleImporter : AssetPostprocessor{    static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)    {        if (!EditorConfig.AUTO_SET_ASSETBUNDLE_NAME) {            return;        }        string[] extList = new[] {".png", ".jpg", ".tga"};        foreach (var item in importedAssets) {            if (item.IndexOf(".") == -1) {                // 文件夹                continue;            }            if (item.IndexOf(EditorConfig.MODEL_PATH) != -1) {                // 打包Model文件夹下的模型或者贴图或者prefab                string basePath = item.Substring(item.IndexOf(EditorConfig.MODEL_PATH) + EditorConfig.MODEL_PATH.Length);                basePath = basePath.Substring(0, basePath.IndexOf("/"));                string ext = Path.GetExtension(item);                if (EditorConfig.EXPORT_MODEL_DIRECTLY.Contains(basePath)) {                    if (extList.Contains(ext) || ext == ".fbx") {                        // 此文件夹下只导出贴图和模型                        string relativePath = item.Substring(item.IndexOf(EditorConfig.PATH_TAG) + EditorConfig.PATH_TAG.Length);                        string prefabName = relativePath.Substring(0, relativePath.IndexOf('.')) + EditorConfig.ASSETBUNDLE_EXT;                        SetAssetBundleName(item, prefabName.ToLower());                    } else {                        SetAssetBundleName(item, null);                    }                } else {                    if (extList.Contains(ext) || ext == ".prefab") {                        // 此文件夹下只导出贴图和prefab                        string relativePath = item.Substring(item.IndexOf(EditorConfig.PATH_TAG) + EditorConfig.PATH_TAG.Length);                        string prefabName = relativePath.Substring(0, relativePath.IndexOf('.')) + EditorConfig.ASSETBUNDLE_EXT;                        SetAssetBundleName(item, prefabName.ToLower());                    } else {                        SetAssetBundleName(item, null);                    }                }            }        }    }    static void SetAssetBundleName(string path, string abName)    {        AssetImporter importer = AssetImporter.GetAtPath(path);        if (abName != null) {            if (importer.assetBundleName != abName) {                importer.assetBundleName = abName;            }        } else {            importer.assetBundleName = null;        }    }}

        3、如何导出AssetBundle

             之前我没有指定导出资源包的目标平台,但是在最新测试的时候发现bug--------导出资源的目标平台并不是项目当前选定的平台。现在的处理代码如下:

using UnityEngine;using UnityEditor;using System;using System.IO;public class ExportAssetBundles : Editor{    public static string GetAssetBundlePath(string path)    {        string dataPath = Application.dataPath.Replace("\\", "/");        string outputPath = dataPath.Substring(0, dataPath.IndexOf("/") + 1);        return Path.Combine(outputPath, path);    }    public static void OnCreateAssetBundleAndroid()    {        string path = GetAssetBundlePath("AssetsBundle/android/");        if (!Directory.Exists(path)) {            Directory.CreateDirectory(path);        }        BuildPipeline.BuildAssetBundles(path, BuildAssetBundleOptions.None, BuildTarget.Android);    }    public static void OnCreateAssetBundleIOS()    {        string path = GetAssetBundlePath("AssetsBundle/ios/");        if (!Directory.Exists(path)) {            Directory.CreateDirectory(path);        }        BuildPipeline.BuildAssetBundles(path, BuildAssetBundleOptions.None, BuildTarget.iOS);    }    public static void OnCreateAssetBundleWindows()    {        string path = GetAssetBundlePath("AssetsBundle/windows/");        if (!Directory.Exists(path)) {            Directory.CreateDirectory(path);        }        BuildPipeline.BuildAssetBundles(path, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64);    }}
          这里需要注意,如果导出的资源的目标平台跟项目当前平台不一致的话,Unity会先转换纹理资源到目标平台,然后导出资源包,最后再转换回来。所以如果资源量非常大的话,转换所消耗的时间非常恐怖,所以建议还是每个平台一个文件夹,设置好项目平台,专门导出此平台的资源。

          另外,在Editor模式下,无论当前平台选择的是什么,都只能使用windows平台的AssetBundle,否则即便模型导入正常,shader也会出问题,提示某某属性找不到。

          4、关于如何加载AssetBundle

             加载AssetBundle可以直接使用WWW.LoadFromCacheOrDownload来加载,使用这个函数而不直接使用new WWW,是因为Load函数会有文件映射处理,只加载文件头,而不是所有文件都加载到内存中,这样可以节约内存。

             我写了一个脚本可以自动生成一个文件夹下assetbundle的资源列表,并解析manifest设置文件依赖。我定义的格式是这样的:文件路径=hash码,依赖项1,依赖项2

             hash码可以解析每个assetbundle对应的manifest获取。依赖项可以解析总的manifest来获取。这里稍微注意一下,如果多个资源项目生成assetbundle到同一个目录的话,这个总的manifest文件是会被覆盖的,这里要自行处理一下。

             鉴于我的项目需求,我对AssetBundle的加载处理略微复杂一些(其实还是很简单的,200行代码而已),关键处理了一个细节,如果我想要加载的assetbundle正在加载中,那么不重复加载,等待正在加载的assetbundle加载完毕通知所有的上层回调。如果想要加载的assetbundle已经加载完毕,那么可以直接使用。

             这里额外说明一下,AssetBundle有一个CreateFromFile的接口,可以同步处理AssetBundle的创建,并且速度是最快的。不过它只能加载未压缩的资源包。所以如果确实需要的话,是可以这么做的(但是个人并不推荐),AssetBundle自行压缩,解压,然后使用这个接口来进行加载,缺点是会浪费磁盘空间。

        5、关于shader丢失的问题

             所有的shader都把assetbundle的导出名字设置为"shader",这样模型会依赖这个shader的ab包,当修改shader的时候不需要重新打包所有的模型。客户端在初始化的时候要加载这个shader的ab包,否则所有模型的shader都会发生错误,即便客户端项目中有一模一样的shader,但是Unity也无法将模型与之关联。如果不加载这个默认包的话,那么就要手工指定Material的shader(使用Shader.Find)。暂时我还没有找到理想的处理方式。


二、关于自动更新

          之前做端游的时候,包括后来用cocos2d做项目的时候,自动更新是这么处理的,对比新旧版本的资源差异,提取出来作为更新包,可以直接用版本号命名,如1000.zip,更新服务器存放一个更新包列表的文件。客户端解析这个列表文件,顺序获取到最新版本之间的更新包,并逐一解压。最后把最新的版本号写到客户端。隔一定时间生成一个1000-1009.zip这样的跨版本更新包,这样可以防止多个小版本之间的重复资源。

          现在鉴于Unity的AssetBundle本身就是压缩的了,并且AssetBundle是多个关联的资源整合到一个包里面的,所以资源粒度本身不会太小。所以这里我的更新处理方式是这样的。维护一个版本号,这个版本号仅仅用来判定是否需要更新。维护一个资源列表,这个在上面已经说明过了,它维护了当前所有assetbundle的文件和hash码。客户端先判断版本号是否需要更新。如果安装包内的版本号更新的话,那么就意味者刚刚安装完一个大版本的更新包,此时要清理所有之前下载的assetbundle。如果服务器版本号更新的话,那么就需要进行自动更新。

         客户端下载资源列表文件,与本地的进行比照,获取需要下载更新的assetbundle。即便我们这是一个新的安装包,资源都放到Resources目录下,没有assetbundle,我们也会在发布时打包assetbundle,并生成资源列表,随客户端发布。

         获取到assetbundle的列表后,逐一下载。更新完毕后把新的资源列表数据和版本号写入到客户端。

         这里插一些关于热更新的看法,如果要进行代码级别的更新,使用uLua是最好的选择了,速度很快,使用也简单。但是在我看来还是应该优先考虑性能、开发效率,然后再考虑热更新。即便不使用lua,不更新代码,我们依然可以更新配置、更新资源。一味的想着使用lua,更新代码,可以快速的消除致命bug,其实不是可取的态度。如界面逻辑,活动任务等是比较适合使用lua来写的,这些功能相对独立,并且与性能没有太大关系。


1 0