Unity零停机热更新实战:GameFramework与YooAsset集成方案

1. 项目概述:为什么我们需要“零停机”热更新?

做Unity游戏开发,尤其是手游,最头疼的事情之一就是更新。传统更新需要玩家重新下载整个安装包,流失率有多高,做过运营的同行都懂。所以,“热更新”成了现代游戏开发的标配。但“热更新”本身也分三六九等,最理想的状态,就是标题里说的“零停机”——玩家在游戏过程中,几乎无感知地完成资源、甚至逻辑代码的更新,游戏体验丝滑不断档。

这听起来像魔法,但背后是GameFramework和YooAsset这两个重量级框架的强强联合。GameFramework(后文简称GF)提供了一个高度模块化、可扩展的游戏程序框架,它定义了资源加载、UI管理、场景流程等一整套规范。而YooAsset则是近年来在Unity社区声名鹊起的下一代资源管理系统,它解决了AssetBundle老方案中依赖管理复杂、打包冗余、加载繁琐等一系列痛点。

这个组合的目标很明确:用GF管理游戏的生命周期和模块,用YooAsset接管所有资源的打包、分发与加载,最终实现一套稳定、高效、对开发者友好、对玩家透明的热更新体系。我经历过从自己手撸AB系统,到用Addressables,再到转向YooAsset的完整周期,实测下来,YooAsset在打包策略的灵活性和运行时加载的简易性上,确实带来了质的提升。接下来,我就把这套方案的完整落地过程,包括核心思路、实操细节、以及我踩过的那些坑,毫无保留地拆解给你。

2. 核心架构设计:GameFramework与YooAsset如何协同工作?

要实现零停机热更新,首先得理清架构。GF和YooAsset不是简单替换关系,而是各司其职的深度集成。

2.1 GameFramework的角色:游戏流程的总指挥

GF的核心是提供一套“有限状态机”(Procedure)来管理游戏的整体流程。比如,游戏启动后,会依次经历“检查版本” -> “更新资源” -> “加载用户数据” -> “进入主城”等状态。热更新,本质上就是这个流程中的一个或多个状态。

GF内置了IResourceManager接口来抽象资源加载行为。我们的目标,就是让YooAsset成为这个接口的具体实现者。这样,游戏内所有通过GF接口(如GameEntry.Resource.LoadAsset)加载资源的请求,都会无缝地转发给YooAsset来处理。游戏业务代码无需关心底层用的是AB、Addressables还是YooAsset,保持了框架的整洁和可替换性。

2.2 YooAsset的角色:资源世界的管家

YooAsset接管了从资源收集、打包、部署到加载的全链路。它的几个核心设计决定了其优势:

  1. 基于标签的打包策略:不同于传统的基于目录打包,YooAsset允许你给资源打上自定义标签(如ui_common,scene_maincity),然后按标签来分组打包。这带来了极大的灵活性,你可以把频繁更新的资源打在一个小包里,把公共基础资源打在一个大包里,更新时只需下载那个小包。
  2. 主动依赖收集与零冗余:YooAsset在打包时会主动分析资源间的依赖关系,并确保相同的资源只被打进一个包中。这彻底解决了手动管理AB依赖时容易出现的“资源冗余”和“依赖丢失”问题。在提供的参考项目中,提到的“生资源”和“成品资源”路径区分,其实就是这种理念的体现:原始素材(生资源)作为输入,由YooAsset分析依赖后,生成最终的、无冗余的AssetBundle(成品资源)。
  3. 强大的运行时API:提供了同步、异步、分包、断点续传等各种加载方式,并且与Unity的Addressables API在某些设计上相似,降低了学习成本。

2.3 集成思路:桥接与替换

集成的核心是创建一个自定义的ResourceManager,继承并实现GF的IResourceManager。在这个自定义管理器中:

  • 初始化阶段:启动YooAsset,初始化资源包,并建立资源路径、资源名与YooAsset资源句柄(AssetHandle)的映射关系。
  • 加载阶段:当GF的业务模块调用LoadAsset时,我们的自定义管理器将其转换为对YooAssetPackage.LoadAssetAsync的调用。
  • 更新阶段:在GF的“更新资源”状态中,调用YooAsset的更新接口,检查资源包版本、下载差异资源。

