如果你不能用最简单的语言来描述,那你就是没有真正领悟。——爱因斯坦
一、依赖关系概述
我们首先通过三张图来了解一下什么是资源依赖。
图一:
在这张图中,模型A使用了一个材质B,而材质B中又包含了一个贴图C。那么他们的实际依赖关系便是:模型A——》材质B——》贴图C。
换言之,如果我们想在游戏中使用模型A,那么我们必须提前加载材质B。但是如果我们想要使用材质B,那么我们又必须提前加载贴图C。
这就是一个最简单的依赖模型。
我们继续看下一张图。
图二:
通过与图一的对比,我们很容看出这里所表达的依赖关系,即:模型B同时依赖材质B与材质C,而材质B又依赖于贴图D。
闲言少叙,我们即将进入正题,请看图三:
没错,图三就是将图一与图二结合在了一起。通过观察,我们发现模型A与模型B共用了相同的材质B。
那么问题来了,我们在对模型A与模型B进行Assetbundle的打包时,应该如何处理这部分共用的资源呢?
这就是我们今天的核心内容:资源的依赖打包。
根据以往的开发经验,我们很容易想到,如果我们忽略了这部分的共用资源,而将它们各自打包进模型A与模型B,势必会增加整体的资源大小,造成不必要的浪费,同时也增加了用户所需加载的资源量,这种做法非常不可取。所以,我们在制作Assetbundle包的同时,就有必要对资源的依赖关系进行分析,在根据每个资源被依赖的数量而采取不同的打包策略。
二、依赖资源的打包策略
有关Assetbundle的打包策略并不是唯一固定的,它是需要根据项目中的实际情况进行不断的调整与优化,以期达到一个合理的平衡。在这里,我仅给出那些被依赖的资源的打包策略的一些思路。
我们回到图三。
假设一:当模型A与模型B相互独立,没有任何关系。换句话说,它们分别在两个不同的项目。
这时,我们完全不需要考虑他们是否存在公共资源,这时由于材质B只被当前唯一的一个模型所依赖,所以,此时的材质B以及其他的资源是可以和当前的模型A或者模型B打包在一起的。
假设二:当模型A与模型B在同一个项目时,且会被同时使用的时候,就非常有必要将材质B进行独立打包。
但是,请注意:模型A所引用的贴图C由于只被模型A一个资源所使用,因此,在我即将提供的打包方案中,对于这种情况,我是将贴图C与模型A打包在一起的。
对于模型B所引用的贴图D亦是如此,贴图D将与模型B打包在一起。
三、打包算法概述
为了简单起见,我在这里设定了几条规则,当然这些规则要根据你的项目的实际情况进行调整。
规则1:
所有将被打包的文件都将被放置在统一的 待打包资源根路径(ResPath) 下,可以根据模块建立子目录。
规则2:
只有处于ResPath下的文件才可以被标记为父级资源。
规则3:
每一个处于ResPath下的文件将被独立打包成assetbundle。
规则4:
当非父资源被大于等于2的父级资源所依赖时,当前非父资源将被独立打包。否则,当前非父资源将与自己所归属的父级资源合并打包。请参考上文中有关图三的假设二的描述。
规则5:
所有的贴图/图集资源均将被独立打包。
好,如果您已经确保理解了这五点规则,那么我们就可以开始编写代码了。
四、打包算法
在这里,我一共使用了两个类来完成打包逻辑,代码内注释完整,我就不再赘述。请结合上文的五个规则,对我的代码进行阅读吧。
AssetBundleMgr.cs
入口文件
/*
* @Author: 千樽江月
* @Date: 2020-03-08 14:04:36
* @LastEditors: 千樽江月
* @LastEditTime: 2020-03-09 14:50:02
* @description: 资源打包管理器
*/
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using System;
using SunixLib;
namespace EditorFrameWork
{
/// <summary>
/// 资源打包管理器
/// </summary>
public class AssetBundleMgr
{
//待处理预设文件列表
private static List<FileInfo> toPackFileList = new List<FileInfo>();
//资源节点集合;
public static Dictionary<string, AssetNodeData> assetNodeDataDict = new Dictionary<string, AssetNodeData>();
/// <summary>
/// 启动打包
/// </summary>
[MenuItem("资源打包工具/资源打包", false)]
public static void StartAssetbundle()
{
//获取当前assetBundle打包时输出的目录;
string outPath = PathHelper.GetBundleOutPath();
Debug.Log("预设根路径:" + Constant.PrefabPath);
Debug.Log("输出路径:" + outPath);
//清除已有资源路径,并重建
ClearBundleFolder(outPath);
//遍历预设根路径,获取合规资源,准备进行下一步依赖查找
ListFile(new DirectoryInfo(Constant.PrefabPath));
//获取依赖分析
CollectDependencies();
//设置资源标签名称,该步骤是打包的核心逻辑以及必要前提
SetAssetsBundleName();
//将保存写入硬盘;
AssetDatabase.SaveAssets();
//执行打包;
BuildPipeline.BuildAssetBundles(outPath,BuildAssetBundleOptions.ChunkBasedCompression, GetTarget());
//刷新面板;
AssetDatabase.Refresh();
}
[MenuItem("资源打包工具/清理资源标签")]
static void CleaarAllABNames()
{
string[] abnames = AssetDatabase.GetAllAssetBundleNames();
foreach (var name in abnames)
AssetDatabase.RemoveAssetBundleName(name, true);
}
/// <summary>
/// 清除已有资源发布路径,并重建
/// </summary>
private static void ClearBundleFolder(string outPath)
{
if (Directory.Exists(outPath))
Directory.Delete(outPath, true);
Directory.CreateDirectory(outPath);
}
/// <summary>
/// 获取当前打包的平台;
/// </summary>
/// <returns></returns>
private static BuildTarget GetTarget()
{
BuildTarget target;
#if UNITY_IOS
target = BuildTarget.iOS;
#elif UNITY_ANDROID
target = BuildTarget.Android;
#elif UNITY_STANDALONE_WIN
target = BuildTarget.StandaloneWindows64;
#endif
return target;
}
/// <summary>
/// 获取依赖分析
/// </summary>
private static void CollectDependencies()
{
for (int i = 0; i < toPackFileList.Count; i++)
{
FileInfo file = toPackFileList[i];
//判断资源路径是否包含Assets,即是否为真实资源而非内置资源
int index = file.FullName.IndexOf(Constant.PathStart);
if (index != -1)
{
//获取当前文件的相对路径,即以Assets开头
string assetPath = file.FullName.Substring(index);
//获取资源节点对象,当前资源都未根节点资源
AssetNodeData node = GetAssetsNodeData(assetPath,true);
//开始分析依赖树
AnalizyDependencies(node);
}
}
}
/// <summary>
/// 设置资源包名称;
/// </summary>
private static void SetAssetsBundleName()
{
foreach (AssetNodeData node in assetNodeDataDict.Values)
node.SetAssetBundleName(2);
}
/// <summary>
/// 分析依赖关系
/// </summary>
private static void AnalizyDependencies(AssetNodeData curNode)
{
//获取资源对象;
UnityEngine.Object asset = curNode.GetAsset();
//获取hash数据,判断是否发生了资源变更;
//TODO 可以根据这个值得变化判断是否要进行依赖检测,提高效率
Hash128 code = AssetDatabase.GetAssetDependencyHash(curNode.AssetPath);
string[] paths = AssetDatabase.GetDependencies(curNode.AssetPath, true);
foreach (string path in paths)
{
AssetNodeData node = GetAssetsNodeData(path);
node.AddParent(curNode);
}
//立即释放资源;
EditorUtility.UnloadUnusedAssetsImmediate();
}
/// <summary>
/// 获取资源节点
/// </summary>
/// <param name="path">资源路径</param>
/// <param name="beRoot">是否为根资源</param>
/// <returns></returns>
private static AssetNodeData GetAssetsNodeData(string path,bool beParent = false)
{
//把win系统中的路径分割符修改为Unity类型
path = path.Replace("\\", "/");
AssetNodeData node = null;
if (!assetNodeDataDict.ContainsKey(path))
{
node = new AssetNodeData(path, beParent);
assetNodeDataDict.Add(path, node);
}
node = assetNodeDataDict[path];
node.beParent = beParent;
return node;
}
/// <summary>
/// 遍历当前文件夹,获取合规资源,准备进行下一步依赖查找
/// </summary>
/// <param name="fileSystemInfo"></param>
/// <param name="fileList"></param>
private static void ListFile(FileSystemInfo fileSystemInfo)
{
//将文件系统转为目录系统
DirectoryInfo dirInfo = fileSystemInfo as DirectoryInfo;
//获取文件夹下所有文件信息(含文件夹、文件)
FileSystemInfo[] fileSystemInfos = dirInfo.GetFileSystemInfos();
//遍历目录下所有项目
foreach (FileSystemInfo item in fileSystemInfos)
{
//将项目转化为文件
FileInfo fileInfo = item as FileInfo;
//证明这是一个文件;
if (fileInfo != null)
{
//把win系统中的路径分割符修改为Unity类型
string strFileFullName = fileInfo.FullName.Replace("\\", "/");
//获取文件扩展名
string fileExt = Path.GetExtension(strFileFullName);
//判断当前后缀是否有效
int index = Array.IndexOf(Constant.UnableExtArray, fileExt);
if (index > -1)
continue;
else
{
//将符合条件的文件放入文件列表;
toPackFileList.Add(fileInfo);
}
}
//这是一个目录
else
ListFile(item);
}
}
}
}
AssetNodeData.cs
辅助数据类,用来对待打包的资源进行引用计算
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
/*
* @Author: 千樽江月
* @Date: 2020-03-08 21:22:25
* @LastEditors: 千樽江月
* @LastEditTime: 2020-03-09 14:57:05
* @description: 打包用资源文件;
*/
namespace EditorFrameWork
{
/// <summary>
/// 资源数据对象;
/// </summary>
public class AssetNodeData
{
//是否为父级资源;
private bool _beParent = false;
//当前资源被引用的资源集合,即我被多少个资源引用
private List<AssetNodeData> parentDataSet = new List<AssetNodeData>();
/// <summary>
/// 构造函数
/// </summary>
/// <param name="assetPath">资源路径</param>
/// <param name="beRoot">是否为根资源</param>
public AssetNodeData(string assetPath, bool beParent = false)
{
this.beParent = beParent;
this.AssetPath = assetPath;
}
/// <summary>
/// 获取当前资源的父级数量;
/// </summary>
public int GetRootParentNum()
{
return parentDataSet.Count;
}
/// <summary>
/// 获取父节点;
/// </summary>
/// <returns></returns>
public AssetNodeData GetParent()
{
//理论上,调用该方法的对象一定只有一个父节点
return parentDataSet[0];
}
/// <summary>
/// 获取当前资源实体
/// </summary>
/// <returns></returns>
public Object GetAsset()
{
return AssetDatabase.LoadMainAssetAtPath(AssetPath);
}
/// <summary>
/// 将指定节点添加为我的父节点
/// </summary>
/// <param name="parent">父类节点</param>
public void AddParent(AssetNodeData parent)
{
if (parent == this || parent == null || HasParent(parent))
return;
parentDataSet.Add(parent);
}
/// <summary>
/// 判断此节点是否为我的父类节点
/// </summary>
/// <param name="targetParent">指定节点</param>
/// <returns></returns>
private bool HasParent(AssetNodeData node)
{
//判断我当前父类集合是否含有指定对象;
if (parentDataSet.Contains(node))
return true;
//判断父类的父类集合是否含有指定对象
var e = parentDataSet.GetEnumerator();
while (e.MoveNext())
{
if (e.Current.HasParent(node))
return true;
}
return false;
}
/// <summary>
/// 设置标签名称
/// </summary>
/// <param name="pieceThreshold"> 打包碎片粒度</param>
public void SetAssetBundleName(int pieceThreshold)
{
//针对图集/贴图的处理,是将每张图集/贴图独立打包
if (AssetsImporter is TextureImporter)
SetTextureBundleName();
else
{
//获取当前根级父容器的数量;
int count = GetRootParentNum();
if (this.beParent)
//设置父级对象的打包名称
SetAloneBundleName();
else if (count >= pieceThreshold)
//设置被多重引用的资源打包名称;
SetAloneBundleName();
else
//非根节点,但是只被引用了一次,所以应该设置成父节点的abName;
SetSonBundleName();
}
}
/// <summary>
/// 设置图集的打包,每张贴图独立打包;
/// </summary>
private void SetTextureBundleName()
{
TextureImporter tai = AssetsImporter as TextureImporter;
tai.spritePackingTag = AbName;
//AssetBundleName和spritePackingTag保持一致
Debug.Log("<color=#6501AB>" + "设置ab,我是贴图: " + this.AssetPath + " abname:" + tai.spritePackingTag + "</color>");
tai.SetAssetBundleNameAndVariant(tai.spritePackingTag, string.Empty);
}
/// <summary>
/// 设置被多重引用的资源打包名称;
/// </summary>
private void SetAloneBundleName()
{
AssetsImporter.SetAssetBundleNameAndVariant(AbName, string.Empty);
Debug.Log("<color=#6501AB>" + "设置ab,我是根资源或者被多资源引用: " + this.AssetPath + " abname:" + AbName + "</color>");
}
/// <summary>
/// 对只被引用了一次的子类对象进行打包名称的设置;
///
/// 该类型资源应该与父类资源打在一起;
/// </summary>
private void SetSonBundleName()
{
//获取父级对象
AssetNodeData parent = GetParent();
//设置打包名称
AssetsImporter.SetAssetBundleNameAndVariant(AbName, string.Empty);
Debug.Log("<color=#DBAF00>" + "设置, 仅有1个引用: " + this.AssetPath + " abname:" + AbName + "</color>");
}
/// <summary>
/// 获取当前资源的bundleName
/// </summary>
private string AbName
{
get {
return this.AssetPath.Replace("/", "_") + ".ab";
}
}
/// <summary>
/// 获取资源导入器
/// </summary>
public AssetImporter AssetsImporter {
get { return AssetImporter.GetAtPath(this.AssetPath); }
}
/// <summary>
/// 当前资源路径;
/// </summary>
/// <value></value>
public string AssetPath { get; private set; }
/// <summary>
/// 是否为父级对象;
/// </summary>
public bool beParent {
get { return _beParent; }
//如果当前已经是父级对象了,就不允许再被标记成非父级对象
set {
if (!_beParent)
_beParent = value;
}
}
}
}
至此,资源的依赖分析与分组打包就完成了。有关如何在项目中对存在依赖关系的资源进行加载,我将在下篇文章进行详细的叙述。