当前位置: 首页 > backend >正文

HybridCLR热更新实例项目及改造流程

热更新项目简单模板

示例项目仓库,结构简单流程易懂

https://github.com/Kerzhrua/HybridCLR_Addressable/tree/MyTest

重要流程代码:

using HybridCLR;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using GamePlay;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using Object = UnityEngine.Object;namespace AOT
{/// <summary>/// 用于启动游戏,执行版本检查,版本更新,加载程序集,进入游戏/// 错误处理是用的打印日志,正式上线的话需要一个错误处理系统来给玩家显示错误信息/// </summary>public class GameLauncher : MonoBehaviour{public const string META_DATA_DLL_SEPARATOR = "!";  //  分割符号  区分元数据dllprivate readonly List<string> _gamePlayDependencyDlls = new List<string>() { };  //  GamePlay程序集依赖的热更程序集,这些程序集要先于gameplay程序集加载,需要手动填写#region 编辑器赋值public UIVersionUpdate _versionUpdateUI;  //  进度条ui#endregion#region 缓存private byte[] _dllBytes;  //  dll文件的字节数组private AddressableAssetManager addressableAssetManager = new AddressableAssetManager();  //  资源管理器private Coroutine _launchCoroutine;  //  启动器协程private Dictionary<string, Assembly> _allHotUpdateAssemblies = new();  //  热更新程序集  程序集名称_程序集#endregionprivate bool enableHybridCLR = !Application.isEditor;  //  是否开启HybridCLR,在编辑器下自动关闭private void Start(){_launchCoroutine = StartCoroutine(Launch());DontDestroyOnLoad(this);}private IEnumerator Launch(){Debug.Log("检查更新");yield return CheckUpdate();  //  检查更新Debug.Log("加载程序集");yield return LoadAssemblies();  //  加载程序集Debug.Log("跳转场景");yield return EnterGame();  //  跳转场景Debug.Log("创建物体还原脚本");var GameTestOp = Addressables.LoadAssetAsync<GameObject>("Assets/Prefabs/GameTest.prefab");yield return new WaitUntil(() => { return GameTestOp.Status == AsyncOperationStatus.Succeeded; });Instantiate(GameTestOp.Result);}/// <summary>/// 检查版本更新/// </summary>/// <returns></returns>private IEnumerator CheckUpdate(){yield return addressableAssetManager.CheckUpdate();if (addressableAssetManager.IsHasContentToDownload()){Debug.Log($"检测到存在内容更新,开始更新内容");Debug.Log($"展示进度ui追踪下载进程");yield return OpenVersionUpdateUI();Debug.Log($"开始下载新内容");yield return Download();}}//打开版本更新UIprivate IEnumerator OpenVersionUpdateUI(){_versionUpdateUI = FindObjectOfType<UIVersionUpdate>(true);if (_versionUpdateUI == null){Debug.LogError("cant find UIVersionUpdate");return null;}_versionUpdateUI.gameObject.SetActive(true);_versionUpdateUI.GetDownloadProgress = addressableAssetManager.GetDownloadProgress;return null;}//下载资源private IEnumerator Download(){yield return addressableAssetManager.DownloadAssets();_versionUpdateUI.GetDownloadProgress = null;}/// <summary>/// 加载程序集/// </summary>/// <returns></returns>private IEnumerator LoadAssemblies(){Debug.Log("为AOT补充元数据");yield return LoadMetadataForAOTAssemblies();Debug.Log("加载游戏程序集依赖的dll");yield return LoadGamePlayDependencyAssemblies();Debug.Log("加载游戏程序集");yield return LoadGamePlayAssemblies();Debug.Log("重新加载catalog");yield return addressableAssetManager.ReloadAddressableCatalog();}/// <summary>/// 补充元数据/// </summary>/// <returns></returns>private IEnumerator LoadMetadataForAOTAssemblies(){var mataDataDlls = GetMetaDataDllToLoad();foreach (var aotDllName in mataDataDlls){if(string.IsNullOrEmpty(aotDllName)) continue;var path = $"Assets/HotUpdateDlls/MetaDataDll/{aotDllName}.bytes";yield return ReadDllBytes(path);if (_dllBytes != null){var state = HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly(_dllBytes, HomologousImageMode.SuperSet);Debug.Log($"加载元数据结果:{aotDllName}. 状态码:{state}");}}}/// <summary>/// 获得要加载的元数据dll 从生成的AOTGenericReferences获取/// 文件从HybridCLRData\AssembliesPostIl2CppStrip\{platform}下获取  注意添加.bytes后缀/// </summary>/// <returns></returns>private string[] GetMetaDataDllToLoad(){return new string[]{"System.Core.dll","System.dll","Unity.Addressables.dll","Unity.ResourceManager.dll","UnityEngine.CoreModule.dll","mscorlib.dll",};}//加载GamePlay依赖的第三方程序集private IEnumerator LoadGamePlayDependencyAssemblies(){foreach (var dllName in _gamePlayDependencyDlls){yield return LoadSingleHotUpdateAssembly(dllName);}}/// <summary>/// 加载GamePlay程序集/// 文件从HybridCLRData\HotUpdateDlls\{platform}获取  注意添加.bytes后缀/// </summary>/// <returns></returns>private IEnumerator LoadGamePlayAssemblies(){yield return LoadSingleHotUpdateAssembly("GamePlay.dll");}private IEnumerator EnterGame(){yield return addressableAssetManager.ChangeScene("Assets/Scenes/GameScenes/StartScene.unity");}#region 工具方法/// <summary>/// 读取动态链接库字节/// </summary>/// <param name="path"></param>private IEnumerator ReadDllBytes(string path){Debug.Log("读取dll数据:" + path);TextAsset dllText = null;yield return addressableAssetManager.LoadAssetCoroutine<TextAsset>(path, (text) =>{dllText = text;});if (dllText == null){Debug.LogError($"读取dll数据失败,路径:{path}");_dllBytes = null;}else{Debug.Log("读取dll数据成功:" + path);_dllBytes = dllText.bytes;}addressableAssetManager.UnloadAsset(dllText);}/// <summary>/// 加载程序集/// </summary>/// <param name="dllName"></param>/// <returns></returns>private IEnumerator LoadSingleHotUpdateAssembly(string dllName){var path = $"Assets/HotUpdateDlls/HotUpdateDll/{dllName}.bytes";yield return ReadDllBytes(path);if (_dllBytes != null){var assembly = Assembly.Load(_dllBytes);_allHotUpdateAssemblies.Add(assembly.FullName, assembly);Debug.Log($"加载程序集成功:{assembly.FullName}");}}/// <summary>/// 获取程序集/// </summary>/// <param name="assemblyName"></param>/// <returns></returns>private Assembly GetAssembly(string assemblyName){assemblyName = assemblyName.Replace(".dll", "");IEnumerable<Assembly> allAssemblies =enableHybridCLR ? _allHotUpdateAssemblies.Values : AppDomain.CurrentDomain.GetAssemblies();return allAssemblies.First(assembly => assembly.FullName.Contains(assemblyName));}#endregion}
}

 更新资源代码:

 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;
