在开发中,如果都在一直用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对象了,可以通过代码来进行资源加载,其中主体是各个资源在进行加载的时候的存储路径图。用图表示:
二、如何使用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之间的关系:
那么如果该assetbundle AB在后面再次被加载,unity不会重新关联其关系,这样在后续的使用中,就会出现一份材质的多个实例:
所以通常情况下,AssetBundle.Unload(false) 并不能带来较为合理的释放结果,AssetBundle.Unload(true)通常用来确保不会在内存中多次拷贝同一个资源,所以其更多的被项目所采纳,此外还有两个常用的方法用来确保其使用:
1)在游戏中,对于场景的卸载有明确的规划,比如在场景切换中或者场景加载中;
2)管理好对每个单独的object的计数,只有在没有引用的时候才卸载该assetbundle,这样可以规避加载和卸载过程中的多份内存拷贝问题。
如果要使用AssetBundle.Unload(false), 那么这些实例化的对象可以通过2中途径卸载:
1)清除对不需要物体的所有引用,场景和代码中都需要清楚,然后调用Resources.UnloadUnusedAssets;
- 在场景加载的时候采用非增量的方式加载,这会清楚当前场景中的所有Objects,然后反射自动调用Resources.UnloadUnusedAssets