1. 项目概述:为什么我们需要一个靠谱的资源热更方案?
做Unity移动端开发,尤其是手游,资源热更新(简称热更)几乎是绕不开的坎。想象一下,你的游戏上线后,发现一个UI贴图错了,或者一个关卡配置需要调整,难道每次都要用户重新下载几百兆甚至几个G的安装包吗?用户流失率会告诉你答案。所以,一套成熟、稳定、易用的热更方案,是项目后期维护和运营的生命线。
在Unity生态里,资源管理方案不少,从早期的AssetBundle手动管理,到Addressables,再到今天要聊的YooAsset。我选择YooAsset作为这个案例的核心,不是因为它最完美,而是它在“易用性”和“功能完整性”之间找到了一个非常不错的平衡点。它不像纯手撸AssetBundle那样对开发者要求极高、坑巨多,也不像某些重型方案那样学习曲线陡峭、配置繁琐。YooAsset的定位很清晰:为Unity项目提供一个开箱即用、性能不错、且能覆盖大多数热更场景的资源管理系统。特别对于中小型团队或者独立开发者,它能极大降低热更功能的接入和维护成本。
这次,我们就聚焦一个非常具体且高频的场景:为Android平台实现一个简单的案例热更。这个“案例”可以理解为你游戏里的一个场景、一套UI预制体、或者一批配置表。通过这个实操,你会掌握YooAsset从零集成、资源打包、版本管理到客户端更新检测、下载、加载的完整闭环。过程中我会穿插我趟过的坑和总结的技巧,目标是让你看完就能在自己的项目里跑起来。
2. YooAsset核心设计思路与工作流拆解
在动手写代码之前,我们必须先理解YooAsset是怎么思考资源管理这个问题的。这能帮你避免“照猫画虎却不知其所以然”的尴尬。
2.1 核心概念:资源包、资源定位与运行时加载
YooAsset将资源管理抽象为几个核心概念:
- 资源包(Package):这是资源管理的顶层容器。一个项目可以包含多个资源包,比如你可以把基础框架资源放在一个包,每个大型资料片或活动资源放在独立的包。这有利于资源的分治和按需更新。在我们的简单案例里,通常一个项目对应一个资源包就够了。
- 资源收集器(Collector):这是编辑器的核心工具,用于定义哪些资源需要被打包。你可以按文件夹、标签、规则等多种方式收集资源。它的输出是一组“资源信息集合”,记录了资源之间的依赖关系。
- 构建管线(BuildPipeline):负责将收集到的资源,按照指定的参数(如压缩格式、构建目标平台)打包成最终的产物。对于热更来说,最重要的产物就是资源包文件(例如
.bundle文件)和构建报告(记录了所有资源的哈希、大小、依赖等信息)。 - 资源定位地址(Address):这是YooAsset实现“简化加载”的关键。你可以为资源设置一个唯一的字符串地址,比如
"Assets/Art/UI/LoginPanel.prefab"或者一个自定义的"ui_login_panel"。运行时,你只需要通过这个地址来加载资源,无需关心它具体在哪个AssetBundle文件里。YooAsset内部会通过构建时生成的资源清单来完成地址到实际文件位置的映射。 - 资源清单(ResourceManifest):这是热更的“地图”。它包含了当前资源版本号、所有资源包的列表、每个资源的地址、哈希值、大小、依赖关系等核心元数据。客户端更新时,首先就是对比和更新这个清单文件。
2.2 热更工作流全景图
理解了概念,我们来看一个完整的热更流程是如何串联起来的:
编辑器端(开发阶段):
- 资源标记:在Unity编辑器中,使用YooAsset的窗口,为需要热更的资源(预制体、场景、图集等)设置资源包名和定位地址。
- 资源收集:运行“资源收集”命令,YooAsset会扫描项目,根据规则收集所有被标记的资源,并分析依赖,生成资源收集报告。
- 资源构建:选择目标平台(如Android),执行构建命令。这个过程会:
- 将资源打包成
.bundle文件(可能还会根据依赖关系分组)。 - 生成对应版本的资源清单文件(
PackageName.manifest)和资源包哈希文件(PackageName.hash)。 - 生成构建报告,告诉你每个包的大小、包含了哪些资源。
- 将资源打包成
服务器端(运营阶段):
- 版本管理:你需要一个简单的HTTP服务器(甚至可以用对象存储服务如阿里云OSS、腾讯云COS)来存放不同版本的资源文件。通常的目录结构是:
http://your-cdn.com/your-game/ ├── Android/ (平台目录) │ ├── v1.0/ (版本目录) │ │ ├── PackageName.manifest │ │ ├── PackageName.hash │ │ └── AssetBundles/ (存放所有的.bundle文件) │ └── version.txt (或一个JSON文件,记录最新的版本号,如`1.1`) - 更新逻辑:客户端通过访问
version.txt获取最新版本号,与本地版本对比。如果发现新版本,则下载新版本的PackageName.manifest,通过对比新旧清单,计算出需要下载或更新的资源包列表,然后从AssetBundles/目录下载这些.bundle文件到本地持久化目录。
客户端(运行时):
- 初始化:游戏启动时,初始化YooAsset,指定资源包名称和运行模式(本地模拟模式或联机更新模式)。
- 版本检测与更新:向服务器请求最新版本信息,执行上述对比和下载流程。
- 资源加载:更新完成后,就可以使用
YooAssets.LoadAssetAsync("你的资源地址")这样的API来异步加载资源了,体验和加载Resources文件夹里的资源类似,但背后是热更后的新资源。
这个流程听起来步骤不少,但YooAsset已经将其中大部分复杂逻辑封装好了,我们主要需要配置和调用。接下来,我们就进入实战环节。
3. 环境准备与YooAsset集成
3.1 项目环境与YooAsset导入
首先,确保你有一个Unity项目(建议使用Unity 2020 LTS或2021 LTS版本,稳定性较好)。YooAsset的获取方式很简单:
- 通过Git URL添加(推荐):在Unity的Package Manager窗口中,点击“+”号,选择“Add package from git URL”,然后输入YooAsset的Git仓库地址:
https://github.com/tuyoogame/YooAsset.git。这种方式可以方便地更新到最新版本。 - 下载Release包:从GitHub Releases页面下载
.unitypackage文件,直接导入工程。
导入后,你会在Unity编辑器顶部菜单栏看到YooAsset的菜单项。
注意:首次导入后,建议关闭Unity,删除项目Library文件夹,然后重新打开Unity,这能避免一些因缓存导致的脚本编译或引用错误。这是一个通用的小技巧。
3.2 创建资源收集与构建配置
YooAsset的核心配置是通过创建ScriptableObject资产文件来完成的。
- 创建资源收集配置:在Project窗口右键
Create/YooAsset/AssetBundle Collector Config。将其命名为AssetBundleCollectorSetting。这个文件定义了资源收集的规则。 - 创建资源构建配置:同样方式,创建
Create/YooAsset/AssetBundle Builder Config,命名为AssetBundleBuilderSetting。这个文件定义了构建的参数。
通常我会在项目根目录创建一个YooAssetSettings文件夹,把这些配置文件都放进去,方便管理。
配置AssetBundleCollectorSetting:双击打开它,我们需要添加一个“收集器”。
- 点击
Add Collector。 Collector Type:选择Main Asset Collector(主资源收集器)或根据需求选择。我们选Main。Collect Path:设置为你需要打包的资源所在的根目录,例如Assets/GameRes。这意味着会收集这个文件夹及其子文件夹下的所有符合规则的可打包资源。Collector Rule:收集规则。Collect All会收集该路径下所有资源;Collect Only和Ignore可以更精细地控制。我们先选Collect All。Address Rule:地址规则。这是关键!它决定了资源加载时的地址字符串。我强烈推荐使用AddressByFileName或AddressByFilePath。AddressByFileName:使用文件名(不含扩展名)作为地址。例如Assets/GameRes/UI/Login.prefab的地址就是Login。简单,但要注意重名问题。AddressByFilePath:使用相对Collect Path的路径作为地址。例如,如果Collect Path是Assets/GameRes,那么上述文件的地址就是UI/Login。更清晰,不易冲突。
Pack Rule:打包规则。决定如何将资源分组到不同的.bundle文件中。PackSeparately是每个资源单独打一个包,粒度最细,但文件数量可能很多。PackTogether是收集路径下的所有资源打成一个包。对于初学者,可以先用PackTogether简化流程。但在实际项目中,为了优化下载和加载,通常会按功能模块(如按场景、按UI、按角色)来分组打包。Filter Rule:过滤规则,保持默认即可。Asset Tags:资源标签,可以为资源打上标签,用于运行时按标签批量加载或卸载,初期可以不填。
配置AssetBundleBuilderSetting:双击打开,主要关注以下参数:
Build Pipeline:构建管线。对于热更,我们选择BuiltinBuildPipeline(Unity内置管线)或ScriptableBuildPipeline(可编程构建管线,Unity 2019.4+)。两者在打包结果上差异不大,SBP在某些情况下更灵活。我们先选BuiltinBuildPipeline。Build Target:构建目标平台。这里要和你最终发布的平台一致!我们做Android案例,就选Android。千万不能选错,否则打出来的包在目标平台上无法加载。Compress Option:压缩选项。Uncompressed不压缩,加载最快,但包体大;LZMA压缩率高,但加载时需要解压,慢且耗CPU;LZ4压缩率和速度比较均衡,是移动端的推荐选项。我们选LZ4。Output Root Directory:构建输出的根目录。建议设置为项目外的某个独立文件夹,比如D:\UnityBuildOutput,避免污染项目目录。YooAsset会在这个目录下创建以平台命名的子文件夹(如Android)来存放构建产物。Build Version:构建版本。每次构建时手动输入,例如1.0.0。这个版本号会写入资源清单,是客户端进行版本比对的关键。
配置好后,保存。这些配置是后续所有打包操作的基础。
4. 资源打包与构建报告分析
配置好了,我们就可以尝试第一次打包了。
4.1 执行资源构建
打开菜单YooAsset/AssetBundle Builder,会弹出一个构建窗口。
- 选择构建管线:和我们配置里选的一样,比如
BuiltinBuildPipeline。 - 选择构建目标:
Android。 - 选择构建版本:输入
1.0.0。 - 清空构建产物:如果是第一次构建,或者确定要完全重新构建,可以勾选
Clear Build Output。 - 点击
Build按钮。
构建过程会在Unity Console窗口输出日志。如果一切顺利,你会看到“Build succeeded”的提示。构建时间取决于你收集的资源数量。
4.2 解读构建产物与报告
构建完成后,打开我们设置的输出目录(如D:\UnityBuildOutput/Android/1.0.0),你会看到类似以下的文件结构:
1.0.0/ ├── BuildReport/ │ └── ... (构建报告文件,可网页打开查看详情) ├── AssetBundles/ │ ├── package1_assets_all_xxxxxx.bundle │ └── ... (其他的.bundle文件) ├── PackageName.manifest ├── PackageName.hash └── PackageName.bytes (可能,清单的二进制格式)AssetBundles/文件夹:里面是所有生成的.bundle资源包文件。文件名通常包含了包名、资源类型和哈希后缀。PackageName.manifest:这是最重要的资源清单文件,是JSON格式,明文存储(生产环境可以考虑加密)。用文本编辑器打开,你能看到Version,AssetBundleList(列出了所有bundle文件及其哈希、大小),AssetList(列出了所有资源及其地址、所属bundle、依赖等信息)。PackageName.hash:存储了清单文件的哈希值,用于快速校验清单文件本身是否被篡改。BuildReport/:里面有一个HTML文件,用浏览器打开,可以看到本次构建的详细可视化报告,包括资源依赖图、每个bundle的大小构成、冗余资源分析等。这个报告对于优化包体大小、理解资源分布至关重要,一定要养成查看的习惯。
构建报告能帮你发现什么问题?
- 资源冗余:同一个资源被打包到了多个不同的bundle里,导致包体膨胀。
- Bundle粒度不合理:某个bundle过大(比如超过10MB),影响下载和加载速度;或者过多的小bundle(成百上千个),导致文件IO开销增大。
- 依赖关系复杂:报告中的依赖图如果像一团乱麻,说明资源耦合度高,不利于局部更新。
第一次打包后的优化建议:
- 打开构建报告,重点看“资源冗余”和“Bundle列表”。
- 如果发现冗余,回到Unity编辑器,检查资源的导入设置(Import Settings),确保相同的纹理、模型等没有被重复引用到不同的目录下,或者考虑使用共享的图集、材质球。
- 如果觉得bundle划分不合理,回到
AssetBundleCollectorSetting,调整Pack Rule。例如,可以为UI资源创建一个收集器,规则为PackTogether并指定一个标签UI;为场景资源创建另一个收集器。这样它们就会被打包到不同的bundle中。
4.3 搭建简易资源服务器(用于测试)
为了测试热更流程,我们需要一个HTTP服务器来托管刚刚构建出的资源文件。在生产环境你会用CDN,本地测试我们可以用任何简单的HTTP服务器。
Python快速搭建(推荐,跨平台):如果你安装了Python,在构建输出目录(D:\UnityBuildOutput/Android)下打开命令行,执行:
# Python 3 python -m http.server 8080这样就在本机的8080端口启动了一个静态文件服务器。访问http://localhost:8080就能看到目录列表。
创建版本文件:在D:\UnityBuildOutput/Android目录下,创建一个version.txt文件,里面只写一行版本号:1.0.0。这模拟了服务器上的最新版本信息。
现在,你的资源服务器结构应该是:
Android/ ├── 1.0.0/ (刚才构建的文件夹) │ ├── AssetBundles/... │ ├── PackageName.manifest │ └── ... └── version.txt (内容:1.0.0)可以通过http://localhost:8080/version.txt和http://localhost:8080/1.0.0/PackageName.manifest来访问这两个关键文件。
5. Android客户端热更逻辑实现
服务器准备好了,现在我们来写客户端的代码。核心流程是:初始化 -> 创建资源包下载器 -> 检测更新 -> 下载更新 -> 加载资源。
5.1 初始化YooAsset与资源包
我们需要一个脚本来管理整个资源生命周期。通常我会创建一个ResourceManager的单例类。
using UnityEngine; using YooAsset; using System.Collections; public class ResourceManager : MonoBehaviour { public static ResourceManager Instance { get; private set; } private ResourcePackage _defaultPackage; // 默认资源包 void Awake() { if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); InitializeYooAsset(); } else { Destroy(gameObject); } } void InitializeYooAsset() { // 初始化YooAsset运行时环境 YooAssets.Initialize(); // 创建资源包。包名需要和构建配置中的包名一致,默认是"DefaultPackage" _defaultPackage = YooAssets.CreatePackage("DefaultPackage"); // 设置该资源包为默认包,这样后续的加载API如果不指定包名,就会使用这个默认包 YooAssets.SetDefaultPackage(_defaultPackage); Debug.Log("YooAsset初始化完成。"); } }把这个脚本挂载到一个GameObject上,并确保它在游戏启动场景中最早被实例化。
5.2 实现版本检测与资源更新
初始化之后,在游戏主逻辑开始前(比如在Loading界面),我们需要检查并更新资源。
public class ResourceManager : MonoBehaviour { // ... 之前的代码 ... // 资源更新流程的入口方法 public IEnumerator CheckAndUpdateResources(System.Action<bool, string> onCompleted) { // 定义资源服务器的根URL。本地测试就是我们Python服务器的地址。 // 注意:实际发布时要换成你的CDN地址。 string hostServerURL = "http://localhost:8080"; string fallbackHostServerURL = "http://localhost:8080"; // 备用地址,可以和主地址一样 // 创建资源包下载器,用于模拟或联机更新。 // 第一个参数:资源服务器的根URL。 // 第二个参数:构建版本号。如果传空,YooAsset会尝试从服务器下载version.txt来获取最新版本。 // 第三个参数:是否启用断点续传。对于大文件更新,建议开启。 var packageDownloader = _defaultPackage.CreateResourceDownloader(hostServerURL, fallbackHostServerURL, "", 30, true); // 如果没有需要下载的内容,packageDownloader.TotalDownloadCount 会为0 if (packageDownloader == null) { onCompleted?.Invoke(false, "创建下载器失败"); yield break; } if (packageDownloader.TotalDownloadCount == 0) { Debug.Log("没有发现需要更新的资源。"); onCompleted?.Invoke(true, "资源已是最新"); yield break; } // 显示更新UI,例如显示总大小和进度条 long totalDownloadBytes = packageDownloader.TotalDownloadBytes; Debug.Log($"发现资源更新,需要下载 {packageDownloader.TotalDownloadCount} 个文件,总计 {totalDownloadBytes / (1024 * 1024)} MB."); // 开始下载 packageDownloader.BeginDownload(); while (!packageDownloader.IsDone) { // 更新进度 float progress = packageDownloader.Progress; long downloadedBytes = packageDownloader.DownloadedBytes; // 这里可以更新UI进度条和文本 Debug.Log($"下载进度: {progress * 100:F1}% ({downloadedBytes}/{totalDownloadBytes} bytes)"); // 如果下载出错 if (packageDownloader.Status == EOperationStatus.Failed) { Debug.LogError($"资源下载失败: {packageDownloader.Error}"); onCompleted?.Invoke(false, packageDownloader.Error); yield break; } yield return null; // 等待一帧 } // 下载完成 if (packageDownloader.Status == EOperationStatus.Succeed) { Debug.Log("资源更新完成!"); // 重要:下载完成后,需要调用这个方法来更新资源包的状态,使其能加载新资源 _defaultPackage.ForceUnloadAllAssets(); // 强制卸载所有已加载资源(可选,确保加载新资源) _defaultPackage.LoadManifest(); // 重新加载清单 onCompleted?.Invoke(true, "更新成功"); } else { onCompleted?.Invoke(false, "更新过程失败"); } } }在Loading场景的逻辑中,你可以这样调用:
// 在某个MonoBehaviour的Start或特定的UI按钮事件中 StartCoroutine(ResourceManager.Instance.CheckAndUpdateResources((success, message) => { if (success) { Debug.Log("资源检查更新完成,进入游戏。"); // 跳转到游戏主场景 SceneManager.LoadScene("MainMenu"); } else { Debug.LogError($"资源更新失败: {message}"); // 给玩家提示,可以选择重试或跳过(如果支持离线模式) // ShowRetryDialog(message); } }));5.3 热更资源的加载与使用
更新完成后,我们就可以像使用普通Resources资源一样来加载热更资源了,关键就是使用我们之前为资源设置的“地址”。
异步加载预制体并实例化:
public class LoadHotfixResourceExample : MonoBehaviour { IEnumerator Start() { // 假设我们有一个UI预制体的地址是 "UI/LoginPanel" string assetAddress = "UI/LoginPanel"; // 异步加载资源 AssetOperationHandle handle = YooAssets.LoadAssetAsync<GameObject>(assetAddress); yield return handle; // 等待加载完成 if (handle.Status == EOperationStatus.Succeed) { GameObject prefab = handle.AssetObject as GameObject; if (prefab != null) { GameObject instance = Instantiate(prefab); instance.transform.SetParent(this.transform, false); Debug.Log("热更资源加载并实例化成功!"); } // 注意:handle 需要在你确定不再需要该资源时手动释放,或者使用子句柄管理。 // 对于场景中长期存在的对象,可以在对象销毁时释放:handle.Release(); // 如果只是临时加载实例化,可以这样: // handle.Release(); } else { Debug.LogError($"加载资源失败: {handle.Error}"); } } }加载场景:如果热更的内容是整个场景,YooAsset也支持。
// 假设场景地址是 "Scenes/Level2" string sceneAddress = "Scenes/Level2"; var sceneHandle = YooAssets.LoadSceneAsync(sceneAddress); yield return sceneHandle; // sceneHandle 会返回一个 SceneOperationHandle,用于管理场景的加载和卸载加载Sprite、AudioClip等其他类型资源:方法完全一样,只需改变泛型参数<T>。
重要心得:资源句柄(AssetOperationHandle)的管理YooAsset的加载API返回的是一个“句柄”(Handle),而不是资源本身。这个设计非常强大,它提供了引用计数、依赖管理、异步操作状态查询等功能。但同时也要求开发者有意识地去管理它的生命周期。
- 规则:每个
LoadAssetAsync调用,都必须有对应的Release()调用,否则会导致资源一直留在内存中,造成内存泄漏。- 简易管理策略:
- 对于场景中永久的对象(如常驻UI),可以在该UI的
OnDestroy方法中释放其加载句柄。- 对于临时对象(如弹出的对话框),可以在对话框关闭销毁时释放句柄。
- 使用
AssetsLoader组件:YooAsset示例中提供了一个AssetsLoader组件,将它挂载在需要加载资源的GameObject上,配置好资源地址,它会在Awake时加载,OnDestroy时自动释放,非常方便。我强烈推荐在项目中使用或参考它来实现自动化的句柄管理。
6. 构建Android测试包与真机调试
代码写好了,我们需要打包一个Android APK,并让它从我们的本地服务器更新资源,完成端到端的测试。
6.1 构建包含YooAsset运行时的APK
- 在Unity中,打开
File -> Build Settings,选择Android平台,点击Switch Platform。 - Player Settings 关键配置:
Other Settings->Identification->Package Name:设置一个合适的包名,如com.yourcompany.hotfixdemo。Other Settings->Configuration->Scripting Backend:建议使用IL2CPP以获得更好的性能和安全性。Other Settings->Target Architectures:根据需求勾选ARMv7和ARM64。现在新设备基本都是64位,可以只勾ARM64以减小包体。Publishing Settings->Minify:发布版本建议开启Proguard(对于IL2CPP是Release模式),以混淆代码。
- 回到
Build Settings窗口,确保将包含ResourceManager和初始场景添加到Scenes In Build。 - 点击
Build,选择一个输出路径,生成APK文件。
6.2 真机测试与ADB调试
将APK安装到Android手机或模拟器上。关键一步:要让手机能访问到你电脑上运行的本地服务器(http://localhost:8080)。
- 如果使用USB真机调试:
localhost或127.0.0.1在手机端指的是手机自己,而不是你的电脑。你需要找到你电脑在局域网内的IP地址(在Windows上ipconfig,在Mac/Linux上ifconfig),假设是192.168.1.100。那么,在ResourceManager脚本中,hostServerURL就应该改为http://192.168.1.100:8080。同时确保电脑的防火墙允许8080端口的入站连接。 - 如果使用Android模拟器(如Android Studio AVD):模拟器有一个特殊的别名
10.0.2.2来指代宿主机的localhost。所以hostServerURL可以设置为http://10.0.2.2:8080。
修改代码中的服务器地址后,重新构建APK并安装。
运行测试:
- 启动游戏,观察日志。你应该能看到“YooAsset初始化完成”,然后进入更新检查。
- 如果网络通畅,游戏会从你设置的URL下载
version.txt,比对版本,然后下载PackageName.manifest和所需的.bundle文件。 - 下载完成后,游戏加载热更资源成功。
使用ADB查看日志:在测试过程中,查看Unity的Console窗口可能不方便(如果发布的是非Development Build)。我们可以使用Android Debug Bridge (ADB) 来抓取手机上的日志。
- 确保手机开启了USB调试,并用USB连接电脑。
- 打开命令行终端,输入:
adb logcat -s Unity。这会过滤出Unity引擎输出的日志。 - 运行你的游戏,所有
Debug.Log的信息都会在这里显示,方便你排查更新下载进度、错误等信息。
6.3 模拟增量更新
现在我们来测试热更的核心价值:增量更新。
- 在Unity编辑器中,修改你的热更资源。比如,修改
Assets/GameRes/UI/LoginPanel.prefab上的一个文本颜色。 - 打开
AssetBundleBuilderSetting,将Build Version改为1.0.1。 - 再次执行构建(
YooAsset/AssetBundle Builder)。这次不要勾选Clear Build Output,YooAsset会进行增量构建,只重新打包发生了变化的资源,这能极大缩短构建时间。 - 构建完成后,将
D:\UnityBuildOutput\Android\1.0.1整个文件夹复制到你的Python服务器目录下(即Android/文件夹内,与1.0.0并列)。 - 修改服务器根目录下的
version.txt文件,将内容改为1.0.1。 - 在手机上再次运行游戏(不要卸载重装)。游戏会检测到新版本(1.0.1),然后下载新的资源清单和仅发生变化的那个bundle文件(而不是全部)。下载完成后,你看到的LoginPanel就应该是修改后的新样式了。
这个过程完美演示了热更如何节省玩家流量和更新时间。
7. 实战避坑指南与进阶技巧
走通了基本流程,但在实际项目开发中,你会遇到更多细节问题。这里分享一些我踩过的坑和总结的经验。
7.1 资源依赖与打包策略优化
问题:为什么我明明只改了一个小图片,却要下载一个几十兆的大bundle?原因:很可能是因为你的资源打包策略导致修改的资源和其他大量资源被打在同一个bundle里。YooAsset的打包规则是基于收集器的。如果多个不相关的资源都在同一个收集路径下,并且使用了PackTogether规则,它们就会在一个bundle里。
优化策略:
- 按功能模块划分收集器:为UI、角色、场景、音效等分别创建收集器,并设置不同的
Pack Rule(如按文件夹名打包PackDirectory或按显式标签分组)。 - 善用资源标签(Asset Tags):在收集器配置中,可以为资源打上标签。运行时,你可以通过标签来批量加载或卸载一组资源,这对于管理一个功能模块的所有资源非常方便。
- 关注构建报告:每次构建后,仔细查看构建报告中的“资源依赖”视图和“Bundle列表”,确保没有意外的巨大bundle或不合理的依赖。
7.2 版本管理与灰度发布
简单版本管理:我们上面用的version.txt是最简单的方案。更健壮的做法是使用一个JSON文件,例如version.json:
{ "latestVersion": "1.0.1", "minRequiredVersion": "1.0.0", // 最低兼容版本 "updateLog": "修复了登录界面UI错位问题。", "packageSize": 5242880 // 本次更新包总大小,单位字节 }客户端解析这个JSON,可以展示更新日志,并提前告知用户需要下载的大小。
灰度发布:你可以在服务器端做更复杂的逻辑。例如,根据用户ID、设备型号、渠道号等,让version.json返回不同的latestVersion。这样可以实现只对部分用户推送新版本,进行小范围测试。
7.3 下载失败、网络异常与断点续传
我们的示例代码已经开启了断点续传(CreateResourceDownloader的最后一个参数为true)。这能有效应对网络不稳定或用户中途退出的情况。但还需要考虑:
- 超时设置:
CreateResourceDownloader的第4个参数是超时时间(秒)。对于移动网络,可以设置得稍长一些,比如60秒。 - 重试机制:YooAsset的下载器在单次下载失败后不会自动重试。你可以在
packageDownloader.Status == EOperationStatus.Failed时,加入一个重试逻辑,比如重试3次,每次间隔2秒。 - 弱网提示:在下载循环中,如果下载速度持续很低(比如通过
packageDownloader.DownloadedBytes的变化计算),可以给玩家一个“网络状况不佳”的提示。
7.4 资源安全与校验
- 清单文件校验:我们下载了
PackageName.hash来校验PackageName.manifest的完整性,这是YooAsset内置的。 - Bundle文件校验:YooAsset在加载bundle时,会使用manifest中记录的哈希值进行校验,如果校验失败会报错。
- 防止篡改(可选):对于敏感资源,可以考虑对构建出的
.bundle文件进行额外的加密,然后在YooAsset加载前进行解密。YooAsset提供了RawFile的加载接口和自定义解密回调的支持,可以用于实现此功能。
7.5 内存管理与资源卸载
热更资源加载到内存后,如果不再使用,需要及时卸载,否则就是内存泄漏。
- 场景卸载:使用
YooAssets.LoadSceneAsync加载的场景,在切换场景时,YooAsset会自动卸载该场景相关的资源(如果该场景不是常驻的)。 - 资源卸载:
- 手动释放:调用
AssetOperationHandle.Release()。每调用一次LoadAssetAsync,就应该在适当的时候调用一次Release。 - 自动释放(谨慎使用):YooAsset提供了
ResourcePackage.UnloadUnusedAssets()方法,类似于Resources.UnloadUnusedAssets(),它会卸载所有引用计数为0的资源。注意:频繁调用此方法可能导致卡顿,建议在场景切换的加载间隙或收到内存警告时调用。 - 使用引用管理工具:对于复杂的项目,建议设计一个简单的资源引用管理系统,跟踪场景中GameObject对资源的引用,在GameObject销毁时自动释放对应的句柄。
- 手动释放:调用
7.6 调试与日志
在开发阶段,开启YooAsset的详细日志有助于排查问题。
// 在初始化YooAsset之前调用 YooLogger.EnableLog = true; // 开启引擎日志 YooAssets.LogLevel = LogLevel.Log; // 设置日志级别,Log会输出所有信息,Warning只输出警告和错误发布版本时,记得关闭日志以减少性能开销和日志泄露风险。
最后,关于热更,我想再强调一点:热更是为了修复问题和优化体验,而不是用来发布全新的大型玩法内容。对于全新的、结构发生巨大变化的资源(比如新增了一个Shader,或者改变了资源地址的映射规则),可能仍然需要强更(即通过应用商店更新APK)。在设计热更策略时,一定要明确热更和强更的边界。