【unity游戏开发——热更新】YooAsset+HybridCLR代码热更新实践
《Unity热更新实践:YooAsset与HybridCLR整合方案》 摘要:本文详细介绍了Unity热更新方案中YooAsset与HybridCLR的整合实践。通过YooAsset管理资源加载与更新,配合HybridCLR实现代码热更新。实践内容包括:1)使用YooAsset加载热更新程序集;2)通过反射调用热更新代码;3)特殊处理编辑器模式与打包模式的程序集加载差异;4)完整的热更新资源打包流
注意
:考虑到热更新的内容比较多,我将热更新的内容分开,并全部整合放在【unity游戏开发——热更新】专栏里,感兴趣的小伙伴可以前往逐一查看学习。
文章目录
前置知识
实践
YooAsset
和HybridCLR
的基础使用的知识就不重复讲解了,还不懂的小伙伴可以点击前面前置知识前往了解。
1、加载热更新程序集
HybridCLR是原生运行时实现,因此调用Assembly Assembly.Load(byte[])即可加载热更新程序集。
创建Assets/LoadDll.cs脚本
using System;
using System.Linq;
using System.Reflection;
using UnityEngine;
using YooAsset;
public class LoadDll : MonoBehaviour
{
void Start()
{
#if !UNITY_EDITOR
// 打包后及非编辑器模式下
// 使用YooAsset
var package = YooAssets.GetPackage("DefaultPackage");
//HotUpdate.dll.bytes
AssetHandle handle = package.LoadAssetSync<TextAsset>("Assets/Res/HotDll/HotUpdate.dll.bytes");
TextAsset text = handle.AssetObject as TextAsset;
Assembly hotUpdateAss = Assembly.Load(text.bytes);
#else
// Editor编辑器模式下,HotUpdate.dll.bytes已经被自动加载,不需要加载,重复加载反而会出问题。
// Editor下无需加载,直接查找获得HotUpdate程序集
Assembly hotUpdateAss = System.AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "HotUpdate");
#endif
}
}
2、调用热更新代码
显然,主工程不能直接引用热更新代码。有多种方式可以从主工程调用热更新程序集中的代码,这里通过反射来调用热更新代码。
在LoadDll.Start函数后面添加反射调用代码,如下:
void Start()
{
//...
//得到Hello类调用函数
Type type = hotUpdateAss.GetType("Hello");
type.GetMethod("Run").Invoke(null, null);
}
3、在main场景中创建一个GameObject对象,挂载LoadDll脚本。
4、打包生成HotUpdate.dll
运行菜单 HybridCLR/Generate/All
进行必要的生成操作。这一步不可遗漏!!!
将{proj}/HybridCLRData/HotUpdateDlls/StandaloneWindows64(MacOS下为StandaloneMacXxx)
目录下的HotUpdate.dll
复制到Assets/Res/HotDll/HotUpdate.dll.bytes
,注意,要加.bytes后缀!!!这样才能被加载!
5、YooAsset资源中加入HotUpdate.dll
然后打一次AB包
将打包出来的Version文件和Bytes文件放入到StreamingAssets/yoo/DefaultPackage
下
6、生成的AB包资源全部复制放在本地资源服务器,启动服务器
7、修改默认场景MyYooAssetTest脚本的UpdateDone函数中,书写加载Main场景的代码
#region 热更新结束回调
private async void UpdateDone()
{
// 场景加载
string scenePath = "Assets/Res/Scene/Main.unity";
var sceneMode = LoadSceneMode.Single;
var physicsMode = LocalPhysicsMode.None;
bool suspendLoad = false;
SceneHandle handle = _package.LoadSceneAsync(scenePath, sceneMode, physicsMode, suspendLoad);
await handle.Task;
Debug.Log("场景名字是:" + handle.SceneName);
}
#endregion
完整代码
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using YooAsset;
/// <summary>
/// YooAsset资源系统测试脚本
/// </summary>
public class MyYooAssetTest : MonoBehaviour
{
string hostServerIP = "http://127.0.0.1";//服务器地址
string appVersion = "v1.0"; //版本号
public EPlayMode PlayMode = EPlayMode.HostPlayMode;//资源系统运行模式
public string packageName = "DefaultPackage"; //默认包名
private ResourcePackage _package = null; //资源包对象
public int downloadingMaxNum = 10;//最大下载数量
public int filedTryAgain = 3;//失败重试次数
private ResourceDownloaderOperation _downloader;//下载器
private UpdatePackageManifestOperation _operationManifest;//更新清单
void Awake()
{
Debug.Log($"资源系统运行模式:{PlayMode}");
Application.targetFrameRate = 60;//设置帧率
Application.runInBackground = true;//设置后台运行
DontDestroyOnLoad(gameObject);//确保该对象不会在场景切换时销毁
}
IEnumerator Start()
{
//1.初始化YooAsset资源系统
YooAssets.Initialize();
//2.初始化资源包
yield return StartCoroutine(InitPackage());
//3.获取资源版本
yield return StartCoroutine(UpdatePackageVersion());
//4.获取文件清单
yield return StartCoroutine(UpdateManifest());
//5.创建资源下载器
CreateDownloader();
//5.开始下载资源文件
yield return StartCoroutine(BeginDownload());
//6.清理未使用的缓存文件
ClearFiles();
}
#region 初始化资源包
private IEnumerator InitPackage()
{
// 获取或创建资源包对象
_package = YooAssets.TryGetPackage(packageName);
if (_package == null)
_package = YooAssets.CreatePackage(packageName);
// 编辑器下的模拟模式
InitializationOperation initializationOperation = null;
if (PlayMode == EPlayMode.EditorSimulateMode)
{
var buildResult = EditorSimulateModeHelper.SimulateBuild(packageName);
var packageRoot = buildResult.PackageRootDirectory;
var createParameters = new EditorSimulateModeParameters();
createParameters.EditorFileSystemParameters = FileSystemParameters.CreateDefaultEditorFileSystemParameters(packageRoot);
initializationOperation = _package.InitializeAsync(createParameters);
}
// 单机运行模式
if (PlayMode == EPlayMode.OfflinePlayMode)
{
var createParameters = new OfflinePlayModeParameters();
createParameters.BuildinFileSystemParameters = FileSystemParameters.CreateDefaultBuildinFileSystemParameters();
initializationOperation = _package.InitializeAsync(createParameters);
}
// 联机运行模式
if (PlayMode == EPlayMode.HostPlayMode)
{
//创建远端服务实例,用于资源请求
string defaultHostServer = GetHostServerURL();
string fallbackHostServer = GetHostServerURL();
IRemoteServices remoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);
// 创建联机模式参数,并设置内置及缓存文件系统参数
// HostPlayModeParameters createParameters = new HostPlayModeParameters
// {
// //创建内置文件系统参数
// BuildinFileSystemParameters = FileSystemParameters.CreateDefaultBuildinFileSystemParameters(),
// //创建缓存系统参数
// CacheFileSystemParameters = FileSystemParameters.CreateDefaultCacheFileSystemParameters(remoteServices)
// };
// 创建联机模式参数,并设置内置及缓存文件系统参数
HostPlayModeParameters createParameters = new HostPlayModeParameters
{
//创建内置文件系统参数
BuildinFileSystemParameters = FileSystemParameters.CreateDefaultBuildinFileSystemParameters(),
//创建缓存系统参数
CacheFileSystemParameters = FileSystemParameters.CreateDefaultCacheFileSystemParameters(remoteServices,new FileOffsetDecryption())
};
//执行异步初始化
initializationOperation = _package.InitializeAsync(createParameters);
}
// WebGL运行模式
if (PlayMode == EPlayMode.WebPlayMode)
{
#if UNITY_WEBGL && WEIXINMINIGAME && !UNITY_EDITOR
var createParameters = new WebPlayModeParameters();
string defaultHostServer = GetHostServerURL();
string fallbackHostServer = GetHostServerURL();
string packageRoot = $"{WeChatWASM.WX.env.USER_DATA_PATH}/__GAME_FILE_CACHE"; //注意:如果有子目录,请修改此处!
IRemoteServices remoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);
createParameters.WebServerFileSystemParameters = WechatFileSystemCreater.CreateFileSystemParameters(packageRoot, remoteServices);
initializationOperation = _package.InitializeAsync(createParameters);
#else
var createParameters = new WebPlayModeParameters();
createParameters.WebServerFileSystemParameters = FileSystemParameters.CreateDefaultWebServerFileSystemParameters();
initializationOperation = _package.InitializeAsync(createParameters);
#endif
}
yield return initializationOperation;
// 如果初始化失败弹出提示界面
if (initializationOperation.Status != EOperationStatus.Succeed)
{
Debug.LogWarning($"{initializationOperation.Error}");
PatchEventDefine.InitializeFailed.SendEventMessage();
}
else
{
Debug.Log("初始化成功-------------------------");
}
}
/// <summary>
/// 获取资源服务器地址
/// </summary>
private string GetHostServerURL()
{
#if UNITY_EDITOR
if (UnityEditor.EditorUserBuildSettings.activeBuildTarget == UnityEditor.BuildTarget.Android)
return $"{hostServerIP}/CDN/Android/{appVersion}";
else if (UnityEditor.EditorUserBuildSettings.activeBuildTarget == UnityEditor.BuildTarget.iOS)
return $"{hostServerIP}/CDN/IPhone/{appVersion}";
else if (UnityEditor.EditorUserBuildSettings.activeBuildTarget == UnityEditor.BuildTarget.WebGL)
return $"{hostServerIP}/CDN/WebGL/{appVersion}";
else
return $"{hostServerIP}/CDN/PC/{appVersion}";
#else
if (Application.platform == RuntimePlatform.Android)
return $"{hostServerIP}/CDN/Android/{appVersion}";
else if (Application.platform == RuntimePlatform.IPhonePlayer)
return $"{hostServerIP}/CDN/IPhone/{appVersion}";
else if (Application.platform == RuntimePlatform.WebGLPlayer)
return $"{hostServerIP}/CDN/WebGL/{appVersion}";
else
return $"{hostServerIP}/CDN/PC/{appVersion}";
#endif
}
/// <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}";
}
}
#endregion
#region 获取资源版本
private IEnumerator UpdatePackageVersion()
{
// 发起异步版本请求
RequestPackageVersionOperation operation = _package.RequestPackageVersionAsync();
yield return operation;
// 处理版本请求结果
if (operation.Status != EOperationStatus.Succeed)
{
Debug.LogWarning(operation.Error);
}
else
{
Debug.Log($"请求的版本: {operation.PackageVersion}");
appVersion = operation.PackageVersion;
}
}
#endregion
#region 获取文件清单
private IEnumerator UpdateManifest()
{
_operationManifest = _package.UpdatePackageManifestAsync(appVersion);
yield return _operationManifest;
// 处理文件清单结果
if (_operationManifest.Status != EOperationStatus.Succeed)
{
Debug.LogWarning(_operationManifest.Error);
PatchEventDefine.PackageManifestUpdateFailed.SendEventMessage();
yield break;
}
else
{
Debug.Log("更新资源清单成功-------------------");
}
}
#endregion
#region 创建资源下载器
void CreateDownloader()
{
_downloader = _package.CreateResourceDownloader(downloadingMaxNum, filedTryAgain);
if (_downloader.TotalDownloadCount == 0)
{
Debug.Log("没有需要更新的文件");
UpdateDone();
}
else
{
// 发现新更新文件后,挂起流程系统
// 注意:开发者需要在下载前检测磁盘空间不足
int count = _downloader.TotalDownloadCount;
long bytes = _downloader.TotalDownloadBytes;
Debug.Log($"需要更新{count}个文件, 大小是{bytes / 1024 / 1024}MB");
}
}
#endregion
#region 开始下载资源文件
private IEnumerator BeginDownload()
{
_downloader.DownloadErrorCallback = DownloadErrorCallback;// 单个文件下载失败
_downloader.DownloadUpdateCallback = DownloadUpdateCallback;// 下载进度更新
_downloader.BeginDownload();//开始下载
yield return _downloader;
// 检测下载结果
if (_downloader.Status != EOperationStatus.Succeed)
{
Debug.LogWarning(_operationManifest.Error);
yield break;
}
else
{
Debug.Log("下载成功-------------------");
}
}
// 单个文件下载失败
public static void DownloadErrorCallback(DownloadErrorData errorData)
{
string fileName = errorData.FileName;
string errorInfo = errorData.ErrorInfo;
Debug.Log($"下载失败, 文件名: {fileName}, 错误信息: {errorInfo}");
}
// 下载进度更新
public static void DownloadUpdateCallback(DownloadUpdateData updateData)
{
int totalDownloadCount = updateData.TotalDownloadCount;
int currentDownloadCount = updateData.CurrentDownloadCount;
long totalDownloadSizeBytes = updateData.TotalDownloadBytes;
long currentDownloadSizeBytes = updateData.CurrentDownloadBytes;
Debug.Log($"下载进度: {currentDownloadCount}/{totalDownloadCount}, " +
$"{currentDownloadSizeBytes / 1024}KB/{totalDownloadSizeBytes / 1024}KB");
}
#endregion
#region 清理未使用的缓存文件
void ClearFiles()
{
var operationClear = _package.ClearCacheFilesAsync(EFileClearMode.ClearUnusedBundleFiles);// 清理未使用的文件
operationClear.Completed += Operation_Completed;// 添加清理完成回调
}
//文件清理完成
private void Operation_Completed(AsyncOperationBase obj)
{
UpdateDone();
}
#endregion
#region 热更新结束回调
private async void UpdateDone()
{
// 场景加载
string scenePath = "Assets/Res/Scene/Main.unity";
var sceneMode = LoadSceneMode.Single;
var physicsMode = LocalPhysicsMode.None;
bool suspendLoad = false;
SceneHandle handle = _package.LoadSceneAsync(scenePath, sceneMode, physicsMode, suspendLoad);
await handle.Task;
Debug.Log("场景名字是:" + handle.SceneName);
}
#endregion
}
8、打包运行
打开Build Settings对话框,点击Build And Run,打包并且运行热更新示例工程。可以看到Hello, HybridCLR
不要忘记了,打包要配置好场景信息。顺序记得不要错了,确保初始化YooAsset在第一个场景执行
‘
并开启允许从http下载
效果
9、测试热更新
修改Assets/HotUpdate/Hello.cs
的Run函数
public class Hello
{
public static void Run()
{
Debug.Log("代码热更新成功");
}
}
运行菜单命令HybridCLR/CompileDll/ActiveBulidTarget
重新编译热更新代码。
将{proj}/HybridCLRData/HotUpdateDlls/StandaloneWindows64(MacOS下为StandaloneMacXxx)
目录下的HotUpdate.dll
复制替换掉工程中已经存在的HotUpdate.dll.bytes
,也要加上后缀bytes
检查YooAsset资源引用有没有丢失
重新用YooAsset打AB包,全部放到资源服务器上
双击重新运行之前打包好的exe运行包。
发现打印语句变了,实现代码热更新!!
专栏推荐
完结
好了,我是向宇
,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,你的每一次支持
都是我不断创作的最大动力。当然如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
更多推荐
所有评论(0)