这篇文章主说一下AssetsBundle ,从打包到使用的基础功能
1 资源标记打包
unity 的assetbundle 是需要先设置bundle名称的,选中资源在Inspector目录的左下角可以看到有一个 AssetBundle选项 这里就是bundle的名称,这个选项是可以手动去输入的,但是一个项目的资源数量都手动去输入bundle名称,那有点离大谱,所有就需要写一个工具类去帮我们手动设置bundle名称
我的策略是按照资源目录的文件夹进行分类,同一个文件夹下的资源可打成一个bundle包
在Editor文件夹下创建一个脚本 命名 BundleBuilder.cs ,代码如下
using System.IO;
using System.Linq;
using UnityEditor;
public class BundleBuilder : Editor
{
/// <summary>
/// 支持打包文件的后缀名
/// </summary>
static string[] suffixFilter = new string[]
{
"prefab",
"mat",
"png",
};
[MenuItem("Asset Bundle/Arts-Material/标记为整包")]
static void SetMaterialTogether()
{
string[] path = new string[]
{
"Assets/Arts/Material",
};
SetAssetBundleTogether(path);
}
[MenuItem("Asset Bundle/Arts-Material/标记为散包")]
static void SetMaterialSeparately()
{
string[] path = new string[]
{
"Assets/Arts/Material",
};
SetAssetBundleSeparately(path);
}
[MenuItem("Asset Bundle/Arts-Prefab/标记为整包")]
static void SetPrefabTogether()
{
string[] path = new string[]
{
"Assets/Arts/Prefab",
};
SetAssetBundleTogether(path);
}
[MenuItem("Asset Bundle/Arts-Prefab/标记为散包")]
static void SetPrefabSeparately()
{
string[] path = new string[]
{
"Assets/Arts/Prefab",
};
SetAssetBundleSeparately(path);
}
[MenuItem("Asset Bundle/Arts-Texture/标记为整包")]
static void SetTextureTogether()
{
string[] path = new string[]
{
"Assets/Arts/Texture",
};
SetAssetBundleTogether(path);
}
[MenuItem("Asset Bundle/Arts-Texture/标记为散包")]
static void SetTextureSeparately()
{
string[] path = new string[]
{
"Assets/Arts/Texture",
};
SetAssetBundleSeparately(path);
}
[MenuItem("Asset Bundle/Build Bundle")]
static void BuildAllAssetBundles()
{
string outputPath = ResourcesManager.GetBundleOutputPath();
if (!Directory.Exists(outputPath))
{
// 如果不存在,创建目录
Directory.CreateDirectory(outputPath);
}
BuildPipeline.BuildAssetBundles(outputPath, BuildAssetBundleOptions.ChunkBasedCompression, EditorUserBuildSettings.activeBuildTarget);
}
[MenuItem("Asset Bundle/Clear Bundle")]
static void ClearAllAssetBundles()
{
string outputPath = ResourcesManager.GetBundleOutputPath();
if (Directory.Exists(outputPath))
{
// 如果不存在,创建目录
Directory.Delete(outputPath,true);
}
}
//标记为整包
static void SetAssetBundleTogether(string[] folderPath)
{
string[] guids = AssetDatabase.FindAssets("", folderPath);
foreach (string guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
string suffix = assetPath.Split('.')[1];
if (!suffixFilter.Contains(suffix))
{
break;
}
int lastSeparatorIndex = assetPath.LastIndexOf("/");
if (lastSeparatorIndex != -1)
{
string folderName = assetPath.Substring(0, lastSeparatorIndex);
string bundleName = folderName.Replace("Assets/Arts/", "");
AssetImporter.GetAtPath(assetPath).SetAssetBundleNameAndVariant(bundleName, "");
}
}
AssetDatabase.Refresh();
}
//标记为散包
static void SetAssetBundleSeparately(string[] folderPath)
{
string[] guids = AssetDatabase.FindAssets("", folderPath);
foreach (string guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
string[] sp = assetPath.Split('.');
string suffix = sp[1];
if (!suffixFilter.Contains(suffix))
{
break;
}
string bundleName = sp[0].Replace("Assets/Arts/", "");
AssetImporter.GetAtPath(assetPath).SetAssetBundleNameAndVariant(bundleName, "");
}
AssetDatabase.Refresh();
}
}
代码解析:根据3个文件夹创建了3个按钮,在菜单栏的Asset Bundle下,每个目录可选择标记为整包,或者散包,整包的bundle命名以文件夹的名称命名,散包的bundle命名以资源的路径命名(所有bundle名称都是以Atrs以后的路径命名)
另外两个按钮是打包和清理按钮,打包的bundle目录我设置到工程根目录的 Output 文件夹下,以不同平台的名称区别不同平台的bundle,打包采用 LZ4 压缩方式
标记完bundle之后点击打包,等待打包完成,即可看到Output目录的bundle文件了
这个目录是和工程的Arts目录对应的,不过我把Texture目录选择的整包,所有上图看起来Texture是一个bundle文件,而并非文件夹
.manifest 文件是记录的bundle之间的依赖记录,这个是unity自动帮我们记录的,不需要我们做任何处理,只需要在加载某个bundle的时候,将其依赖项全部加载到内存即可,这个后面会说到
2 资源加载
unity提供了直接加载bundel的方法,当并不会加载bundle的依赖项,所以需要我们手动去把相关依赖给加载到,这里就用到上面说到的 .manifest 文件了,打开文件可以看到所有的bundle依赖项都记录在此文件内,所有我们在初始化的时候先加载到这个文件,使其一直储存在内存中 代码会在下面放出
3资源卸载
众所周知,资源是需要卸载的,unity也提供了我们卸载assetBundle的方法,但由于我们开发中肯定会有公用资源的,例如两个 prefab 共用了同一个材质球,在删除prefab a的时候,是不能卸载这个材质球的,因为prefab还要用,这个就涉及到下面说的资源引用计数了
4引用计数
顾名思义,加载一个bundle,存入内存中,计数+1,如还需要bundle,则直接去内存中获取就可以,不需要再次加载,并计数+1,每次销毁资源的时候,使其计数-1,计数为0的时候卸载bundle即可,
我的策略是使用统一的方法去加载资源,并且每个资源都需要绑定到GameObject上,来记录其销毁时机,当gameObject呗销毁的时候,计数-1
上代码
创建 ResourcesManager.cs 脚本, 并将其设为单例模式
提供两个公用函数 获取目录和平台名称
/// <summary>
/// 获取当前平台名称
/// </summary>
/// <returns></returns>
public static string GetPlatformName()
{
#if UNITY_STANDALONE
return "Windows";
#elif UNITY_ANDROID
return "Android";
#elif UNITY_IOS
return "iOS";
#endif
}
/// <summary>
/// 获取bundle目录
/// </summary>
/// <returns></returns>
public static string GetBundleOutputPath()
{
string assetsPath = Application.dataPath;
string projectPath = assetsPath.Substring(0, assetsPath.Length - "Assets".Length);
string outputPath = Path.Combine(projectPath, $"Output/AssetBundle/{GetPlatformName()}");
return outputPath;
}
字段 : manifest储存、bundle缓存列表
/// <summary>
/// 它保存了各个AssetBundle的依赖信息
/// </summary>
private AssetBundleManifest abManifest;
/// <summary>
/// 缓存加载的AssetBundle,防止多次加载
/// </summary>
private Dictionary<string, LoadedAssetBundle> abCache = new Dictionary<string, LoadedAssetBundle>();
函数:加载 manifest
/// <summary>
/// 加载Manifest文件
/// </summary>
public void LoadManifest()
{
if(abManifest == null)
{
string manifestPath = Path.Combine(GetBundleOutputPath(), GetPlatformName());
AssetBundle ab = AssetBundle.LoadFromFile(manifestPath);
abManifest = ab.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
}
}
函数: 加载AssetsBundle
/// <summary>
/// 加载AssetsBundle
/// </summary>
/// <param name="abName">bundle名称</param>
/// <returns></returns>
private LoadedAssetBundle LoadAssetBundle(string abName)
{
LoadedAssetBundle lab;
if (!abCache.ContainsKey(abName))
{
string abResPath = Path.Combine(GetBundleOutputPath(), abName);
AssetBundle ab = AssetBundle.LoadFromFile(abResPath);
lab = new LoadedAssetBundle(ab);
abCache.Add(abName, lab);
}
else
{
lab = abCache[abName];
}
lab.AddReferenced();
return lab;
}
函数:加载依赖项
/// <summary>
/// 加载依赖项 AssetsBundle
/// </summary>
/// <param name="abName">bundle名称</param>
/// <returns></returns>
private List<string> LoadAssetBundleDepend(string abName)
{
LoadManifest();
List<string> dependNames = new List<string>();
string[] dependences = abManifest.GetAllDependencies(abName);
if (dependences.Length > 0)
{
foreach (var item in dependences)
{
LoadedAssetBundle lab = LoadAssetBundle(item);
dependNames.Add(lab.GetName());
}
}
return dependNames;
}
公开函数:实例化Prefab
/// <summary>
/// 加载prefab 并实例化它
/// </summary>
/// <param name="assetbundleName">AssetBundle名称</param>
/// <param name="assetName">资源名称</param>
/// <returns></returns>
public GameObject InstantiatePrefab(string assetbundleName, string assetName)
{
assetbundleName = assetbundleName.ToLower();
assetName = assetName.ToLower();
LoadedAssetBundle lab = LoadAssetBundle(assetbundleName);
List<string> dependNames = LoadAssetBundleDepend(assetbundleName);
GameObject prefab = lab.GetAsset<GameObject>(assetName);
GameObject go = GameObject.Instantiate(prefab);
AssetWatcher watcher = go.AddComponent<AssetWatcher>();
watcher.Add(lab.GetName());
watcher.Add(dependNames);
return go;
}
公开函数:加载资源
/// <summary>
/// 加载资源
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="assetbundleName">AssetBundle名称</param>
/// <param name="assetName">资源名称</param>
/// <returns></returns>
public T LoadAsset<T>(string assetbundleName, string assetName, GameObject watcherGo) where T : Object
{
if (watcherGo == null)
{
Debug.LogError("加载资源必须绑定GameObject,用于检测卸载资源");
return null;
}
assetbundleName = assetbundleName.ToLower();
assetName = assetName.ToLower();
AssetWatcher watcher = watcherGo.GetComponent<AssetWatcher>();
if (watcher == null)
{
watcher = watcherGo.AddComponent<AssetWatcher>();
}
LoadedAssetBundle lab = LoadAssetBundle(assetbundleName);
List<string> dependNames = LoadAssetBundleDepend(assetbundleName);
watcher.Add(lab.GetName());
watcher.Add(dependNames);
T t = lab.GetAsset<T>(assetName);
return t;
}
创建一个内部类 LoadedAssetBundle.cs 用于储存已经加载过的bundle,避免重复加载,和计数
/// <summary>
/// 已经加载过得Bundle 用于缓存和计数
/// </summary>
public class LoadedAssetBundle
{
AssetBundle assetBundle; //bundle
int referencedCount; //引用次数
Dictionary<string, Object> asset; //asset缓存
public LoadedAssetBundle(AssetBundle ab)
{
asset = new Dictionary<string, Object>();
assetBundle = ab;
referencedCount = 0;
}
public void AddReferenced()
{
referencedCount++;
}
public void RemReferenced()
{
referencedCount--;
}
public bool CheckHaveReferenced()
{
return referencedCount > 0;
}
public int GetyReferenced()
{
return referencedCount;
}
public string GetName()
{
return assetBundle.name;
}
public T GetAsset<T>(string assetName) where T : Object
{
if (asset.ContainsKey(assetName))
{
return (T)asset[assetName];
}
T obj = assetBundle.LoadAsset<T>(assetName);
asset.Add(assetName, obj);
return obj;
}
public void Unload()
{
assetBundle.Unload(true);
referencedCount = 0;
asset.Clear();
}
}
创建 AssetWatcher.cs 脚本, 挂在GameObject上,用于记录此GameObject使用了哪些bundle, 并在销毁的时候尝试卸载这些bundle(计数是否为0)
using System.Collections.Generic;
using UnityEngine;
public class AssetWatcher : MonoBehaviour
{
[SerializeField]
List<string> ReferenceNames = new List<string>();
public void Add(string abName)
{
ReferenceNames.Add(abName);
}
public void Add(List<string> abName)
{
ReferenceNames.AddRange(abName);
}
private void OnDestroy()
{
if(ReferenceNames.Count > 0)
{
foreach(string abName in ReferenceNames)
{
ResourcesManager.Instance.UnloadAssetBundle(abName);
}
}
}
}
InstantiatePrefab方法,加载并实例化prefab,在实例化的prefab上添加 AssetWatcher脚本来记录使用了那些bundle
LoadAsset方法, 加载资源并返回泛型T,需要传入一个GameObject,此GameObject也会添加AssetWatcher脚本来记录使用了那些bundle
5 测试
创建脚本 Test.cs 并挂到场景内任何gameObject上 代码如下
using UnityEngine;
public class Test : MonoBehaviour
{
private async void OnGUI()
{
if (GUILayout.Button("LoadCube"))
{
ResourcesManager.Instance.InstantiatePrefab("prefab/cube", "cube");
}
if (GUILayout.Button("LoadSphere"))
{
ResourcesManager.Instance.InstantiatePrefab("prefab/Sphere", "Sphere");
}
if (GUILayout.Button("LoadCapsule"))
{
GameObject Capsule = ResourcesManager.Instance.InstantiatePrefab("prefab/Capsule", "Capsule");
Material mat = ResourcesManager.Instance.LoadAsset<Material>("material/SphereMaterial2", "SphereMaterial2", Capsule);
Capsule.GetComponent<MeshRenderer>().material = mat;
}
}
}
好了,到此也就结束了,这里我只做了同步加载的bundle,异步加载会在出一篇文章
至于bundle的整包或者散包选择,看各位自己的策略了