using UnityEngine.Serialization;namespace AOT
{/// <summary>/// Addressable的资源管理器(只用于启动游戏时更新)/// </summary>public class AddressableAssetManager{//记录在playerPres里的需要下载的catalogs的ID  记录这个数据是为了当下载过程被打断,下次登录可以根据这个信息继续下载const string DOWNLOAD_CATALOGS_ID = "DownloadCatalogIDs";#region 缓存private List<object> _KeysNeedToDownload = new();  //  需要下载的文件keys,key是用来做资源定位的,也就是资源的唯一标识//此对象里保存了需要下载的catalog,每次获取新的catalog会将此对象保存到手机上,如果在下载的过程中关闭了游戏,下次打开还能拿到catalog继续下载private DownloadContent _downloadContent = new();private AsyncOperationHandle _downloadOP;  //  异步操作处理对象  用于异步下载#endregion[Serializable]private class DownloadContent  //  需要下载的内容{public List<string> catalogIDs = new();  //  需要更新的catalog的id列表}/// <summary>/// 是否存在需要下载的更新内容/// </summary>/// <returns></returns>public bool IsHasContentToDownload(){if (_downloadContent != null && _downloadContent.catalogIDs != null &&_downloadContent.catalogIDs.Count > 0){return true;}return false;}/// <summary>/// 加载资源/// </summary>/// <param name="path"></param>/// <typeparam name="T"></typeparam>/// <returns></returns>public IEnumerator LoadAssetCoroutine<T>(string path, System.Action<T> onComplete){var op = Addressables.LoadAssetAsync<T>(path);if (!op.IsValid()){Debug.LogError($"Invalid Addressables path: {path}");onComplete?.Invoke(default);yield break;}// 等待异步加载完成yield return op;if (op.Status == AsyncOperationStatus.Succeeded){onComplete?.Invoke(op.Result);}else{Debug.LogError($"Failed to load asset at path: {path}, Error: {op.OperationException}");onComplete?.Invoke(default);}}/// <summary>/// 释放加载的资源/// </summary>/// <param name="asset"></param>public void UnloadAsset(UnityEngine.Object asset){if (asset != null)Addressables.Release(asset);}/// <summary>/// 检查addressable更新  更新catalog文件到最新版本,找出所有需要更新的资源唯一id/// </summary>/// <returns></returns>public IEnumerator CheckUpdate(){//  检查catalog需要更新的内容var checkUpdateOP = Addressables.CheckForCatalogUpdates(false);yield return checkUpdateOP;  //  等待检查完成if (checkUpdateOP.Status == AsyncOperationStatus.Succeeded){#region 确保下载内容对象_downloadContent的下载内容正确性,主要考虑下载中断问题_downloadContent.catalogIDs = checkUpdateOP.Result;  //  赋值到下载内容对象  这是需要更新的catalog的id列表,目前不知道怎么出现多个catalogif (IsHasContentToDownload())  //  存在需要更新的内容{Debug.Log("检测到存在新内容");//说明服务器上有新的资源,记录要下载的catalog值在playerprefs中,如果下载的过程中被打断,下次打开游戏使用该值还能继续下载var jsonStr = JsonUtility.ToJson(_downloadContent);PlayerPrefs.SetString(DOWNLOAD_CATALOGS_ID, jsonStr);PlayerPrefs.Save();}else  //  不存在需要更新的内容  但这只是catalog与本地一致,并不代表资源更新完毕{if (PlayerPrefs.HasKey(DOWNLOAD_CATALOGS_ID))  // 如果上次下载内容中断 {//上一次的更新还没下载完Debug.Log("继续上一次的下载更新");var jsonStr = PlayerPrefs.GetString(DOWNLOAD_CATALOGS_ID);  //  获取未下载完成的内容JsonUtility.FromJsonOverwrite(jsonStr, _downloadContent);  //  覆盖到下载内容对象}else{//没有需要下载的内容Debug.Log("不存在需要下载的内容");}}#endregionif (IsHasContentToDownload())  //  有内容需要下载{Debug.Log("资源更新列表:" + JsonUtility.ToJson(_downloadContent));Debug.Log("根据更新列表进行下载更新");//  更新catalog,如果不提供catalog的id列表,会检查所有已加载的catalog以获取更新//  如果只调用Addressables.UpdateCatalogs,它会返回所有资源的索引,但是如果只更新对应的catalogId,就只返回那些需要更新的资源索引,以此避免全部重新下载AsyncOperationHandle<List<IResourceLocator>> updateCatalogOP = Addressables.UpdateCatalogs(_downloadContent.catalogIDs, false);//  等待更新完成  这样catalog已经确保是最新的了yield return updateCatalogOP;  if (updateCatalogOP.Status == AsyncOperationStatus.Succeeded){//  更新所有需要更新的资源定位key_KeysNeedToDownload.Clear();foreach (var resourceLocator in updateCatalogOP.Result){_KeysNeedToDownload.AddRange(resourceLocator.Keys);}Debug.Log("需要更新的资源key:" + JsonUtility.ToJson(_KeysNeedToDownload));}else{Debug.LogError($"更新catalog失败:{updateCatalogOP.OperationException.Message}");}Addressables.Release(updateCatalogOP);}}else{Debug.LogError($"检查catalog更新失败:{checkUpdateOP.OperationException.Message}");}Addressables.Release(checkUpdateOP);//更新完catalog后重新加载一下Addressable的Catalogyield return ReloadAddressableCatalog();}/// <summary>/// 下载资源  根据需要更新资源的唯一id数组,计算需要下载的大小,/// </summary>/// <returns></returns>public IEnumerator DownloadAssets(){var downloadSizeOp = Addressables.GetDownloadSizeAsync((IEnumerable)_KeysNeedToDownload);yield return downloadSizeOp;Debug.Log($"download size:{downloadSizeOp.Result / (1024f * 1024f)}MB");if (downloadSizeOp.Result > 0)  //  如果有需要下载的内容{Addressables.Release(downloadSizeOp);/*  开始下载用于合并请求结果的选项。如果键(A,B)映射到结果([1,2,4],[3,4,5])UseFirst(或None)获取第一个键的结果。--[1,2,4]Union获取每个键的结果,并收集与任何键匹配的项。--[1,2,3,4,5]Intersection获取每个关键字的结果,并收集与每个关键字匹配的项。--[4]*/_downloadOP = Addressables.DownloadDependenciesAsync((IEnumerable)_KeysNeedToDownload, Addressables.MergeMode.Union, false);yield return _downloadOP;  //  等待下载完成  全部的if (_downloadOP.Status == AsyncOperationStatus.Succeeded)Debug.Log($"download finish!");elseDebug.LogError($"Download Failed! exception:{_downloadOP.OperationException.Message} \r\n {_downloadOP.OperationException.StackTrace}");Addressables.Release(_downloadOP);}//清除需要下载的内容Debug.Log($"delete key:{DOWNLOAD_CATALOGS_ID}");PlayerPrefs.DeleteKey(DOWNLOAD_CATALOGS_ID);}/// <summary>/// 获取下载进程/// </summary>/// <returns></returns>public UIVersionUpdate.DownloadInfo GetDownloadProgress(){if (!_downloadOP.IsValid())return default;var downloadStatus = _downloadOP.GetDownloadStatus();return new UIVersionUpdate.DownloadInfo(downloadStatus.Percent, downloadStatus.DownloadedBytes, downloadStatus.TotalBytes);}public IEnumerator ChangeScene(string sceneName){var op = Addressables.LoadSceneAsync(sceneName, LoadSceneMode.Single);//  等待完成yield return op;if (op.Status != AsyncOperationStatus.Succeeded){Debug.LogError($"加载场景失败:{op.OperationException.Message} \r\n {op.OperationException.StackTrace}");}}#region Other/// <summary>/// 重新加载catalog/// Addressable初始化时热更新代码所对应的ScriptableObject的类型会被识别为System.Object,需要在热更新dll加载完后重新加载一下Addressable的Catalog/// https://hybridclr.doc.code-philosophy.com/docs/help/commonerrors#%E4%BD%BF%E7%94%A8addressable%E8%BF%9B%E8%A1%8C%E7%83%AD%E6%9B%B4%E6%96%B0%E6%97%B6%E5%8A%A0%E8%BD%BD%E8%B5%84%E6%BA%90%E5%87%BA%E7%8E%B0-unityengineaddressableassetsinvlidkeyexception-exception-of-type-unityengineaddressableassetsinvalidkeyexception-was-thrown-no-asset-found-with-for-key-xxxx-%E5%BC%82%E5%B8%B8/// </summary>/// <returns></returns>public IEnumerator ReloadAddressableCatalog(){var op = Addressables.LoadContentCatalogAsync($"{Addressables.RuntimePath}/catalog.json");//  等待加载完成yield return op;if (op.Status != AsyncOperationStatus.Succeeded){Debug.LogError($"加载catalog失败:{op.OperationException.Message} \r\n {op.OperationException.StackTrace}");}}#endregion}
}

 如何改造一个现有项目支持热更?

 将游戏的热更逻辑都放在一个程序集中,挂上需要的引用,处理好无报错

 创建一个初始Gamelauncher场景,仅包含更新资源和加载dll相关的代码,这些代码在Assembly-CSharp中

 当流程处理完即dll文件加载完成后跳转场景到游戏加载场景Load,注意在Load场景中什么都不要放,当场景加载完成后通过实例化预制体的方式来还原脚本,即把项目原本的Load场景的脚本全部放到一个预制体里加载还原。

 游戏加载完成后根据需求是否还要切换场景,如还要切到Main场景,切换流程和切换到Load场景一致,Main场景中也什么都没有,在加载完成后再实例化预制体以还原脚本。

 

 当这些处理完成后,可以开始remote打包流程。

 