这种设计确保了热更新逻辑被封装在资源管理层,游戏上层逻辑完全无感。玩家在登录时,流程可能是:启动游戏 -> GF进入“检查更新”流程 -> 调用YooAsset检查服务器资源版本 -> 发现更新,显示更新界面并下载 -> 下载完成后,YooAsset内部切换资源包版本 -> GF进入下一个流程(如加载游戏)。整个过程,除了下载时可能需要等待,游戏主程序无需重启。

3. 环境准备与工程配置

理论清晰了,我们开始动手。假设你已有一个初步的Unity项目(版本建议2020.3 LTS或更新,参考项目用了2022.3),并导入了GameFramework。

3.1 导入YooAsset

  1. 通过Package Manager导入:这是最推荐的方式。在Unity编辑器中,打开Window -> Package Manager,点击左上角“+”号,选择“Add package from git URL”,输入YooAsset的Git仓库地址(通常为https://github.com/tuyoogame/YooAsset.git)。这种方式便于后续更新。
  2. 或下载UnityPackage:从YooAsset的官方仓库或发布页面下载最新的.unitypackage文件,直接导入工程。

导入后,你会在菜单栏看到YooAsset选项,说明导入成功。

3.2 配置YooAsset资源收集器

这是YooAsset打包前最关键的一步,决定了资源如何被分组。

  1. 在Project窗口,右键选择YooAsset -> Create AssetBundle Collector Config。这会在项目中创建一个配置文件。
  2. 选中该配置文件,在Inspector面板中,你可以定义多个“资源收集器”。
  3. 每个收集器主要配置:
    • Collect Path:要收集资源的目录(如Assets/GameMain/Textures)。
    • Collector Type:收集类型,最常用的是Main Asset Collector,它只收集该目录下的主资源(如Prefab、Scene),并自动收集其依赖。
    • Group Name:资源组名,对应打包后的AssetBundle名称的一部分。你可以按功能分组,如uirolescene
    • Tags:给这组资源打上标签。标签是运行时加载的重要依据,比如你可以给所有UI通用图集打上ui_common标签。

实操心得:标签的设计要有前瞻性。不要简单地按文件夹分标签,而应该按更新频率使用场景来分。例如,所有新手引导的资源可以打上tutorial标签并打在一个包里,这样当需要修改引导时,只需更新这个很小的包。公共字体、音效可以打上base标签。

3.3 创建自定义ResourceManager

  1. 在脚本中,创建一个类,例如YooAssetResourceManager,继承自GameFramework.Resource.ResourceManagerBase(这是GF抽象资源管理器的基类)。
  2. 你需要重写一系列抽象方法,最重要的是InitializeUpdateLoadAssetUnloadAsset等。
  3. Initialize方法中,你需要初始化YooAsset的ResourcePackage。这通常包括:
    // 创建资源包 string packageName = "DefaultPackage"; var package = YooAssets.CreatePackage(packageName); YooAssets.SetDefaultPackage(package); // 设为默认包,方便全局调用 // 初始化参数 var initParameters = new OfflinePlayModeParameters(); // 编辑器模拟模式 // 或上线模式:new HostPlayModeParameters(),并设置内置的查询服务和下载服务 // initParameters.BuildinRootURL = "http://your-cdn-server/"; // 内置资源根路径 // initParameters.RemoteServices = new RemoteServices("http://your-remote-server/"); // 远程服务 var initOperation = package.InitializeAsync(initParameters); yield return initOperation; // 等待初始化完成
  4. LoadAsset方法中,将GF的加载请求转发给YooAsset:
    public override object LoadAsset(string assetName, Type assetType) { // 实际项目中,这里可能需要一个从assetName到YooAsset实际路径的映射表 var handle = YooAssets.LoadAssetSync(assetName, assetType); return handle.AssetObject; } // 异步加载同理,重写 LoadAssetAsync

3.4 将自定义管理器注册到GameFramework

在游戏入口处(通常是某个Procedure),你需要用自定义的YooAssetResourceManager替换掉GF默认的资源管理器。

// 在GameEntry启动时,或在某个初始化Procedure中 GameEntry.RegisterComponent<YooAssetResourceManager>(); // 替换原有的ResourceManager GameEntry.GetComponent<YooAssetResourceManager>().Initialize();

完成以上步骤,GF和YooAsset的桥梁就搭建好了。游戏内所有通过GameEntry.Resource的加载调用,都会经由你的自定义类,最终由YooAsset执行。

4. 热更新流程的完整实现

架构和基础搭好了,现在我们来深入最核心的热更新流程。目标是实现:游戏启动时,自动检测资源更新,并在玩家无感或可接受的方式下完成更新。

4.1 版本定义与资源清单

热更新的基石是版本管理。你需要维护两套版本:

  1. 应用程序版本(App Version):即玩家从商店下载的IPA/APK包的版本。这个版本号通常写在Application.version或自定义配置中。大功能更新、引擎特性变更需要升级此版本。
  2. 资源包版本(Resource Version):独立于App版本,每次你发布新的AssetBundle资源包时,递增此版本号。YooAsset通过对比本地和远程的“资源清单”(Package Manifest)文件来判断是否需要更新。

资源清单是YooAsset在打包时自动生成的JSON文件,包含了所有资源文件的哈希值、大小、所属资源包等信息。服务器上需要存放最新的资源清单和对应的资源包文件。

4.2 更新状态机设计

在GF的框架下,我们设计一个专门的ProcedureUpdateResources(更新资源流程)状态。这个状态是热更新的核心控制器。

// 伪代码,展示流程 internal class ProcedureUpdateResources : ProcedureBase { private enum UpdateStep { CheckVersion, // 检查版本 UpdateManifest, // 更新资源清单 DownloadFiles, // 下载资源文件 Done // 完成 } private UpdateStep _currentStep = UpdateStep.CheckVersion; protected override void OnEnter(ProcedureOwner procedureOwner) { base.OnEnter(procedureOwner); StartUpdate(); } private async void StartUpdate() { var package = YooAssets.GetPackage("DefaultPackage"); // 步骤1:检查版本(可连接自己服务器获取最新资源版本号) int localVersion = GetLocalResourceVersion(); int remoteVersion = await GetRemoteResourceVersionFromServer(); if(remoteVersion > localVersion) { _currentStep = UpdateStep.UpdateManifest; // 步骤2:更新资源清单 var updateManifestOperation = package.UpdatePackageManifestAsync(remoteVersion); await updateManifestOperation.Task; if(updateManifestOperation.Status == EOperationStatus.Succeed) { _currentStep = UpdateStep.DownloadFiles; // 步骤3:创建下载器,下载差异资源 int downloadingMaxNum = 10; // 最大并发下载数 int failedTryAgain = 3; // 失败重试次数 var downloader = package.CreateResourceDownloader(downloadingMaxNum, failedTryAgain); // 没有需要下载的资源,则直接完成 if(downloader.TotalDownloadCount == 0) { OnUpdateComplete(); return; } // 注册下载进度回调,用于更新UI进度条 downloader.OnDownloadProgressCallback = OnDownloadProgress; downloader.OnDownloadErrorCallback = OnDownloadError; // 开始下载 downloader.BeginDownload(); await downloader.Task; if(downloader.Status == EOperationStatus.Succeed) { OnUpdateComplete(); } else { // 处理下载失败 } } } else { // 无需更新,直接进入游戏 OnUpdateComplete(); } } private void OnUpdateComplete() { // 更新本地记录的版本号 SaveLocalResourceVersion(remoteVersion); // 切换到下一个流程,如进入游戏主菜单 ChangeState<ProcedureMainMenu>(_procedureOwner); } }

4.3 边玩边下(On-Demand Downloading)

“零停机”的精髓在于,不是所有更新都需要在登录时阻塞进行。对于关卡资源、大型场景等,可以实现“边玩边下”。

YooAsset的Package提供了CreateResourceDownloader方法,你可以指定要下载哪些标签的资源。例如,当玩家点击进入“副本A”时:

public void OnEnterDungeonA() { // 预检查副本A所需资源包(标签为`dungeon_a`)是否已就绪 var package = YooAssets.GetPackage("DefaultPackage"); var downloader = package.CreateResourceDownloader(new string[]{"dungeon_a"}, 10, 3); if(downloader.TotalDownloadCount > 0) { // 显示“正在下载资源,请稍候...”的提示,但不阻塞主线程 ShowDownloadingUI(downloader); downloader.BeginDownload(); // 可以等待下载完成,也可以允许玩家在下载过程中进行其他操作 downloader.Completed += (op) => { if(op.Status == EOperationStatus.Succeed) { HideDownloadingUI(); StartDungeonA(); // 真正开始加载场景和资源 } }; } else { // 资源已就绪,直接开始 StartDungeonA(); } }

注意事项:边玩边下需要精细的UI提示和网络状态管理。要处理好下载失败、网络切换、玩家取消等情况。同时,要合理设计资源包大小,避免单个关卡资源包过大,导致下载时间过长影响体验。

4.4 本地模拟与远程部署

开发阶段,我们不需要每次都上传资源到CDN。YooAsset的OfflinePlayMode(离线播放模式)可以直接从本地磁盘加载资源,用于快速迭代。

当需要测试完整的更新流程时,可以搭建一个本地HTTP服务器(如HFS,参考项目中提到的)。将打包输出的StreamingAssets目录(或指定的输出目录)作为服务器根目录,然后将YooAsset初始化参数改为HostPlayModeParameters,并设置BuildinRootURLRemoteServices指向你的本地服务器IP。这样,就能在编辑器或真机上模拟从服务器下载资源的过程。

上线时,你需要:

  1. 将打包生成的资源文件(AssetBundles)和清单文件(PackageManifest)上传到CDN。
  2. 在游戏中,将HostPlayModeParameters的远程地址设置为你的CDN域名。
  3. 你的游戏服务器(或一个简单的版本服务器)需要提供一个接口,让客户端查询最新的资源版本号。这个版本号用于上面流程中的UpdatePackageManifestAsync调用。

5. 打包、部署与自动化

一套好的热更新系统,离不开稳定高效的打包和发布流程。

5.1 YooAsset打包命令

YooAsset提供了编辑器菜单和API两种打包方式。对于自动化,我们使用API。

你可以创建一个编辑器脚本,例如BuildScript.cs,包含一个静态方法:

using UnityEditor; using YooAsset.Editor; public static class BuildScript { public static void BuildAndroid() { BuildTarget buildTarget = BuildTarget.Android; string packageName = "DefaultPackage"; // 构建参数 var buildParameters = new BuildParameters(); buildParameters.BuildTarget = buildTarget; buildParameters.BuildPipeline = EBuildPipeline.BuiltinBuildPipeline; // 或可编程构建管线 buildParameters.BuildMode = EBuildMode.ForceRebuild; // 强制重建 buildParameters.PackageName = packageName; buildParameters.PackageVersion = "1.0.0"; // 本次打包的资源版本号 buildParameters.OutputRoot = "Project/BuildOutput"; // 输出目录 buildParameters.BuildinRoot = "Assets/StreamingAssets"; // 内置资源拷贝目录 buildParameters.CompressOption = ECompressOption.LZ4; // 压缩格式 // 创建构建上下文 var buildContext = new BuildContext(); buildContext.SetContextObject(buildParameters); // 开始构建 var builder = new BuiltinBuildPipeline(); var buildResult = builder.Run(buildContext); if(buildResult.Success) { Debug.Log($"资源打包成功!输出路径:{buildResult.OutputPackageDirectory}"); // 这里可以添加后续步骤,如自动上传到CDN } else { Debug.LogError($"资源打包失败:{buildResult.ErrorInfo}"); } } }

然后可以通过Unity命令行调用这个方法:Unity -batchmode -quit -executeMethod BuildScript.BuildAndroid

5.2 与CI/CD集成(以Jenkins为例)

参考项目中提到了Jenkins自动化。核心思路是:

  1. Jenkins Job配置:创建一个自由风格或流水线项目。
  2. 参数化构建:添加构建参数,如RES_VERSION(资源版本号)、IS_HOTFIX(是否仅更新资源不出包)、PLATFORM(目标平台)。
  3. 构建步骤
    • 拉取代码:从Git仓库拉取最新项目代码。
    • 执行Unity打包:调用Unity命令行,执行上述的打包方法,并传入Jenkins参数。
      Unity -batchmode -nographics -quit -projectPath /path/to/your/project -executeMethod BuildScript.BuildAndroid -resourceVersion ${RES_VERSION}
    • 后续处理
      • 如果IS_HOTFIX为真,则只将BuildOutput中的资源文件(不包括可执行程序)上传到CDN,并更新版本服务器上的资源版本号。
      • 如果为假(全量更新),则生成完整的APK/IPA,并可能将其提交到应用商店或内部测试渠道。
  4. 白名单与灰度发布:可以在打包脚本或游戏初始化时,读取一个白名单(如特定用户ID列表)。只有白名单内的用户,才会从测试CDN地址拉取最新的热更资源,其他用户仍走正式渠道。这需要在资源更新检查逻辑中加入分支判断。

5.3 版本回退机制

热更新虽好,但必须考虑回滚。万一新资源包有严重Bug怎么办?

  1. 资源版本回退:YooAsset支持加载旧版本的资源清单。在服务器端,你需要保留历史上发布过的所有资源包版本。当需要回退时,将版本服务器的“最新版本号”指向旧版本。客户端下次检查更新时,会发现本地版本比服务器“新”,此时不应自动降级,而是应该提示玩家“发现服务器维护,请重启游戏获取更新”(实际上重启后拉取的是旧版资源)。更安全的方式是,客户端在更新前备份当前资源清单,如果更新后启动失败,则自动回滚到备份的清单。
  2. 代码热更回退:如果结合了HybridCLR(代码热更新),情况更复杂。通常代码热更与资源热更是绑定的。回退时,需要同时回退代码DLL和对应的资源包。这要求打包时,代码版本和资源版本有明确的对应关系,并一起发布和回滚。

实操心得:每次发布热更包前,务必在本地和测试环境进行完整流程测试,包括更新、回退、清除本地缓存后更新等场景。务必确保CDN的缓存策略设置正确(资源文件应设置为长期缓存,清单文件应设置为不缓存或很短时间缓存),否则玩家可能无法及时获取到最新的清单。

6. 性能优化与内存管理

集成YooAsset后,资源加载方式变了,性能调优的点也有所不同。

6.1 资源包划分策略

这是影响加载速度和更新体积的关键。不好的分包会导致首次加载慢,或更新时下载大量无关资源。

  • 按功能模块分包:UI、角色、场景、音效等分开。这是基础。
  • 按使用时机分包:登录界面资源、主城资源、战斗资源分开。结合“边玩边下”。
  • 按更新频率分包:将几乎不变的底层库(如Shader、通用字体)放在一个“基础包”,频繁调整的活动UI放在“活动包”。基础包可以随App发布,活动包走热更。
  • 控制包体大小:单个AssetBundle不宜过大(建议不超过10MB),也不宜过小(避免大量小文件请求)。可以利用YooAsset的“自动收集依赖”功能,它会帮你合理合并依赖,但你需要通过标签控制粒度。

6.2 资源加载与卸载

YooAsset通过AssetHandle来管理加载的资源。必须妥善管理这些句柄的生命周期,否则会导致内存泄漏。

// 正确的加载与卸载 public class UIWindow : MonoBehaviour { private AssetHandle _iconHandle; void OnEnable() { // 异步加载,并保存句柄 _iconHandle = YooAssets.LoadAssetAsync<Sprite>("ui_icon_hero"); _iconHandle.Completed += (handle) => { if(handle.Status == EOperationStatus.Succeed) { GetComponent<Image>().sprite = handle.AssetObject as Sprite; } }; } void OnDisable() { // 窗口关闭时,释放资源句柄 if(_iconHandle != null) { _iconHandle.Release(); _iconHandle = null; } } }

注意事项AssetHandle.Release()只是减少引用计数。当该资源的所有句柄都被释放,且没有被其他资源引用时,YooAsset才会在合适的时机(或手动调用Package.UnloadUnusedAssets)将其从内存中卸载。切勿在异步加载完成前释放句柄,也不要在资源还在使用时(如Sprite正在被Image引用)就强制卸载包。

6.3 依赖管理与冗余检测

YooAsset最大的优势之一就是自动处理依赖。但你需要理解其原理:打包时,它分析资源引用,确保共享资源只存在于一个包中(比如材质A被模型B和C引用,那么材质A只会被打进B或C所在的包,或者一个公共包)。加载模型B时,YooAsset会自动加载其依赖的材质A所在的包。

在运行时,可以通过YooAssets.GetAssetInfo()来查询资源的依赖信息,辅助调试。如果发现内存中有意外的资源残留,检查一下是不是有隐藏的句柄没有释放,或者资源被静态变量引用。

7. 常见问题排查与实战技巧

这条路我踩过不少坑,这里总结几个最典型的。

7.1 打包后资源丢失或引用错误

  • 现象:编辑器里运行正常,打包后图片变粉、模型消失、脚本丢失。
  • 排查
    1. 检查资源收集器配置:确认所有需要的资源目录都被正确的收集器覆盖。特别注意那些通过代码动态加载的资源路径,是否在收集范围内。
    2. 查看打包报告:YooAsset打包结束后会生成一个报告文件(BuildReport.html),用浏览器打开。仔细查看“资源构建结果”和“资源包列表”,确认你的资源是否被打进了预期的包中,依赖关系是否正确。
    3. 检查AssetBundle名称冲突:确保不同的收集器配置的Group Name不会导致最终生成的AB文件名重复。
    4. Shader和材质问题:如果使用URP/HDRP,确保Shader被打包进去。有时需要将常用的Shader Variant收集到一个独立的包中,或使用ShaderVariantCollection

7.2 热更新失败,一直卡在更新界面

  • 现象:更新进度条不动,或下载失败。
  • 排查
    1. 网络与CDN:首先检查设备网络。然后检查CDN上的资源文件是否可以正常访问(用浏览器直接打开一个资源文件的URL试试)。特别注意清单文件,确保PackageManifest文件能被正确下载,且内容无误。
    2. 版本号管理:确认客户端本地记录的版本号、服务器返回的版本号、CDN上资源包的实际版本号,三者逻辑一致。常见错误是服务器版本号更新了,但CDN文件还没上传或生效。
    3. 下载器配置:检查CreateResourceDownloader时设置的最大并发数和重试次数是否合理。在弱网络环境下,并发数过高可能导致请求失败率上升。
    4. 日志分析:开启YooAsset的详细日志(YooAssets.Logger = new UnityLogger()),查看下载过程中的错误信息。错误信息通常会明确指出是网络超时、HTTP状态码错误还是文件校验失败(哈希值不匹配)。

7.3 内存占用异常增长

  • 现象:游戏运行一段时间后,内存持续上升,甚至导致崩溃。
  • 排查
    1. 句柄泄漏:使用Profiler的Memory Snapshot工具,查看AssetHandle类型的对象数量是否只增不减。重点检查UI界面、场景切换时,加载的资源句柄是否在适当的时候(如界面关闭、场景卸载)被Release
    2. 资源包未卸载:加载了一个资源包(特别是场景包)后,如果不再需要,应该调用Package.UnloadBundle()来卸载整个包。只释放资源句柄不会卸载AssetBundle文件本身。
    3. 纹理格式与大小:检查热更的资源中是否包含未压缩或尺寸过大的纹理。移动端上,ASTC/ETC2压缩是必须的,并且要根据显示尺寸设置合理的Max Size。

7.4 真机上的兼容性问题

  • 现象:编辑器、部分安卓机正常,但在某些特定机型(尤其是iOS或低端安卓机)上崩溃或加载失败。
  • 排查
    1. iOS文件路径大小写:iOS文件系统是大小写敏感的,而Windows和macOS默认不敏感。确保代码中所有加载资源的路径字符串,其大小写与打包后CDN上的文件名完全一致。最好在项目中就强制使用统一的小写命名规范。
    2. Android存储权限:如果热更资源下载到设备的持久化路径(如Application.persistentDataPath),在Android 10及以上版本,需要确保应用有存储权限,或者使用UnityEngine.Application.temporaryCachePath等无需权限的路径。YooAsset的HostPlayMode会自动处理这些。
    3. Shader变体:不同GPU支持的Shader特性不同。在低端机上,如果加载了包含高端特性(如计算着色器、曲面细分)的Shader变体,可能导致崩溃。务必在打包前做好Shader变体的收集和剥离。

最后,关于参考项目中提到的HybridCLR(代码热更新),这是一个更进阶的话题。它允许你更新C#脚本逻辑,而不仅仅是资源。其与YooAsset的集成,核心在于将热更的DLL文件也视为一种特殊的资源,通过YooAsset进行下载和管理,然后在游戏启动早期,由HybridCLR运行时加载这些DLL。这实现了真正的“代码+资源”双热更,但复杂度也更高,需要对IL2CPP、AOT、元数据管理有更深的理解。如果你的项目对动态更新游戏逻辑有强需求,那么在搞定YooAsset之后,HybridCLR是下一个值得深入的技术方向。