目录
[1. 插件AssetBundle Browser 打AB包](#1. 插件AssetBundle Browser 打AB包)
[(1)Unity(我用的版本是2020.3.8)导入AssetBundle Browser](#(1)Unity(我用的版本是2020.3.8)导入AssetBundle Browser)
[2. 代码打AB包](#2. 代码打AB包)
[1. 准备工作](#1. 准备工作)
[(2) 通过Packages清单安装YooAsset](#(2) 通过Packages清单安装YooAsset)
[(1)Enable Addressable](#(1)Enable Addressable)
[(2)Pack Rule打包规则](#(2)Pack Rule打包规则)
[(1)Build Mode](#(1)Build Mode)
一、AssetBundle
AB包可以存储绝大部分Unity资源但无法直接存储C#脚本,所以代码的热更新需要使用Lua或者存储编译后的DLL文件。
AB包不能重复进行加载,当AB包已经加载进内存后必须卸载后才能重新加载。
多个资源分布在不同的AB包可能会出现一个预制体的贴图等部分资源不在同一个包下,直接加载会出现部分资源丢失的情况,即AB包之间是存在依赖关系的,在加载当前AB包时需要一并加载其所依赖的包。
打包完成后,会自动生成一个主包(主包名称随平台不同而不同),主包的manifest下会存储有版本号、校验码(CRC)、所有其它包的相关信息(名称、依赖关系)。
1. 插件AssetBundle Browser 打AB包
(1)Unity(我用的版本是2020.3.8)导入AssetBundle Browser
从github下载插件,将下载后的安装包解压到Unity工程的Packages文件夹下,如果报错,删除Tests即可。
(2)设置Prefab
(3)AssetBundleBrowser面板
正确获取到并安装完插件后,通过 Windows/AssetBundle Browser下打开AB包管理面板 一共有三个面板。利用AssetBundleBrowser打包时,我们用的是Build面板,设置好之后点击[build]即可。
-
Configure面板 :能查看当前AB包及其内部资源的基本情况(大小,资源,依赖情况等)。
-
Build面板:负责AssetBundle打包的相关设置。
-
Inspect面板:用来查看已经打包后的AB包文件的一些详细情况(大小,资源路径等)。
2. 代码打AB包
通过代码打AB包,不需要设置prefab的AssetBundle名称。
cs
private static void BuildAssetBundles(string tempDir, string outputDir, BuildTarget target)
{
Debug.Log("AB outputDir:"+ outputDir);
Directory.CreateDirectory(tempDir);
Directory.CreateDirectory(outputDir);
List<AssetBundleBuild> abs = new List<AssetBundleBuild>();
{
var prefabAssets = new List<string>();
string testPrefab = $"{Application.dataPath}/Prefabs/Cube.prefab";
prefabAssets.Add(testPrefab);
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
abs.Add(new AssetBundleBuild
{
assetBundleName = "prefabs",
assetNames = prefabAssets.Select(s => ToRelativeAssetPath(s)).ToArray(),
});
}
BuildPipeline.BuildAssetBundles(outputDir, abs.ToArray(), BuildAssetBundleOptions.None, target);
AssetDatabase.Refresh(ImportAssetOptions.ForceUpdate);
}
二、YooAsset
AssetBundle 的管理和使用相对复杂,需要处理依赖关系、版本控制、加载策略等问题。因此,很多开发者会选择使用如 YooAsset 这样的资源管理框架来简化 AssetBundle 的使用,自动处理这些复杂的任务。
1. 准备工作
(1)官方文档
(2) 通过Packages清单安装YooAsset
直接修改Packages文件夹下的清单文件manifest.json:
{
"dependencies": {
"com.tuyoogame.yooasset": "2.1.0",
......
},
"scopedRegistries": [
{
"name": "package.openupm.cn",
"url": "https://package.openupm.cn",
"scopes": [
"com.tuyoogame.yooasset"
]
}
]
}
如果出现以下报错,说明Unity没有安装Windows Build Support(IL2CPP)模块,安装即可。
2.全局配置
Project窗体内右键 -> Create -> YooAsset -> Create YooAsset Setting可创建YooAssetSettings全局配置文件,该文件需放在Resources文件夹下:
- Manifest File Name : 清单文件名称
- DefaultYooFolderName:Yoo文件夹名称
3.资源配置
Project窗体内右键 -> Create -> YooAsset -> Create AssetBundle Collector Setting可创建该文件,需要注意的是一个工程只能有一个AssetBundleCollectorSetting。
重点关注Enable Addressable及Pack Rule打包规则:
(1)Enable Addressable
启用可寻址资源定位系统。启用后加载资源时可以不写全路径,只根据资源名称即可加载:
cs
YooAssets.LoadSceneAsync("scene_home");
(2)Pack Rule打包规则
规则可以自定义扩展。下面是内置规则:
- PackSeparately 以文件路径作为资源包名,每个资源文件单独打包(比如场景文件)。
- PackDirectory 以文件所在的文件夹路径作为资源包名,该文件夹下所有文件打进一个资源包。
- PackTopDirectory 以收集器下顶级文件夹为资源包名,该文件夹下所有文件打进一个资源包。
- PackCollector 以收集器路径作为资源包名,收集的所有文件打进一个资源包。
- PackGroup 以分组名称作为资源包名,收集的所有文件打进一个资源包。
- PackRawFile 目录下的资源文件会被处理为原生资源包。
4.资源构建
(1)Build Mode
- ForceRebuild:强制构建模式,会删除指定构建平台下的所有构建记录,重新构建所有资源包。
- IncrementalBuild:增量构建模式,以上一次构建结果为基础,对于发生变化的资源进行增量构建。
(2)Encryption
加密类列表。Build时选的加密类型要和加载资源时的解密类型一致,否则加载资源时会报错。比如Build时选择的加密方式为FileOffsetEncryption,则文件解密服务接口也需要为FileOffset类型:
cs
{
...
var initParameters = new HostPlayModeParameters();//HostPlayModeParameters继承自 InitializeParameters
initParameters.BuildinQueryServices = new GameQueryServices();//内置资源查询服务接口
initParameters.DecryptionServices = new FileOffsetDecryption();//如果资源包在构建的时候有加密,需要提供实现IDecryptionServices接口的实例类。
...
}
/// <summary>
/// 资源文件偏移加载解密类
/// </summary>
private class FileOffsetDecryption : IDecryptionServices
{
/// <summary>
/// 同步方式获取解密的资源包对象
/// 注意:加载流对象在资源包对象释放的时候会自动释放
/// </summary>
AssetBundle IDecryptionServices.LoadAssetBundle(DecryptFileInfo fileInfo, out Stream managedStream)
{
managedStream = null;
return AssetBundle.LoadFromFile(fileInfo.FileLoadPath, fileInfo.ConentCRC, GetFileOffset());
}
/// <summary>
/// 异步方式获取解密的资源包对象
/// 注意:加载流对象在资源包对象释放的时候会自动释放
/// </summary>
AssetBundleCreateRequest IDecryptionServices.LoadAssetBundleAsync(DecryptFileInfo fileInfo, out Stream managedStream)
{
managedStream = null;
return AssetBundle.LoadFromFileAsync(fileInfo.FileLoadPath, fileInfo.ConentCRC, GetFileOffset());
}
private static ulong GetFileOffset()
{
return 32;
}
}
5.YooAsset运行时
以官方提供的Space Shooter这个Demo为例,重新编写YooAsset相关的流程。先确保Demo能正常运行:
- Space Shooter在导入完成后,打开YooAsset->AssetBundle Collector窗口。
- 点击修复按钮,然后点击Save按钮保存配置,最后关闭窗口。
- 找到Boot.scene场景启动游戏。
打开Boot.cs文件,注释掉 IEnumerator Start方法,在TestLoad方法中一步一步进行YooAsset资源的加载:
cs
private void Start()
{
StartCoroutine(TestLoad());
}
IEnumerator TestLoad()
{
...
}
(1)初始化
cs
IEnumerator TestLoad()
{
// 初始化资源系统
YooAssets.Initialize();
// 创建默认的资源包
var package = YooAssets.CreatePackage("DefaultPackage");
// 设置该资源包为默认的资源包,可以使用YooAssets相关加载接口加载该资源包内容。
YooAssets.SetDefaultPackage(package);
...
}
(2)资源系统的运行模式
cs
...
if (PlayMode == EPlayMode.EditorSimulateMode)
{
var initParameters = new EditorSimulateModeParameters();//EditorSimulateModeParameters继承自InitializeParameters
string simulateManifestFilePath = EditorSimulateModeHelper.SimulateBuild(EDefaultBuildPipeline.BuiltinBuildPipeline.ToString(), "DefaultPackage");
initParameters.SimulateManifestFilePath = simulateManifestFilePath;
yield return package.InitializeAsync(initParameters);
}
else if (PlayMode == EPlayMode.OfflinePlayMode)
{
var initParameters = new OfflinePlayModeParameters();//OfflinePlayModeParameters继承自InitializeParameters
initParameters.DecryptionServices = new FileOffsetDecryption();//需要补充这个
yield return package.InitializeAsync(initParameters);
}
else if (PlayMode == EPlayMode.HostPlayMode)
{
// 注意:GameQueryServices.cs 太空战机的脚本类,详细见StreamingAssetsHelper.cs
string defaultHostServer = "https://static0.xesimg.com/project-yooasset/0130Offset";
string fallbackHostServer = "https://static0.xesimg.com/project-yooasset/0130Offset";
var initParameters = new HostPlayModeParameters();//HostPlayModeParameters继承自InitializeParameters
initParameters.BuildinQueryServices = new GameQueryServices();//内置资源查询服务接口
initParameters.DecryptionServices = new FileOffsetDecryption();//如果资源包在构建的时候有加密,需要提供实现IDecryptionServices接口的实例类。
initParameters.RemoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);//远端服务器查询服务接口
var initOperation = package.InitializeAsync(initParameters);
yield return initOperation;
if (initOperation.Status == EOperationStatus.Succeed)
{
Debug.Log("资源包初始化成功!");
}
else
{
Debug.LogError($"资源包初始化失败:{initOperation.Error}");
}
}
else if (PlayMode == EPlayMode.WebPlayMode)
{
string defaultHostServer = "http://127.0.0.1/CDN/WebGL/V1.0";
string fallbackHostServer = "http://127.0.0.1/CDN/WebGL/V1.0";
var initParameters = new WebPlayModeParameters();//WebPlayModeParameters继承自InitializeParameters
initParameters.BuildinQueryServices = new GameQueryServices();
initParameters.RemoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);
var initOperation = package.InitializeAsync(initParameters);
yield return initOperation;
if (initOperation.Status == EOperationStatus.Succeed)
{
Debug.Log("资源包初始化成功!");
}
else
{
Debug.LogError($"资源包初始化失败:{initOperation.Error}");
}
}
补充两个类:
cs
/// <summary>
/// 资源文件偏移加载解密类
/// </summary>
private class FileOffsetDecryption : IDecryptionServices
{
/// <summary>
/// 同步方式获取解密的资源包对象
/// 注意:加载流对象在资源包对象释放的时候会自动释放
/// </summary>
AssetBundle IDecryptionServices.LoadAssetBundle(DecryptFileInfo fileInfo, out Stream managedStream)
{
managedStream = null;
return AssetBundle.LoadFromFile(fileInfo.FileLoadPath, fileInfo.ConentCRC, GetFileOffset());
}
/// <summary>
/// 异步方式获取解密的资源包对象
/// 注意:加载流对象在资源包对象释放的时候会自动释放
/// </summary>
AssetBundleCreateRequest IDecryptionServices.LoadAssetBundleAsync(DecryptFileInfo fileInfo, out Stream managedStream)
{
managedStream = null;
return AssetBundle.LoadFromFileAsync(fileInfo.FileLoadPath, fileInfo.ConentCRC, GetFileOffset());
}
private static ulong GetFileOffset()
{
return 32;
}
}
/// <summary>
/// 远端资源地址查询服务类
/// </summary>
private class RemoteServices : IRemoteServices
{
private readonly string _defaultHostServer;
private readonly string _fallbackHostServer;
public RemoteServices(string defaultHostServer, string fallbackHostServer)
{
_defaultHostServer = defaultHostServer;
_fallbackHostServer = fallbackHostServer;
}
string IRemoteServices.GetRemoteMainURL(string fileName)
{
return $"{_defaultHostServer}/{fileName}";
}
string IRemoteServices.GetRemoteFallbackURL(string fileName)
{
return $"{_fallbackHostServer}/{fileName}";
}
}
(3)获取资源版本
cs
...
var package = YooAssets.GetPackage("DefaultPackage");
var operation = package.UpdatePackageVersionAsync();
yield return operation;
if (operation.Status == EOperationStatus.Succeed)
{
//更新成功
string packageVersion = operation.PackageVersion;
Debug.Log($"Updated package Version : {packageVersion}");
}
else
{
//更新失败
Debug.LogError(operation.Error);
}
(4)更新资源清单
对于联机运行模式,在获取到资源版本号之后,就可以利用UpdatePackageManifestAsync更新资源清单了:
cs
...
bool savePackageVersion = true;
var package = YooAssets.GetPackage("DefaultPackage");
var operation = package.UpdatePackageManifestAsync(packageVersion, savePackageVersion);
yield return operation;
if (operation.Status == EOperationStatus.Succeed)
{
//更新成功
}
else
{
//更新失败
Debug.LogError(operation.Error);
}
(5)资源包下载
cs
IEnumerator Download()
{
int downloadingMaxNum = 10;
int failedTryAgain = 3;
var package = YooAssets.GetPackage("DefaultPackage");
//创建资源下载器,下载所有资源
var downloader = package.CreateResourceDownloader(downloadingMaxNum, failedTryAgain);
//没有需要下载的资源
if (downloader.TotalDownloadCount == 0)
{
Debug.Log("TotalDownloadCount = 0");
yield break;
}
//需要下载的文件总数和总大小
int totalDownloadCount = downloader.TotalDownloadCount;
long totalDownloadBytes = downloader.TotalDownloadBytes;
//注册回调方法
downloader.OnDownloadErrorCallback = OnDownloadErrorFunction;
downloader.OnDownloadProgressCallback = OnDownloadProgressUpdateFunction;
downloader.OnDownloadOverCallback = OnDownloadOverFunction;
downloader.OnStartDownloadFileCallback = OnStartDownloadFileFunction;
//开启下载
downloader.BeginDownload();
yield return downloader;
//检测下载结果
if (downloader.Status == EOperationStatus.Succeed)
{
Debug.Log("Finish");
}
else
{
Debug.Log("DownLoad Failed");
}
}
private void OnStartDownloadFileFunction(string fileName, long sizeBytes)
{
Debug.Log("fileName:" + fileName + ",sizeBytes:" + sizeBytes);
}
private void OnDownloadOverFunction(bool isSucceed)
{
Debug.Log("isSucceed");
}
private void OnDownloadProgressUpdateFunction(int totalDownloadCount, int currentDownloadCount, long totalDownloadBytes, long currentDownloadBytes)
{
Debug.Log("totalDownloadCount:" + totalDownloadCount + ",currentDownloadCount" + currentDownloadCount + ",totalDownloadBytes:" + totalDownloadBytes + ",currentDownloadBytes" + currentDownloadBytes);
}
private void OnDownloadErrorFunction(string fileName, string error)
{
Debug.Log("DownloadError:" + fileName + ",error:" + error);
}
(6)资源加载
cs
//加载场景,启用可寻址功能(Enable Addressable)后,不用写全路径,直接写资源名称即可
string location = "scene_home";
var sceneMode = UnityEngine.SceneManagement.LoadSceneMode.Single;
bool suspendLoad = false;
SceneHandle handle = package.LoadSceneAsync(location, sceneMode, suspendLoad);
yield return handle;
Debug.Log($"Scene name is {handle.SceneObject.name}");
//加载预制体
AssetHandle handle0 = package.LoadAssetAsync<GameObject>("UIHome");//不用加后缀
yield return handle0;
GameObject go = handle0.InstantiateSync();
Debug.Log($"Prefab name is {go.name}");
//加载音频
AssetHandle handle1 = package.LoadAssetAsync<AudioClip>("music_background");
yield return handle1;
AudioClip audioClip = handle1.AssetObject as AudioClip;
GameObject audio = new GameObject("AudioSource");
audio.AddComponent<AudioSource>().clip= audioClip;
audio.GetComponent<AudioSource>().Play();
(7)完整TestLoad
cs
IEnumerator TestLoad()
{
//0.别忘初始化项目中这两个事件相关的类
// 游戏管理器 注册场景事件
GameManager.Instance.Behaviour = this;
// 初始化事件系统
UniEvent.Initalize();
//1.初始化
Debug.Log("1. 初始化");
// 初始化资源系统
YooAssets.Initialize();
// 创建默认的资源包
var package = YooAssets.CreatePackage("DefaultPackage");
// 设置该资源包为默认的资源包,可以使用YooAssets相关加载接口加载该资源包内容。
YooAssets.SetDefaultPackage(package);
//2.资源系统的运行模式
Debug.Log("2. 资源系统的运行模式");
if (PlayMode == EPlayMode.EditorSimulateMode)
{
var initParameters = new EditorSimulateModeParameters();//EditorSimulateModeParameters继承自InitializeParameters
string simulateManifestFilePath = EditorSimulateModeHelper.SimulateBuild(EDefaultBuildPipeline.BuiltinBuildPipeline.ToString(), "DefaultPackage");
initParameters.SimulateManifestFilePath = simulateManifestFilePath;
yield return package.InitializeAsync(initParameters);
}
else if (PlayMode == EPlayMode.OfflinePlayMode)
{
var initParameters = new OfflinePlayModeParameters();//OfflinePlayModeParameters继承自InitializeParameters
initParameters.DecryptionServices = new FileOffsetDecryption();//需要补充这个
yield return package.InitializeAsync(initParameters);
}
else if (PlayMode == EPlayMode.HostPlayMode)
{
// 注意:GameQueryServices.cs 太空战机的脚本类,详细见StreamingAssetsHelper.cs
string defaultHostServer = "https://static0.xesimg.com/project-yooasset/0130Offset";
string fallbackHostServer = "https://static0.xesimg.com/project-yooasset/0130Offset";
var initParameters = new HostPlayModeParameters();//HostPlayModeParameters继承自InitializeParameters
initParameters.BuildinQueryServices = new GameQueryServices();//内置资源查询服务接口
initParameters.DecryptionServices = new FileOffsetDecryption();//如果资源包在构建的时候有加密,需要提供实现IDecryptionServices接口的实例类。
initParameters.RemoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);//远端服务器查询服务接口
var initOperation = package.InitializeAsync(initParameters);
yield return initOperation;
if (initOperation.Status == EOperationStatus.Succeed)
{
Debug.Log("资源包初始化成功!");
}
else
{
Debug.LogError($"资源包初始化失败:{initOperation.Error}");
}
}
else if (PlayMode == EPlayMode.WebPlayMode)
{
string defaultHostServer = "http://127.0.0.1/CDN/WebGL/V1.0";
string fallbackHostServer = "http://127.0.0.1/CDN/WebGL/V1.0";
var initParameters = new WebPlayModeParameters();//WebPlayModeParameters继承自InitializeParameters
initParameters.BuildinQueryServices = new GameQueryServices();
initParameters.RemoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);
var initOperation = package.InitializeAsync(initParameters);
yield return initOperation;
if (initOperation.Status == EOperationStatus.Succeed)
{
Debug.Log("资源包初始化成功!");
}
else
{
Debug.LogError($"资源包初始化失败:{initOperation.Error}");
}
}
//3.获取资源版本:UpdatePackageVersionAsync
Debug.Log("3. 获取资源版本");
var operation = package.UpdatePackageVersionAsync();
yield return operation;
if (operation.Status == EOperationStatus.Succeed)
{
string packageVersion = operation.PackageVersion;
Debug.Log($"Updated package Version : {packageVersion}");
//4.更新资源清单:对于联机运行模式,在获取到资源版本号之后,就可以更新资源清单了:UpdatePackageManifestAsync
//联机运行模式
//通过传入的清单版本,优先比对当前激活清单的版本,如果相同就直接返回成功。如果有差异就从缓存里去查找匹配的清单,如果缓存里不存在,就去远端下载并保存到沙盒里。最后加载沙盒内匹配的清单文件。
Debug.Log("4. 更新资源清单");
bool savePackageVersion = true;
var operation2 = package.UpdatePackageManifestAsync(packageVersion, savePackageVersion);
yield return operation2;
if (operation2.Status == EOperationStatus.Succeed)
{
//5.资源包下载
Debug.Log("5. 资源包下载");
yield return Download();
//6.加载场景,启用可寻址功能(Enable Addressable)后,不用写全路径,直接写资源名称即可
//YooAssets.LoadSceneAsync("scene_home");
string location = "scene_home";
var sceneMode = UnityEngine.SceneManagement.LoadSceneMode.Single;
bool suspendLoad = false;
SceneHandle handle = package.LoadSceneAsync(location, sceneMode, suspendLoad);
yield return handle;
Debug.Log($"Scene name is {handle.SceneObject.name}");
//7.加载预制体
AssetHandle handle0 = package.LoadAssetAsync<GameObject>("UIHome");//不用加后缀
yield return handle0;
GameObject go = handle0.InstantiateSync();
Debug.Log($"Prefab name is {go.name}");
//8.加载音频
AssetHandle handle1 = package.LoadAssetAsync<AudioClip>("music_background");
yield return handle1;
AudioClip audioClip = handle1.AssetObject as AudioClip;
GameObject audio = new GameObject("AudioSource");
audio.AddComponent<AudioSource>().clip= audioClip;
audio.GetComponent<AudioSource>().Play();
//9.资源释放
handle0.Release();
package.UnloadUnusedAssets();
}
else
{
//更新失败
Debug.LogError(operation.Error);
}
}
else
{
//更新失败
Debug.LogError(operation.Error);
}
}
6.YooAsset测试
无论是通过增量构建还是强制构建,都会生成一个以Build Version命名的文件夹,我们把这个文件夹统称为补丁包。补丁包里包含了游戏运行需要的所有资源,我们可以无脑的将补丁包内容覆盖到CDN目录下。
Host Play Mode下,YooAsset资源加载顺序是:先检查StreamingAsset目录,再检查同Library目录的的Yoo缓存目录(_data),最后才Host服务器下载。
Offline Play Mode下:检查StreamingAsset目录。