Unity热更新方案HybridCLR YooAsset教程:从零开始,C#开发热更保姆级指南

文章目录:

  • 一、前言
  • 二、创建空工程
  • 三、接入HybridCLR
  • 四、接入YooAsset
  • 五、搭建本地资源服务器Nginx
  • 六、实战
  • 七、最后
  • 八、后记
  • 一、前言

    unity热更有很多方案,各种lua热更,ILRuntime等,这里介绍的是YooAsset+HybridCLR的热更方案,HybridCLR负责更新c#代码,YooAsset负责更新资源。

    简单来说,流程就是将c#代码打成dll,然后把dll当做一个资源,用YooAsset热更dll资源之后,动态加载dll程序集,然后执行新逻辑

    HybridCLR相比其他代码热更方案而言,纯c#方便开发,更加符合开发者习惯,更新的代码执行效率也更好。

    YooAsset热更资源,主要是省去了自己亲自管理ab包,ab包的管理挺繁琐,AssetBundle坑也很多,而且YooAsset有下载器,不用自己手写网络下载,也不用自己记录资源,比对资源列表来判定需要热更什么资源。

    这里将从零开始,用一个空工程,展示YooAsset+HybridCLR热更的使用过程。

    二、创建空工程

    这里使用的版本是unity2022.3.17.f1c1

    三、接入HybridCLR

    unity必须添加了Windows Build Support(IL2CPP)或Mac Build Support(IL2CPP)模块

    安装IDE及相关编译环境
    Win
    需要安装visual studio 2019或更高版本。安装时至少要包含使用Unity的游戏开发和使用c++的游戏开发组件。
    安装git

    Mac
    要求MacOS版本 >= 12,xcode版本 >= 13,例如xcode 13.4.1, macos 12.4。
    安装 git

    添加ConsoleToScreen.cs脚本
    和热更无关,主要在屏幕上显示log,代码如下:

    using System.Collections.Generic;
    using UnityEngine;
    
    public class ConsoleToScreen : MonoBehaviour
    {
        const int maxLines = 50;
        const int maxLineLength = 120;
        private string _logStr = "";
    
        private readonly List<string> _lines = new();
    
        public int fontSize = 15;
    
        void OnEnable() { Application.logMessageReceived += Log; }
        void OnDisable() { Application.logMessageReceived -= Log; }
    
        public void Log(string logString, string stackTrace, LogType type)
        {
            foreach (var line in logString.Split('\n'))
            {
                if (line.Length <= maxLineLength)
                {
                    _lines.Add(line);
                    continue;
                }
                var lineCount = line.Length / maxLineLength + 1;
                for (int i = 0; i < lineCount; i++)
                {
                    if ((i + 1) * maxLineLength <= line.Length)
                    {
                        _lines.Add(line.Substring(i * maxLineLength, maxLineLength));
                    }
                    else
                    {
                        _lines.Add(line.Substring(i * maxLineLength, line.Length - i * maxLineLength));
                    }
                }
            }
            if (_lines.Count > maxLines)
            {
                _lines.RemoveRange(0, _lines.Count - maxLines);
            }
            // _lines.Add(stackTrace);
            _logStr = string.Join("\n", _lines);
        }
    
        void OnGUI()
        {
            GUI.matrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity,
                new Vector3(Screen.width / 1200.0f, Screen.height / 800.0f, 1.0f));
            GUI.Label(new Rect(10, 10, 800, 370), _logStr, new GUIStyle { fontSize = 10 });
        }
    }
    

    工程初始设置
    创建Main场景
    将ConsoleToScreen挂载一个创建的空物体上
    菜单栏 File/BuildSettings添加Main场景
    创建Assets/HotUpdate目录

    创建热更程序集
    在HotUpdate目录下 右键 Create/Assembly Definition,创建一个名为HotUpdate的程序集模块

    安装HybridCLR
    主菜单中点击Windows/Package Manager
    Add package from git URL…
    填入:https://gitee.com/focus-creative-games/hybridclr_unity.git
    如下:

    初始化HybridCLR
    打开菜单HybridCLR/Installer…, 点击Install按钮进行安装。 耐心等待30s左右,安装完成后会在最后打印 安装成功日志。


    配置HybridCLR
    打开菜单 HybridCLR/Settings, 在热更新Assembly Definitions配置项中添加HotUpdate程序集。

    设置PlayerSettings
    打开菜单Edit/Project Setting,在Player选项中,设置如下几个配置:
    Scripting Backend 切换为 IL2CPP。
    Api Compatability Level 切换为 .Net 4.x(Unity 2019-2020) 或.Net Framework(Unity 2021+)

    HybridCLR方面的操作告一段落,接下来接入YooAsset

    四、接入YooAsset

    安装YooAsset
    打开菜单Edit/Project Settings/Package Manager
    在Package Manager选项找那个,输入如下内容,点击save:

    (国际版)
    Name: package.openupm.com
    URL: https://package.openupm.com
    Scope(s): com.tuyoogame.yooasset

    (中国版)
    Name: package.openupm.cn
    URL: https://package.openupm.cn
    Scope(s): com.tuyoogame.yooasset
    (这个配置好像报错了,我用的上面国际版的地址)

    打开菜单Windows/Package Manager
    Packages选择My Registries,出现了YooAsset,点击Install安装。

    五、搭建本地资源服务器Nginx

    为了模拟热更流程,需要一个服务器作为热更资源的下载,我们可以在本机搭建一个资源服务器。我这里用的是Nginx。
    下载地址:https://nginx.org/en/download.html

    随便下一个,下载之后是个zip包,解压之后如下:

    注意:端口冲突时,更改端口:打开文件:conf-nginx.conf,修改第36行的listen,我改的是8084,自己随意
    然后运行nginx.exe即可

    现在打开网址http://127.0.0.1:8084如下:

    html文件夹下,就可以放需要热更的资源,比如我准备打包测试的是pc平台,我就在html文件夹下建了个TestProject文件夹,在里面再建个PC文件夹,TestProject是项目名,PC是平台名,用以放接下来要热更的资源

    六、实战

    创建热更目录
    之前的HotUpdate文件夹是为了放c#代码,用以生成程序集的,接下来创建的目录,是为了热更资源的(包括c#代码的dll资源,dll也是一种资源),创建如下目录:

    准备工作
    HotUpdate文件夹下创建InstantiateByAsset.cs,这是会被热更的代码:

    using UnityEngine;
    
    public class InstantiateByAsset : MonoBehaviour
    {
        void Start()
        {
            Debug.Log("原始代码");
        }
    }
    

    创建一个cube预制体,挂载InstantiateByAsset组件,放入MyAsset/Prefabs中

    收集热更资源

    创建Resources文件夹,在Resources文件夹内通过右键创建配置文件(Project窗体内右键 -> Create -> YooAsset -> Create Setting),将配置文件放在Resources文件夹下

    打开菜单YooAsset/AssetBundle Collector,点击ShowPackages,再点击+号,创建packages,再点击ShowPackages,只显示Groups就行,方便操作


    打开Enable Addressable
    添加code和prefab分组,具体设置如下:

    选项含义如下,其他含义,请看官网

    编写LoadDll.cs
    编写代码初始化YooAsset,从资源服加载热更资源,加载热更程序集,将LoadDll.cs挂在Main场景的Main Camera上。

    整个代码流程就是先初始化,然后下载热更资源,然后补充元数据,然后开始执行热更代码。
    关于补充元数据,通俗的理解,举个例子:

    由于Unity IL2CPP打包的代码裁剪,假设在打包时的代码,从来没有用过List泛型,打包时,List的元数据会被裁掉了。到玩家手机的APP上,根本不存在List的程序定义等关键信息。当玩家运行app时,在运行前预编译这个app的代码时,是不存在List的相关定义的,编译完成后,在运行时,热更逻辑中,使用了List,虽然这个热更的dll中有List的程序定义,但是已经过了编译阶段,现在是执行阶段,执行阶段是直接找预编译时的程序定义去进行实例化。所以在动态执行包含有List的代码时,由于编译阶段缺失了List的程序定义相关的元数据,无法对其进行实例化。所以在运行包含了List的热更代码时,先补齐编译阶段缺失的List元数据,之后才能正常运行和实例化热更中的包含了List的代码。

    常见的需要补充的元数据如:mscorlib.dll, System.dll, System.Core.dll

    详细信息原理请浏览官网

    注意!注意!注意!
    如果你用较新版的YooAsset插件,导致这个脚本中的各种类找不到,请翻到本文第八小节:后记,里面有适应最新版(到2024.12.21日,YooAsset2.2.7版)的LoadDll.cs,可以用那个代码,并更改上文中YooAssetSettings资源的位置,新位置在后记中也已标注。

    代码如下:

    using HybridCLR;
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using UnityEngine;
    using YooAsset;
    
    /// <summary>
    /// 脚本工作流程:
    /// 1.下载资源,用yooAsset资源框架进行下载
    ///    1.资源文件,ab包
    ///    2.热更新dll
    /// 2.给AOT DLL补充元素据,通过RuntimeApi.LoadMetadataForAOTAssembly
    /// 3.通过实例化prefab,运行热更代码
    /// </summary>
    public class LoadDll : MonoBehaviour
    {
        /// <summary>
        /// 资源系统运行模式
        /// </summary>
        public EPlayMode PlayMode = EPlayMode.HostPlayMode;
    
        private ResourcePackage _defaultPackage;
    
        void Start()
        {
            StartCoroutine(InitYooAssets(StartGame));
        }
    
        #region YooAsset初始化
    
        IEnumerator InitYooAssets(Action onDownloadComplete)
        {
            // 1.初始化资源系统
            YooAssets.Initialize();
    
            string packageName = "DefaultPackage";
            var package = YooAssets.TryGetPackage(packageName) ?? YooAssets.CreatePackage(packageName);
            YooAssets.SetDefaultPackage(package);
            if (PlayMode == EPlayMode.EditorSimulateMode)
            {
                //编辑器模拟模式
                var initParameters = new EditorSimulateModeParameters { SimulateManifestFilePath = EditorSimulateModeHelper.SimulateBuild(EDefaultBuildPipeline.BuiltinBuildPipeline, "DefaultPackage") };
                yield return package.InitializeAsync(initParameters);
            }
            else if (PlayMode == EPlayMode.HostPlayMode)
            {
                //联机运行模式
                string defaultHostServer = GetHostServerURL();
                string fallbackHostServer = GetHostServerURL();
                Debug.Log(defaultHostServer);
                var initParameters = new HostPlayModeParameters();
                initParameters.BuildinQueryServices = new GameQueryServices();
                // initParameters.DecryptionServices = new GameDecryptionServices();
                // initParameters.DeliveryQueryServices = new DefaultDeliveryQueryServices();
                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}");
                }
            }
    
    
            //2.获取资源版本
            var operation = package.UpdatePackageVersionAsync();
            yield return operation;
    
            if (operation.Status != EOperationStatus.Succeed)
            {
                //更新失败
                Debug.LogError(operation.Error);
                yield break;
            }
    
            string packageVersion = operation.PackageVersion;
            Debug.Log($"Updated package Version : {packageVersion}");
    
            //3.更新补丁清单
            // 更新成功后自动保存版本号,作为下次初始化的版本。
            // 也可以通过operation.SavePackageVersion()方法保存。
            var operation2 = package.UpdatePackageManifestAsync(packageVersion);
            yield return operation2;
    
            if (operation2.Status != EOperationStatus.Succeed)
            {
                //更新失败
                Debug.LogError(operation2.Error);
                yield break;
            }
    
            //4.下载补丁包
            yield return Download();
    
            //判断是否下载成功
            var assets = new List<string> { "HotUpdate.dll" }.Concat(AOTMetaAssemblyFiles);
            foreach (var asset in assets)
            {
                var handle = package.LoadAssetAsync<TextAsset>(asset);
                yield return handle;
                var assetObj = handle.AssetObject as TextAsset;
                s_assetDatas[asset] = assetObj;
                Debug.Log($"dll:{asset}   {assetObj == null}");
            }
    
            _defaultPackage = package;
            onDownloadComplete();
        }
        
        private string GetHostServerURL()
        {
            //模拟下载地址,8084为Nginx里面设置的端口号,项目名,平台名
            return "http://127.0.0.1:8084/TestProject/PC";
        }
    
        /// <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}";
            }
        }
    
        /// <summary>
        /// 资源文件查询服务类
        /// </summary>
        internal class GameQueryServices : IBuildinQueryServices
        {
            public bool Query(string packageName, string fileName, string fileCRC)
            {
    #if UNITY_IPHONE
                throw new Exception("Ios平台需要内置资源");
                return false;
    #else
                return false;
    #endif
            }
        }
    
        #endregion
    
        #region 下载热更资源
    
        IEnumerator Download()
        {
            int downloadingMaxNum = 10;
            int failedTryAgain = 3;
            var package = YooAssets.GetPackage("DefaultPackage");
            var downloader = package.CreateResourceDownloader(downloadingMaxNum, failedTryAgain);
    
            //没有需要下载的资源
            if (downloader.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("更新完成");
            }
            else
            {
                //下载失败
                Debug.Log("更新失败");
            }
        }
    
        /// <summary>
        /// 开始下载
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="sizeBytes"></param>
        private void OnStartDownloadFileFunction(string fileName, long sizeBytes)
        {
            Debug.Log(string.Format("开始下载:文件名:{0},文件大小:{1}", fileName, sizeBytes));
        }
    
        /// <summary>
        /// 下载完成
        /// </summary>
        /// <param name="isSucceed"></param>
        private void OnDownloadOverFunction(bool isSucceed)
        {
            Debug.Log("下载" + (isSucceed ? "成功" : "失败"));
        }
    
        /// <summary>
        /// 更新中
        /// </summary>
        /// <param name="totalDownloadCount"></param>
        /// <param name="currentDownloadCount"></param>
        /// <param name="totalDownloadBytes"></param>
        /// <param name="currentDownloadBytes"></param>
        private void OnDownloadProgressUpdateFunction(int totalDownloadCount, int currentDownloadCount, long totalDownloadBytes, long currentDownloadBytes)
        {
            Debug.Log(string.Format("文件总数:{0},已下载文件数:{1},下载总大小:{2},已下载大小{3}", totalDownloadCount, currentDownloadCount, totalDownloadBytes, currentDownloadBytes));
        }
    
        /// <summary>
        /// 下载出错
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="error"></param>
        private void OnDownloadErrorFunction(string fileName, string error)
        {
            Debug.Log(string.Format("下载出错:文件名:{0},错误信息:{1}", fileName, error));
        }
    
        #endregion
    
        #region 补充元数据
    
        //补充元数据dll的列表
        //通过RuntimeApi.LoadMetadataForAOTAssembly()函数来补充AOT泛型的原始元数据
        private static List<string> AOTMetaAssemblyFiles { get; } = new() { "mscorlib.dll", "System.dll", "System.Core.dll", };
        private static Dictionary<string, TextAsset> s_assetDatas = new Dictionary<string, TextAsset>();
        private static Assembly _hotUpdateAss;
        
        public static byte[] ReadBytesFromStreamingAssets(string dllName)
        {
            if (s_assetDatas.ContainsKey(dllName))
            {
                return s_assetDatas[dllName].bytes;
            }
    
            return Array.Empty<byte>();
        }
    
        
    
        /// <summary>
        /// 为aot assembly加载原始metadata, 这个代码放aot或者热更新都行。
        /// 一旦加载后,如果AOT泛型函数对应native实现不存在,则自动替换为解释模式执行
        /// </summary>
        private static void LoadMetadataForAOTAssemblies()
        {
            /// 注意,补充元数据是给AOT dll补充元数据,而不是给热更新dll补充元数据。
            /// 热更新dll不缺元数据,不需要补充,如果调用LoadMetadataForAOTAssembly会返回错误
            HomologousImageMode mode = HomologousImageMode.SuperSet;
            foreach (var aotDllName in AOTMetaAssemblyFiles)
            {
                byte[] dllBytes = ReadBytesFromStreamingAssets(aotDllName);
                // 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码
                LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(dllBytes, mode);
                Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. mode:{mode} ret:{err}");
            }
        }
    
        #endregion
    
        #region 运行测试
    
        void StartGame()
        {
            // 加载AOT dll的元数据
            LoadMetadataForAOTAssemblies();
            // 加载热更dll
    #if !UNITY_EDITOR
            _hotUpdateAss = Assembly.Load(ReadBytesFromStreamingAssets("HotUpdate.dll"));
    #else
            _hotUpdateAss = System.AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "HotUpdate");
    #endif
            Debug.Log("运行热更代码");
            StartCoroutine(Run_InstantiateComponentByAsset());
        }
    
        IEnumerator Run_InstantiateComponentByAsset()
        {
            // 通过实例化assetbundle中的资源,还原资源上的热更新脚本
            var package = YooAssets.GetPackage("DefaultPackage");
            var handle = package.LoadAssetAsync<GameObject>("Cube");
            yield return handle;
            handle.Completed += Handle_Completed;
        }
    
        private void Handle_Completed(AssetHandle obj)
        {
            Debug.Log("准备实例化");
            GameObject go = obj.InstantiateSync();
            Debug.Log($"Prefab name is {go.name}");
        }
    
        #endregion
    }
    //PS:版本不同可能有一些类名发生变化,请参照现阶段版本自行修改,官网可能更新不及时。
    

    打包阶段
    (这里演示的是pc平台)
    打开菜单 HybridCLR/Generate/All,耐心等待之后,
    回到Assets同级目录,将HybridCLRData/HotUpdateDlls/StandaloneWindows64/HotUpdate.dll复制到Assets/MyAssset/Codes内,并且加上后缀.bytes,这是包含热更逻辑代码的dll

    再将HybridCLRData/AssembliesPostIl2CppStrip/StandaloneWindows64目录下的mscorlib.dll, System.dll, System.Core.dll这三个dll复制到Assets/MyAssset/Codes内,并且加上后缀.bytes,这是包含补充元数据的dll

    打开菜单YooAsset/AssetBundle Builder,BuildModel选ForceRebuild,全量构建。一般第一次选这个,后面IncrementalBuild热更选增量构建,点击ClickBuild

    构建完之后会自动打开构建后的资源目录,在Asset同级目录下的Bundles/StandaloneWindows64/DefaultPackage/2024-06-27-1194(构建时的版本号)

    把里面的所有东西,放在本地资源服务器Nginx的目录下,之前代码里访问的地址:http://127.0.0.1:8084/TestProject/PC就是这里了,到时候热更就是从这里下载最新的资源

    操作完上述之后,打开菜单 File/Build Settings/Build先把.exe打出来,双击运行,能看到先从http://127.0.0.1:8084/TestProject/PC路径下下载资源包,然后补充元数据,然后实例化了Cube预制体,执行了预制体上的代码,打印了“原始代码”。


    开始热更
    终于到了激动人心的热更环节,前面搞了那么多就是为了热更代码和资源。接下来我们把cube预制体尺寸x改为10,将InstantiateByAsset脚本的输出从“原始代码”改为“热更后的代码”。

    点击菜单 HybridCLR/CompileDll/ActiveBuildTarget,把新的HotUpdate.dll复制替换掉MyAsset里面原来的HotUpdate.dll,记得加后缀.bytes,另外三个mscorlib.dll, System.dll, System.Core.dll也复制替换过去记得加后缀.bytes

    打开菜单YooAsset/AssetBundle Builder,BuildModel选IncrementalBuild,增量构建,点击ClickBuild,然后自动打开了生成的资源所在的文件夹,把这一堆生成的东西,复制到本地资源服务器Nginx的TestProject/PC目录下,之前的旧资源删掉。

    然后重新打开之前的exe程序,看到代码和资源都热更了,大功告成!!!

    七、最后

    至于其他的更多细节,比如加密解密,资源收集设置等待,可以多多浏览官网以及官网的示例项目。
    HybridCLR
    YooAsset

    github工程:https://github.com/zhanghan2007/HotDemo

    八、后记

    2024.12.31日

    文章发出后,很多小伙伴表示里面各种类找不到,那是由于新版本的YooAsset插件里面有一些改动,我用当天最新版2.2.7版本的YooAsset插件,修改了报错部分并重试了一下,demo可以跑通。

    修改部分如下:

    1.修改LoadDll.cs脚本,修复报错,代码如下:

    using HybridCLR;
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using UnityEngine;
    using YooAsset;
    
    /// <summary>
    /// 脚本工作流程:
    /// 1.下载资源,用yooAsset资源框架进行下载
    ///    1.资源文件,ab包
    ///    2.热更新dll
    /// 2.给AOT DLL补充元素据,通过RuntimeApi.LoadMetadataForAOTAssembly
    /// 3.通过实例化prefab,运行热更代码
    /// </summary>
    public class LoadDll : MonoBehaviour
    {
        /// <summary>
        /// 资源系统运行模式
        /// </summary>
        public EPlayMode PlayMode = EPlayMode.HostPlayMode;
    
        private ResourcePackage _defaultPackage;
    
        void Start()
        {
            StartCoroutine(InitYooAssets(StartGame));
        }
    
        #region YooAsset初始化
    
        IEnumerator InitYooAssets(Action onDownloadComplete)
        {
            // 1.初始化资源系统
            YooAssets.Initialize();
    
            string packageName = "DefaultPackage";
            var package = YooAssets.TryGetPackage(packageName) ?? YooAssets.CreatePackage(packageName);
            YooAssets.SetDefaultPackage(package);
            InitializationOperation initializationOperation = null; 
            //===================================适应新版本YooAsset插件的修改===================================
            if (PlayMode == EPlayMode.EditorSimulateMode)
            {
                // //编辑器模拟模式
                var simulateBuildParam = new EditorSimulateBuildParam { PackageName = packageName };
                var simulateBuildResult = EditorSimulateModeHelper.SimulateBuild(simulateBuildParam);
                var createParameters = new EditorSimulateModeParameters { EditorFileSystemParameters = FileSystemParameters.CreateDefaultEditorFileSystemParameters(simulateBuildResult) };
                initializationOperation = package.InitializeAsync(createParameters);
            }
            else if (PlayMode == EPlayMode.HostPlayMode)
            {
                //联机运行模式
                string defaultHostServer = GetHostServerURL();
                string fallbackHostServer = GetHostServerURL();
                IRemoteServices remoteServices = new RemoteServices(defaultHostServer, fallbackHostServer);
                var createParameters = new HostPlayModeParameters { BuildinFileSystemParameters = FileSystemParameters.CreateDefaultBuildinFileSystemParameters(), CacheFileSystemParameters = FileSystemParameters.CreateDefaultCacheFileSystemParameters(remoteServices) };
                initializationOperation = package.InitializeAsync(createParameters);
            }
    
            yield return initializationOperation;
            //===================================适应新版本YooAsset插件的修改===================================
                
            if (initializationOperation.Status == EOperationStatus.Succeed)
            {
                Debug.Log("资源包初始化成功!");
            }
            else
            {
                Debug.LogError($"资源包初始化失败:{initializationOperation.Error}");
            }
    
            //2.获取资源版本
            var operation = package.RequestPackageVersionAsync();
            yield return operation;
    
            if (operation.Status != EOperationStatus.Succeed)
            {
                //更新失败
                Debug.LogError(operation.Error);
                yield break;
            }
    
            string packageVersion = operation.PackageVersion;
            Debug.Log($"Updated package Version : {packageVersion}");
    
            //3.更新补丁清单
            // 更新成功后自动保存版本号,作为下次初始化的版本。
            // 也可以通过operation.SavePackageVersion()方法保存。
            var operation2 = package.UpdatePackageManifestAsync(packageVersion);
            yield return operation2;
    
            if (operation2.Status != EOperationStatus.Succeed)
            {
                //更新失败
                Debug.LogError(operation2.Error);
                yield break;
            }
    
            //4.下载补丁包
            yield return Download();
    
            //判断是否下载成功
            var assets = new List<string> { "HotUpdate.dll" }.Concat(AOTMetaAssemblyFiles);
            foreach (var asset in assets)
            {
                var handle = package.LoadAssetAsync<TextAsset>(asset);
                yield return handle;
                var assetObj = handle.AssetObject as TextAsset;
                s_assetDatas[asset] = assetObj;
                Debug.Log($"dll:{asset}   {assetObj == null}");
            }
    
            _defaultPackage = package;
            onDownloadComplete();
        }
        
        private string GetHostServerURL()
        {
            //模拟下载地址,8084为Nginx里面设置的端口号,项目名,平台名
            return "http://127.0.0.1:8084/TestProject/PC";
        }
    
        /// <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}";
            }
        }
        
    //===================================适应新版本YooAsset插件的修改===================================
    //     /// <summary>
    //     /// 资源文件查询服务类
    //     /// </summary>
    //     internal class GameQueryServices : IBuildinQueryServices
    //     {
    //         public bool Query(string packageName, string fileName, string fileCRC)
    //         {
    // #if UNITY_IPHONE
    //             throw new Exception("Ios平台需要内置资源");
    //             return false;
    // #else
    //             return false;
    // #endif
    //         }
    //     }
    //===================================适应新版本YooAsset插件的修改===================================
    
        #endregion
    
        #region 下载热更资源
    
        IEnumerator Download()
        {
            int downloadingMaxNum = 10;
            int failedTryAgain = 3;
            var package = YooAssets.GetPackage("DefaultPackage");
            var downloader = package.CreateResourceDownloader(downloadingMaxNum, failedTryAgain);
    
            //没有需要下载的资源
            if (downloader.TotalDownloadCount == 0)
            {
                yield break;
            }
    
            //需要下载的文件总数和总大小
            int totalDownloadCount = downloader.TotalDownloadCount;
            long totalDownloadBytes = downloader.TotalDownloadBytes;
    
            //===================================适应新版本YooAsset插件的修改===================================
            //注册回调方法
            downloader.DownloadErrorCallback = OnDownloadErrorFunction;
            downloader.DownloadUpdateCallback = OnDownloadProgressUpdateFunction;
            downloader.DownloadFinishCallback = OnDownloadOverFunction;
            downloader.DownloadFileBeginCallback = OnStartDownloadFileFunction;
            //===================================适应新版本YooAsset插件的修改===================================
    
            //开启下载
            downloader.BeginDownload();
            yield return downloader;
    
            //检测下载结果
            if (downloader.Status == EOperationStatus.Succeed)
            {
                //下载成功
                Debug.Log("更新完成");
            }
            else
            {
                //下载失败
                Debug.Log("更新失败");
            }
        }
    
        //===================================适应新版本YooAsset插件的修改===================================
        /// <summary>
        /// 开始下载
        /// </summary>
        private void OnStartDownloadFileFunction(DownloadFileData downloadFileData)
        {
            Debug.Log($"开始下载:文件名:{downloadFileData.FileName},文件大小:{downloadFileData.FileSize}");
        }
    
        /// <summary>
        /// 下载完成
        /// </summary>
        private void OnDownloadOverFunction(DownloaderFinishData downloaderFinishData)
        {
            Debug.Log("下载" + (downloaderFinishData.Succeed ? "成功" : "失败"));
        }
    
        /// <summary>
        /// 更新中
        /// </summary>
        private void OnDownloadProgressUpdateFunction(DownloadUpdateData downloadUpdateData)
        {
            Debug.Log($"文件总数:{downloadUpdateData.TotalDownloadCount},已下载文件数:{downloadUpdateData.CurrentDownloadCount},下载总大小:{downloadUpdateData.TotalDownloadBytes},已下载大小{downloadUpdateData.CurrentDownloadBytes}");
        }
    
        /// <summary>
        /// 下载出错
        /// </summary>
        /// <param name="errorData"></param>
        private void OnDownloadErrorFunction(DownloadErrorData errorData)
        {
            Debug.Log($"下载出错:包名:{errorData.PackageName} 文件名:{errorData.FileName},错误信息:{errorData.ErrorInfo}");
        }
        //===================================适应新版本YooAsset插件的修改===================================
    
        #endregion
    
        #region 补充元数据
    
        //补充元数据dll的列表
        //通过RuntimeApi.LoadMetadataForAOTAssembly()函数来补充AOT泛型的原始元数据
        private static List<string> AOTMetaAssemblyFiles { get; } = new() { "mscorlib.dll", "System.dll", "System.Core.dll", };
        private static Dictionary<string, TextAsset> s_assetDatas = new Dictionary<string, TextAsset>();
        private static Assembly _hotUpdateAss;
        
        public static byte[] ReadBytesFromStreamingAssets(string dllName)
        {
            if (s_assetDatas.ContainsKey(dllName))
            {
                return s_assetDatas[dllName].bytes;
            }
    
            return Array.Empty<byte>();
        }
    
        
    
        /// <summary>
        /// 为aot assembly加载原始metadata, 这个代码放aot或者热更新都行。
        /// 一旦加载后,如果AOT泛型函数对应native实现不存在,则自动替换为解释模式执行
        /// </summary>
        private static void LoadMetadataForAOTAssemblies()
        {
            /// 注意,补充元数据是给AOT dll补充元数据,而不是给热更新dll补充元数据。
            /// 热更新dll不缺元数据,不需要补充,如果调用LoadMetadataForAOTAssembly会返回错误
            HomologousImageMode mode = HomologousImageMode.SuperSet;
            foreach (var aotDllName in AOTMetaAssemblyFiles)
            {
                byte[] dllBytes = ReadBytesFromStreamingAssets(aotDllName);
                // 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码
                LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(dllBytes, mode);
                Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. mode:{mode} ret:{err}");
            }
        }
    
        #endregion
    
        #region 运行测试
    
        void StartGame()
        {
            // 加载AOT dll的元数据
            LoadMetadataForAOTAssemblies();
            // 加载热更dll
    #if !UNITY_EDITOR
            _hotUpdateAss = Assembly.Load(ReadBytesFromStreamingAssets("HotUpdate.dll"));
    #else
            _hotUpdateAss = System.AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "HotUpdate");
    #endif
            Debug.Log("运行热更代码");
            StartCoroutine(Run_InstantiateComponentByAsset());
        }
    
        IEnumerator Run_InstantiateComponentByAsset()
        {
            // 通过实例化assetbundle中的资源,还原资源上的热更新脚本
            var package = YooAssets.GetPackage("DefaultPackage");
            var handle = package.LoadAssetAsync<GameObject>("Cube");
            yield return handle;
            handle.Completed += Handle_Completed;
        }
    
        private void Handle_Completed(AssetHandle obj)
        {
            Debug.Log("准备实例化");
            GameObject go = obj.InstantiateSync();
            Debug.Log($"Prefab name is {go.name}");
        }
    
        #endregion
    }
    //PS:版本不同可能有一些类名发生变化,请参照现阶段版本自行修改,官网可能更新不及时。
    

    2.YooAssetSettings资源放到别的文件夹,修改如下:
    原位置
    新位置

    3.其他操作不变

    作者:capricorn1245

    物联沃分享整理
    物联沃-IOTWORD物联网 » Unity热更新方案HybridCLR YooAsset教程:从零开始,C#开发热更保姆级指南

    发表回复