目录
打包与资源加载框架目录
正文
正文开始前,先把打包代码放过来,请注意,前面的代码已省略,自己去对比前面的文章。本篇文章从第一次执行打包代码开始。
public void PostAssetBuild()
{
//前面的代码省略,和上一篇文章一致
Log($"开始构建......");
BuildAssetBundleOptions opt = MakeBuildOptions();
AssetBundleManifest buildManifest = BuildPipeline.BuildAssetBundles(OutputPath, buildInfoList.ToArray(), opt, BuildTarget);
if (buildManifest == null)
throw new Exception("[BuildPatch] 构建过程中发生错误!");
//本篇的代码从这开始==============================================
// 清单列表
string[] allAssetBundles = buildManifest.GetAllAssetBundles();
Log($"资产清单里总共有{allAssetBundles.Length}个资产");
//create res manifest
var resManifest = CreateResManifest(buildMap, buildManifest);
var manifestAssetInfo = new AssetInfo(AssetDatabase.GetAssetPath(resManifest));
var label = "Assets/Manifest";
manifestAssetInfo.ReadableLabel = label;
manifestAssetInfo.AssetBundleVariant = PatchDefine.AssetBundleDefaultVariant;
manifestAssetInfo.AssetBundleLabel = HashUtility.BytesMD5(Encoding.UTF8.GetBytes(label));
var manifestBundleName = $"{manifestAssetInfo.AssetBundleLabel}.{manifestAssetInfo.AssetBundleVariant}".ToLower();
_labelToAssets.Add(manifestBundleName, new List<AssetInfo>() { manifestAssetInfo });
//build ResManifest bundle
buildInfoList.Clear();
buildInfoList.Add(new AssetBundleBuild()
{
assetBundleName = manifestAssetInfo.AssetBundleLabel,
assetBundleVariant = manifestAssetInfo.AssetBundleVariant,
assetNames = new[] { manifestAssetInfo.AssetPath }
});
var resbuildManifest = BuildPipeline.BuildAssetBundles(OutputPath, buildInfoList.ToArray(), opt, BuildTarget);
//加密代码省略,后面文章讲解
}
第一次调用BuildPipeline.BuildAssetBundles打包API后(详见代码第七行),会返回AssetBundleManifest的引用,
【疑问】:BuildPipeline.BuildAssetBundles打包API已经帮我们创建好了AB包之间的依赖关系引用了,为何还要创建AB包的引用关系?
【解答】:BuildPipeline.BuildAssetBundles打包API执行完生成的UnityManifest.manifest文件记录了所有AB包信息以及依赖关系,但是!企业级项目打包是要考虑增量打包的,因此我们想要知道每个AB是哪个版本打出的,需要一个标记,比如记录该AB包是从SVN 某某某阶段打出来的。因此打包接口生成的UnityManifest.manifest文件是个半成品。
下面开始正式介绍对UnityManifest.manifest文件的二次加工
string[] allAssetBundles = buildManifest.GetAllAssetBundles();拿到allAssetBundles再使用CreateResManifest方法创建一个Unity的Asset文件,把UnityManifest.manifest内为数不多的数据都序列化到该asset文件内。asset的序列化脚本是ResManifes,如下图
![unity manifest,开发心得,项目管理,unity,游戏开发,Powered by 金山文档](https://imgs.yssmx.com/Uploads/2024/01/772636-1.png)
UnityManifest.manifest文件的二次加工代码如下:
//assetList在前面的打包代码里有
//buildManifest第一次打包API返回的文件
private ResManifest CreateResManifest(List<AssetInfo> assetList , AssetBundleManifest buildManifest)
{
string[] bundles = buildManifest.GetAllAssetBundles();
var bundleToId = new Dictionary<string, int>();
for (int i = 0; i < bundles.Length; i++)
{
bundleToId[bundles[i]] = i;
}
var bundleList = new List<BundleInfo>();
for (int i = 0; i < bundles.Length; i++)
{
var bundle = bundles[i];
var deps = buildManifest.GetAllDependencies(bundle);
var hash = buildManifest.GetAssetBundleHash(bundle).ToString();
var encryptMethod = ResolveEncryptRule(bundle);
bundleList.Add(new BundleInfo()
{
Name = bundle,
Deps = Array.ConvertAll(deps, _ => bundleToId[_]),
Hash = hash,
EncryptMethod = encryptMethod
});
}
var assetRefs = new List<AssetRef>();
var dirs = new List<string>();
foreach (var assetInfo in assetList)
{
if (!assetInfo.IsCollectAsset) continue;
var dir = Path.GetDirectoryName(assetInfo.AssetPath).Replace("\\", "/");
CollectionSettingData.ApplyReplaceRules(ref dir);
var foundIdx = dirs.FindIndex(_ => _.Equals(dir));
if (foundIdx == -1)
{
dirs.Add(dir);
foundIdx = dirs.Count - 1;
}
var nameStr = $"{assetInfo.AssetBundleLabel}.{assetInfo.AssetBundleVariant}".ToLower();
assetRefs.Add(new AssetRef()
{
Name = Path.GetFileNameWithoutExtension(assetInfo.AssetPath),
BundleId = bundleToId[$"{assetInfo.AssetBundleLabel}.{assetInfo.AssetBundleVariant}".ToLower()],
DirIdx = foundIdx
});
}
var resManifest = GetResManifest();
resManifest.Dirs = dirs.ToArray();
resManifest.Bundles = bundleList.ToArray();
resManifest.AssetRefs = assetRefs.ToArray();
EditorUtility.SetDirty(resManifest);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
return resManifest;
}
![unity manifest,开发心得,项目管理,unity,游戏开发,Powered by 金山文档](https://imgs.yssmx.com/Uploads/2024/01/772636-2.png)
下面是序列化数据的代码:
/// <summary>
/// design based on Google.Android.AppBundle AssetPackDeliveryMode
/// </summary>
[Serializable]
public enum EAssetDeliveryMode
{
// ===> AssetPackDeliveryMode.InstallTime
Main = 1,
// ====> AssetPackDeliveryMode.FastFollow
FastFollow = 2,
// ====> AssetPackDeliveryMode.OnDemand
OnDemand = 3
}
/// <summary>
/// AssetBundle打包位置
/// </summary>
[Serializable]
public enum EBundlePos
{
/// <summary>
/// 普通
/// </summary>
normal,
/// <summary>
/// 在安装包内
/// </summary>
buildin,
/// <summary>
/// 游戏内下载
/// </summary>
ingame,
}
[Serializable]
public enum EEncryptMethod
{
None = 0,
Quick, //padding header
Simple,
X, //xor
QuickX //partial xor
}
[Serializable]
[ReadOnly]
public struct AssetRef
{
[ReadOnly, EnableGUI]
public string Name;
[ReadOnly, EnableGUI]
public int BundleId;
[ReadOnly, EnableGUI]
public int DirIdx;
}
[Serializable]
public enum ELoadMode
{
None,
LoadFromStreaming,
LoadFromCache,
LoadFromRemote,
}
[Serializable]
public struct BundleInfo
{
[ReadOnly, EnableGUI]
public string Name;
[ReadOnly, EnableGUI]
[ListDrawerSettings(Expanded=false)]
public int[] Deps;
[ReadOnly]
public string Hash;
[ReadOnly]
public EEncryptMethod EncryptMethod;
// public ELoadMode LoadMode;
}
public class ResManifest : ScriptableObject
{
[ReadOnly, EnableGUI]
public string[] Dirs = new string[0];
[ListDrawerSettings(IsReadOnly = true)]
public AssetRef[] AssetRefs = new AssetRef[0];
[ListDrawerSettings(IsReadOnly = true)]
public BundleInfo[] Bundles = new BundleInfo[0];
}
}
看图就可知,CreateResManifest方法就是创建了一套属于我们自己的,资源与AB包索引关系。
ResManifes序列化(代码在下面)文件存储了3类数据,
所有资源文件夹List
资源所在的AB包List编号、资源所在文件夹List编号
AB包的Name、依赖包名字、版本号MD5,使用加密类型。
【疑问】:为何要序列化这个asset文件?
回答问题之前,先提出一个问题:资源加载肯定是给开发人员用的,开发人员要如何找到想要的资源在哪个ab包里?
【解答】:项目启动的时候,我们要使用这个asset文件去创建所有资源的一个引用信息,项目启动后是要加载这个asset,加载代码如下。
protected virtual ResManifest LoadResManifest()
{
string label = "Assets/Manifest";
var manifestBundleName = $"{HashUtility.BytesMD5(Encoding.UTF8.GetBytes(label))}.unity3d";
string loadPath = GetAssetBundleLoadPath(manifestBundleName);
var offset = AssetSystem.DecryptServices.GetDecryptOffset(manifestBundleName);
var usingFileSystem = GetLocation(loadPath) == AssetLocation.App
? FileSystemManagerBase.Instance.MainVFS
: FileSystemManagerBase.Instance.GetSandboxFileSystem(PatchDefine.MainPackKey);
if (usingFileSystem != null)
{
offset += usingFileSystem.GetBundleContentOffset(manifestBundleName);
}
AssetBundle bundle = AssetBundle.LoadFromFile(loadPath, 0, offset);
if (bundle == null)
throw new Exception("Cannot load ResManifest bundle");
var manifest = bundle.LoadAsset<ResManifest>("Assets/Manifest.asset");
if (manifest == null)
throw new Exception("Cannot load Assets/Manifest.asset asset");
for (var i = 0; i < manifest.Dirs.Length; i++)
{
var dir = manifest.Dirs[i];
_dirToIds[dir] = i;
}
for (var i = 0; i < manifest.Bundles.Length; i++)
{
var info = manifest.Bundles[i];
_bundleMap[info.Name] = i;
}
foreach (var assetRef in manifest.AssetRefs)
{
var path = StringFormat.Format("{0}/{1}", manifest.Dirs[assetRef.DirIdx], assetRef.Name);
// MotionLog.Log(ELogLevel.Log, $"path is {path}");
if (!_assetToBundleMap.TryGetValue(assetRef.DirIdx, out var assetNameToBundleId))
{
assetNameToBundleId = new Dictionary<string, int>();
_assetToBundleMap.Add(assetRef.DirIdx, assetNameToBundleId);
}
assetNameToBundleId.Add(assetRef.Name, assetRef.BundleId);
}
bundle.Unload(false);
return manifest;
}
看上面代码就知道,这个asset文件也是被打进了bundle里,并且单独一个ab包。再看一下本篇文章的标题:《使用Manifest二次构建资源索引》,那么,这个asset所在的bundle就是本篇文章的核心!!!
讲述一下在项目中开发人员是如何加载资源的,首先,开发人员会调用一个Loader去加载资源,如果是使用AB包加载模式(本地资源加载不讨论),那么一定会传入一个资源路径,和加载成功回调
Loader.Load("Assets/Works/Resource/Sprite/UIBG/bg_lihui",callbackFunction)
//成功后回调
void callbackFunction(资源文件)
{
//使用资源文件
}
我们知道,项目启动时会加载这个资源索引文件,所以框架当然知道所有资源路径和它引用的AB包名称,因此加载资源时会自然而然的找到对应的AB包,同时资源索引文件还记录了AB包的互相依赖关系,加载目标AB包时,递归加载所有依赖包就好啦。
项目里如何使用这个二次构建的资源索引文件上面已经讲清楚了,下面开始讲如何在项目启动时热更下载所有AB包。
CreatePatchManifestFile方法是创建AB包下载清单,请注意,创建新清单前会先加载老清单,并且对比AB包生成的MD5有没有发生变化,如果没变化,则继续沿用老清单的版本号,举个例子:假设UI_Login预设是在版本1生成的,这次打包时版本2,由于UI_Login在本次打包中对比发现MD5没变化,则UI_Login所在的AB包版本依然写1,其他变化的、以及新添加的资源版本号写2。
/// <summary>
/// 1. 创建补丁清单文件到输出目录
/// params: isInit 创建的是否是包内的补丁清单
/// useAAB 创建的是否是aab包使用的补丁清单
/// </summary>
private void CreatePatchManifestFile(string[] allAssetBundles, bool isInit = false, bool useAAB = false)
{
// 加载旧文件
PatchManifest patchManifest = LoadPatchManifestFile(isInit);
// 删除旧文件
string filePath = OutputPath + $"/{PatchDefine.PatchManifestFileName}";
if (isInit)
filePath = OutputPath + $"/{PatchDefine.InitManifestFileName}";
if (File.Exists(filePath))
File.Delete(filePath);
// 创建新文件
Log($"创建补丁清单文件:{filePath}");
var sb = new StringBuilder();
using (FileStream fs = File.OpenWrite(filePath))
{
using (var bw = new BinaryWriter(fs))
{
// 写入强更版本信息
//bw.Write(GameVersion.Version);
//sb.AppendLine(GameVersion.Version.ToString());
int ver = BuildVersion;
// 写入版本信息
// if (isReview)
// {
// ver = ver * 10;
// }
bw.Write(ver);
sb.AppendLine(ver.ToString());
// 写入所有AssetBundle文件的信息
var fileCount = allAssetBundles.Length;
bw.Write(fileCount);
for (var i = 0; i < fileCount; i++)
{
var assetName = allAssetBundles[i];
string path = $"{OutputPath}/{assetName}";
string md5 = HashUtility.FileMD5(path);
long sizeKB = EditorTools.GetFileSize(path) / 1024;
int version = BuildVersion;
EBundlePos tag = EBundlePos.buildin;
string readableLabel = "undefined";
if (_labelToAssets.TryGetValue(assetName, out var list))
{
readableLabel = list[0].ReadableLabel;
if (useAAB)
tag = list[0].bundlePos;
}
// 注意:如果文件没有变化使用旧版本号
PatchElement element;
if (patchManifest.Elements.TryGetValue(assetName, out element))
{
if (element.MD5 == md5)
version = element.Version;
}
var curEle = new PatchElement(assetName, md5, version, sizeKB, tag.ToString(), isInit);
curEle.Serialize(bw);
if (isInit)
sb.AppendLine($"{assetName}={readableLabel}={md5}={sizeKB}={version}={tag.ToString()}");
else
sb.AppendLine($"{assetName}={readableLabel}={md5}={sizeKB}={version}");
}
}
string txtName = "PatchManifest.txt";
if (isInit)
txtName = "InitManifest.txt";
File.WriteAllText(OutputPath + "/" + txtName, sb.ToString());
Debug.Log($"{OutputPath}/{txtName} OK");
}
}
生成的AB包清单长下面这个样子。
![unity manifest,开发心得,项目管理,unity,游戏开发,Powered by 金山文档](https://imgs.yssmx.com/Uploads/2024/01/772636-3.png)
第一行是SVN版本号
第二行是AB包数量
从第三行开始是资源包信息,以=号分割开有效数据,分别是
MD5.unity3d = 资源路径 = 资源路径的HashId = 包体KB大小 = SVN版本号 = 启动热更模式
最终把这个InitManifest.txt写成bytes,传到服务器就可以对比数据包了
本系列文章加载篇我会正式的讲解AB包的加载,本文只是简单介绍一下。
第一步:
当客户端启动后,进入下载清单状态机, Http先下载InitManifest.txt或者InitManifest.bytes文件,并解析AB包清单。
下面是解析AB包清单的代码。
public class PatchElement
{
/// <summary>
/// 文件名称
/// </summary>
public string Name { private set; get; }
/// <summary>
/// 文件MD5
/// </summary>
public string MD5 { private set; get; }
/// <summary>
/// 文件版本
/// </summary>
public int Version { private set; get; }
/// <summary>
/// 文件大小
/// </summary>
public long SizeKB { private set; get; }
/// <summary>
/// 构建类型
/// buildin 在安装包中
/// ingame 游戏中下载
/// </summary>
public string Tag { private set; get; }
/// <summary>
/// 是否是安装包内的Patch
/// </summary>
public bool IsInit { private set; get; }
/// <summary>
/// 下载文件的保存路径
/// </summary>
public string SavePath;
/// <summary>
/// 每次更新都会先下载到Sandbox_Temp目录,防止下到一半重启导致逻辑不一致报错
/// temp目录下的文件在重新进入更新流程时先校验md5看是否要跳过下载
/// </summary>
public bool SkipDownload { get; set; }
public PatchElement(string name, string md5, int version, long sizeKB, string tag, bool isInit = false)
{
Name = name;
MD5 = md5;
Version = version;
SizeKB = sizeKB;
Tag = tag;
IsInit = isInit;
SkipDownload = false;
}
public void Serialize(BinaryWriter bw)
{
bw.Write(Name);
bw.Write(MD5);
bw.Write(SizeKB);
bw.Write(Version);
if (IsInit)
bw.Write(Tag);
}
public static PatchElement Deserialize(BinaryReader br, bool isInit = false)
{
var name = br.ReadString();
var md5 = br.ReadString();
var sizeKb = br.ReadInt64();
var version = br.ReadInt32();
var tag = EBundlePos.buildin.ToString();
if (isInit)
tag = br.ReadString();
return new PatchElement(name, md5, version, sizeKb, tag, isInit);
}
}
第二步:
请注意,中断续传也是个很重要的功能,AB包清单记录了每个AB包的大小,当项目启动时,优先遍历Temp文件夹内的AB包,如果大小和清单内的不一致,则开启Http的下载功能,Http是支持断点续传的,Http的Header里定义要下载的数据段。如果你觉得这样不保险,可以直接删掉这个AB包重新下载。
AB包清单解析完后,切换到下载清单状态机,开启清单的每一个文件下载,请注意,热更下载文件时,我们可以先创建一个Temp文件夹,未全部下载成功前的AB包都在这里,全部下载成功后,再全部剪切到PersistentData文件夹内,PersistentData文件夹是Unity内置的沙盒目录,Unity有读写权限。
全部下载完成后,完成PersistentData文件夹剪切工作。
第三步:
全部资源已就绪,启动正式业务框架。
疑问:为何在热更完后再启动正式业务框架?文章来源:https://www.toymoban.com/news/detail-772636.html
目前大多数商业项目都是Tolua、Xlua框架,很多框架层代码都是写到Lua中去的,Lua代码属于AB包的一部分,因此只能等热更完后启动。文章来源地址https://www.toymoban.com/news/detail-772636.html
到了这里,关于[游戏开发][Unity]Assetbundle打包篇(5)使用Manifest二次构建资源索引的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!