热更的Generate All执行流程如下:

// 构建目标平台

BuildTarget target = EditorUserBuildSettings.activeBuildTarget;

// 编译目标平台的dll文件

CompileDllCommand.CompileDll(target);

// 生成必要的 IL2CPP 头文件和定义,扩展 IL2CPP 以支持解释执行

Il2CppDefGeneratorCommand.GenerateIl2CppDef();

// 生成 Link.xml 文件,避免热更新代码因裁剪而无法运行

LinkGeneratorCommand.GenerateLinkXml(target);

// 生成裁剪后的AOT dll 在打包过程中,Unity 会对 AOT 程序集进行裁剪(Strip),移除未使用的代码。AOTDlls 会保存这些裁剪后的 DLL,用于补充元数据(如泛型实例化)

StripAOTDllCommand.GenerateStripedAOTDlls(target);

// 桥接函数生成依赖于AOT dll,必须保证已经build过,生成AOT dll

// 在 AOT 代码和解释执行代码之间建立桥接,使得热更新代码和AOT代码可以互相调用

MethodBridgeGeneratorCommand.GenerateMethodBridge(target);

// 在 AOT 环境下,native 回调 C# 委托需要固定的函数指针,而热更新代码的动态性会导致回调失败。

// 故要为热更新代码中的委托(Delegate)生成反向 P/Invoke 包装器,使得 C# 委托可以被 native 代码(如 Unity 引擎或 C++ 插件)回调

