Resources与AssetBundle的无缝切换加载

在开发中,如果都在一直用Resources.Load()方法,那么项目到后期的时候想切换成AssetBundle时就很是麻烦了,所以我们要有一套Resources与AssetBundle的无缝切换加载方案。思路大致就是,将两种的加载方式进行融合,封装。自定义Resources与AssetBundle的读取方法,构架AssetBundle的时候需要记录资源和Bundle之间的引用关系,通Resources加载的目录取到对应的AssetBundle的加载目录进行无缝切换。

1.构建资源生成资源的描述文件BulidAB.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;using UnityEditor;
using System.IO;
public class BulidAB  { 
   [MenuItem("Tools/BulidAssetsBundle")]   
 static void BulidAssetsBundle() {  
      //合并路径        string outPath = Path.Combine(Application.dataPath, "StreamingAssets");
        //如果目录存在则删除       
 if (Directory.Exists(outPath)) {
            Directory.Delete(outPath,true); 
       }       
   Directory.CreateDirectory(outPath);  
   List<AssetBundleBuild> builds = new List<AssetBundleBuild>();  
      //设置Bundle的名称,确定多少资源打在统一个Bundle中 
       builds.Add(new AssetBundleBuild() { assetBundleName = "Menu.unity3d",addressableNames = new string[]{ "Assets/Prefabs/FX_Decay.prefab", "Assets/Prefabs/Sherman_Dead.prefab" } }); 
       //生成AssetBundle      
BuildPipeline.BuildAssetBundles(outPath, builds.ToArray(),     BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.DeterministicAssetBundle, BuildTarget.StandaloneOSX);    
    //生成描述文件   
     BundleList bundleList = ScriptableObject.CreateInstance<BundleList>();  
      foreach (var item in builds) 
       {              
 foreach (var res in item.addressableNames)   
         {         
       bundleList.bundleDatas.Add(new BundleData() { resPath = res, bundlePath = item.assetBundleName });  
          }        }    
    AssetDatabase.CreateAsset(bundleList, "Assets/Resources/bunleList.asset");   
     //刷新文件表     
   AssetDatabase.Refresh();
    }}

2.构架的时候序列化资源路径和AssetBundle的加载路径BundleData.cs,BundleList.cs

[System.Serializable]public class BundleData{
    //保存每一个res对应的路径
    public string resPath = string.Empty;
    public string bundlePath = string.Empty;
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class BundleList : ScriptableObject{
    public List<BundleData> bundleDatas = new List<BundleData>();
}

3.提供一个Assets.cs类进行代替Rescoures类这样就不用再使用Rescoures加载了,而是使用Assets.Load<T>()加载

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class Assets {
    static Dictionary<string, string> m_ResAbDic = new Dictionary<string, string>();
    static Dictionary<string, AssetBundle> m_BundleCache = new Dictionary<string, AssetBundle>();
    static Assets() {
        BundleList list = Resources.Load<BundleList>("bundleList"); 
       foreach (var bundleData in list.bundleDatas)
        { 
           m_ResAbDic[bundleData.resPath] = bundleData.bundlePath;
        }
    }    
static public T LoadAsset<T>(string path) where T : Object { 
       //从AssetBundle 中加载资源,最好提供后缀名,不然无法区分同名文件
        string bundlePath; 
       string resPath = Path.Combine("Assets/Resources", path);
        if (typeof(T) == typeof(Object)) { 
           resPath = Path.ChangeExtension(resPath, "prefab"); 
       }       
 //如果Bundle有这个资源,则从Bundle中加载
        if (m_ResAbDic.TryGetValue(resPath, out bundlePath)) {
            AssetBundle assetbundle;
            if (!m_BundleCache.TryGetValue(bundlePath, out assetbundle)) {
                assetbundle = m_BundleCache[bundlePath] = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, bundlePath)); 
           }            
return assetbundle.LoadAsset<T>(resPath);
        }        
//如果Bundle中没有这个资源则从Resources目录中加载
        return Resources.Load<T>(path); 
   }}

