Unity自动更新、AssetBundle整理

发表于2017-07-14
评论0 2.2k浏览
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来写的,这些功能相对独立,并且与性能没有太大关系。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

标签:

0个评论