ReversePInvokeWrapperGeneratorCommand.GenerateReversePInvokeWrapper(target);

// 生成 AOT 泛型引用,以解决 AOT 泛型限制问题,让热更新代码可以使用未在 AOT 中实例化的泛型类或方法(如 List<MyHotUpdateType>)

AOTReferenceGeneratorCommand.GenerateAOTGenericReference(target);

在webgl平台下打包有额外的步骤

https://hybridclr.doc.code-philosophy.com/docs/basic/buildwebgl

以管理员权限打开命令行窗口,这个操作不同操作系统版本不一样,请酌情处理。在Win11下为在开始菜单上右键,选中终端管理员菜单项。

运行 cd /d {editor_install_dir}/Editor/Data/il2cpp, 切换目录到安装目录的il2cpp目录

运行ren libil2cpp libil2cpp-origin 将原始libil2cpp改名为libil2cpp-origin

运行 mklink /D libil2cpp "{project}/HybridCLRData/LocalIl2CppData-{platform}/il2cpp/libil2cpp" 建立Editor目录的libil2cpp到本地libil2cpp目录的符号引用

示例:mklink /D libil2cpp "E:\Unity\UnityProjects\DiveVsSlimeLFS/HybridCLRData/LocalIl2CppData-WindowsEditor/il2cpp/libil2cpp"