4.使用方法

using System.Collections;using System.Collections.Generic;
using UnityEngine;
public class test : MonoBehaviour {
    // Use this for initialization
    void Start () {
        GameObject.Instantiate<GameObject>(Assets.LoadAsset<GameObject>("FX_Decay"));            
       GameObject.Instantiate<GameObject>(Assets.LoadAsset<GameObject>("Sherman_Dead"));
    }}

补充:

一、什么是AssetBundle

AssetBundle就像一个ZIP压缩文件,里面存储着不同平台的特殊资源(models/texture/prefabs/materials/audio clip/scenes...), 这些资源都可以在运行时进行加载。具体的assetBundle中主要包含什么?主要包含两种互相关联的东西:

1. 磁盘上的文件:也就是assetbundle文档,可以将其视为一个容器或者文件夹,其中包含两类文件:序列化文件和资源文件,序列化文件就是资源在打包后对应的各个平台的序列化操作后的文件,资源文件主要是针对textures/audio等较大的文件打包的二进制文件,这类文件在加载的时候是在其他线程执行的(效率更高)。

2. 就是实际的assetbundle对象了,可以通过代码来进行资源加载,其中主体是各个资源在进行加载的时候的存储路径图。用图表示:

image
image.gif

二、如何使用AssetBundle

在说完bundle的分类,打包后,接下来就是如何在实际的游戏中加载使用这些bundle。在unity5以后,提供了4种不同类型的加载接口,下面逐一分析一下这四种不同接口的使用:

1、AssetBundle.LoadFromMemoryAsync(byte[] binary, unit crc = 0)

这个方法用来加载ab数据的bytes数组,如果数据是使用LZMA的压缩格式,那么在加载的时候会进行解压的操作,LZ4格式的数据则会保持其压缩的状态,使用示

using UnityEngine;
using System.Collections;using System.IO;
public class Example : MonoBehaviour{
    IEnumerator LoadFromMemoryAsync(string path)    {
        AssetBundleCreateRequest createRequest = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path));
        yield return createRequest;
        AssetBundle bundle = createRequest.assetBundle;
        var prefab = bundle.LoadAsset<GameObject>("MyObject");
        Instantiate(prefab);
    }}

当然,对于bytes数组,也可以使用File.ReadAllBytes(path)的方式来加载数组。

2、 AssetBundle.LoadFromFile

在加载非压缩文件或者LZ4压缩类型文件的时候,该接口效率极高,对于LZMA压缩格式的文件,也会在加载的时候执行解压的操作,使用示例:

public class LoadFromFileExample extends MonoBehaviour {function Start() {
        var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetBundle")); 
       if (myLoadedAssetBundle == null) {
            Debug.Log("Failed to load AssetBundle!");
            return;        }
        var prefab = myLoadedAssetBundle.LoadAsset.<GameObject>("MyObject");
        Instantiate(prefab);    }}

ps: 在unity5.3及更早的版本中,在安卓平台上如果从streaming assets路径中加载文件会失败(路径文件夹中会额外包含.jar文件)。

3、WWW.LoadFromCacheOrDownload

这个接口会被淘汰(被UnityWebRequest替换),那么就不过多的讲解这个接口(注意这个接口会进行存储分配的操作以容纳资源,如果分配不足以存储会使得加载失败)。

4、UnityWebRequest

这个接口,会有两步操作,首先是创建一个web request(调用UnityWebRequest.GetAssetBundle), 然后进行资源的获取(调用DownloadHandlerAssetBundle.GetContent),unity提供的使用示例为:

IEnumerator InstantiateObject()    {
        string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;                UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0); 
       yield return request.Send();
        AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request); 
       GameObject cube = bundle.LoadAsset<GameObject>("Cube"); 
       GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
        Instantiate(cube);
        Instantiate(sprite);    }