注意!!!在热更下RuntimeInitializeOnLoadMethod未被支持,故如果想实现类似效果可获取程序集通过反射实现。

http://www.xdnf.cn/news/14774.html

相关文章:

  • 现代 JavaScript (ES6+) 入门到实战(五):告别回调地狱,Promise 完全入门
  • 免费SSL证书一键申请与自动续期
  • STM32——HAL库总结
  • 【AGI】Qwen VLo:多模态AI的范式重构与AGI演进关键里程碑
  • mac触摸板设置右键
  • 【HuggingFace】模型下载至本地访问
  • 基于Pandas和FineBI的昆明职位数据分析与可视化实现(三)- 职位数据统计分析
  • 条件概率:不确定性决策的基石
  • C#写破解rar文件密码例程
  • 【硬核数学】10. “价值标尺”-损失函数:信息论如何设计深度学习的损失函数《从零构建机器学习、深度学习到LLM的数学认知》
  • Android大图加载优化:BitmapRegionDecoder深度解析与实战
  • IDE/IoT/实践小熊派LiteOS工程配置、编译、烧录、调试(基于 bearpi-iot_std_liteos 源码)
  • 马斯克的 Neuralink:当意念突破肉体的边界,未来已来
  • 同步日志系统深度解析【链式调用】【宏定义】【固定缓冲区】【线程局部存储】【RAII】
  • 《汇编语言:基于X86处理器》第5章 过程(2)
  • C# 委托(为委托添加方法和从委托移除方法)
  • 暑假复习篇之类与对象
  • gantt-task-react的改造使用
  • 源码运行效果图(六)
  • cocos creator 3.8 - 精品源码 - 六边形消消乐(六边形叠叠乐、六边形堆叠战士)
  • 《自动控制原理 》- 第 1 章 自动控制的基本原理与方式
  • 计算机操作系统(十七)内存管理
  • OpenCV图像噪点消除五大滤波方法
  • 能否仅用两台服务器实现集群的高可用性??
  • 创建套接字时和填充地址时指定类型的异同
  • 【LeetCode 热题 100】438. 找到字符串中所有字母异位词——(解法三)不定长滑动窗口+数组
  • 使用docker编译onlyoffice server 8.2.2 成功版 含踩坑记录
  • C++ STL深度剖析:Stack、queue、deque容器适配器核心接口
  • FDA IND审评流程及临床研究暂停要点
  • Ubuntu20.04离线安装Realtek b852无线网卡驱动