使用这种方式,可以使得开发者更为灵活的操作下载数据,同时进行内存使用分配,会逐渐的被用来WWW接口。

在加载完assetBundle后,接下来,就是如何从bundle中获取资源(asset),其基本的接口模板为:

T objectFromBundle = bundleObject.LoadAsset<T>(assetName);

如果想获取所有的assets则可以使用接口:

Unity.Object[] objectArray = loadedAssetBundle.LoadAllAssets();

一旦获取到asset,那么就可以在游戏中使用这些资源了(一般是实例化创建操作)。

5、加载AssetBundle Manifest

除了加载assetbundle,一般还会加载其对应的manifest(与其存储在同一个文件夹下的相同名字的manifest),一般加载manifest的操作示例:

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");

在前文也提及到,如果一个assetbundle依赖于另一个assetbundle,那么需要提前加载依赖相关的bundle,那么依据manifest,可以加载其依赖的assetbundle:

AssetBundle assetBundle = AssetBundle.LoadFromFile(manifestFilePath);
AssetBundleManifest manifest = assetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
string[] dependencies = manifest.GetAllDependencies("assetBundle"); 
//Pass the name of the bundle you want the dependencies for.
foreach(string dependency in dependencies){    AssetBundle.LoadFromFile(Path.Combine(assetBundlePath, dependency));}

现在,已经加载了assetbundle, 也获取了assetbundle的dependencies,以及其中assets,这样就可以管理这些assetbundle了。

五、管理AssetBundle

unity在场景中的Object被移除的时候不自动释放objects,资源的清理需要再特定的时间触发(场景切换)或者手动的管理。所以怎么加载和卸载资源显得尤为重要,不合适的加载可能会导致资源的重复加载,不合适的卸载可能会带来资源的缺失(比如丢失贴图)。

对于assetbundle的资源管理,最重要的是掌握什么时候调用AssetBundle.Unload(bool)这个函数,传入true/false会有不同的卸载策略。这个API会卸载对应的assetbundle的头部信息,参数对应着是否同时卸载从该assetbundle中实例化的所有Objects。

AssetBundle.Unload(true)会卸载assetbundle中的所有gameobjects以及其依赖关系,但是并不包括基于其Objects实例化(复制)的Object(因为这些object不属于该assetbundle,只是引用),所以当卸载贴图相关的assetbundle的时候,场景中对其引用的实例化物体上会出现贴图丢失,也就是场景中会出现红色的区域,unity都会将其处理成贴图丢失。

举例说明,假设材质M来自于assetbundle AB, 如果 AB.Unload(true), 那么场景中任何M的实例都会被卸载和销毁,如果AB.Unload(false), 那么就会切断材质M实例与AB之间的关系:

image
image.gif

那么如果该assetbundle AB在后面再次被加载,unity不会重新关联其关系,这样在后续的使用中,就会出现一份材质的多个实例:

image
image.gif

所以通常情况下,AssetBundle.Unload(false) 并不能带来较为合理的释放结果,AssetBundle.Unload(true)通常用来确保不会在内存中多次拷贝同一个资源,所以其更多的被项目所采纳,此外还有两个常用的方法用来确保其使用:

1)在游戏中,对于场景的卸载有明确的规划,比如在场景切换中或者场景加载中;

2)管理好对每个单独的object的计数,只有在没有引用的时候才卸载该assetbundle,这样可以规避加载和卸载过程中的多份内存拷贝问题。

如果要使用AssetBundle.Unload(false), 那么这些实例化的对象可以通过2中途径卸载:

1)清除对不需要物体的所有引用,场景和代码中都需要清楚,然后调用Resources.UnloadUnusedAssets;

  1. 在场景加载的时候采用非增量的方式加载,这会清楚当前场景中的所有Objects,然后反射自动调用Resources.UnloadUnusedAssets

个人学习博客谢谢关注

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351