择一业,爱一生
目录
[1. MainAssetCollector(主资源收集器)](#1. MainAssetCollector(主资源收集器))
[2. DependAssetCollector(依赖资源收集器)](#2. DependAssetCollector(依赖资源收集器))
[3. StaticAssetCollector(静态资源收集器)](#3. StaticAssetCollector(静态资源收集器))
[Boot ------ 一切的起点](#Boot —— 一切的起点)
[FSM 状态机 ------ Patch 的骨架](#FSM 状态机 —— Patch 的骨架)
[Event 系统 ------ 真正的"解耦灵魂"](#Event 系统 —— 真正的“解耦灵魂”)
[PatchWindow ------ 玩家眼中的"热更系统"](#PatchWindow —— 玩家眼中的“热更系统”)
[GameManager 对外唯一话事人](#GameManager 对外唯一话事人)
[1. 底层:YooAsset 资源库(第三方)](#1. 底层:YooAsset 资源库(第三方))
[2. 核心调度层(热更流程中枢)](#2. 核心调度层(热更流程中枢))
[3. 全局业务管理层](#3. 全局业务管理层)
[4. 表现层 UI](#4. 表现层 UI)
[5. 程序入口](#5. 程序入口)
[阶段 1:游戏初始化(Boot)](#阶段 1:游戏初始化(Boot))
[阶段 2:资源包初始化(FsmInitializePackage)](#阶段 2:资源包初始化(FsmInitializePackage))
[阶段 3:请求远端版本号(FsmRequestPackageVersion)](#阶段 3:请求远端版本号(FsmRequestPackageVersion))
[阶段 4:下载资源清单 Manifest(FsmUpdatePackageManifest)](#阶段 4:下载资源清单 Manifest(FsmUpdatePackageManifest))
[阶段 5:统计需要更新的资源(FsmCreateDownloader)](#阶段 5:统计需要更新的资源(FsmCreateDownloader))
[阶段 6:资源下载(FsmDownloadPackageFiles)](#阶段 6:资源下载(FsmDownloadPackageFiles))
[阶段 7:下载收尾 + 清理缓存](#阶段 7:下载收尾 + 清理缓存)
[阶段 8:进入游戏主流程(FsmStartGame)](#阶段 8:进入游戏主流程(FsmStartGame))
[阶段 9:场景切换(游戏内)](#阶段 9:场景切换(游戏内))
[1. 简述这套「UniEvent + FSM + YooAsset」热更新整体架构分层,每层职责是什么?](#1. 简述这套「UniEvent + FSM + YooAsset」热更新整体架构分层,每层职责是什么?)
[2. 为什么要用 UniEvent 事件总线解耦?直接调用方法不好吗?说出 3 个优势。](#2. 为什么要用 UniEvent 事件总线解耦?直接调用方法不好吗?说出 3 个优势。)
[3. FSM 有限状态机相比 if/while 顺序流程写热更,优势在哪?](#3. FSM 有限状态机相比 if/while 顺序流程写热更,优势在哪?)
[4. YooAsset 四种运行模式(EPlayMode)分别适用什么场景?初始化有什么核心区别?](#4. YooAsset 四种运行模式(EPlayMode)分别适用什么场景?初始化有什么核心区别?)
[5. 看代码:FsmDownloadPackageFiles 里给 downloader 绑定 DownloadError、DownloadProgressChanged 委托,会存在什么隐患?如何修复?](#5. 看代码:FsmDownloadPackageFiles 里给 downloader 绑定 DownloadError、DownloadProgressChanged 委托,会存在什么隐患?如何修复?)
[6. 框架中为什么所有临时数据(PackageName、Downloader、PackageVersion)存在 FSM 黑板,不用静态全局变量?](#6. 框架中为什么所有临时数据(PackageName、Downloader、PackageVersion)存在 FSM 黑板,不用静态全局变量?)
[7. 代码里 GameManager 统一托管所有协程,不把 Mono 传给 FSM,设计目的是什么?](#7. 代码里 GameManager 统一托管所有协程,不把 Mono 传给 FSM,设计目的是什么?)
[8. 区分两类事件 PatchXXXEvent / UserXXXEvent,设计上为什么要严格分开,不能混用?](#8. 区分两类事件 PatchXXXEvent / UserXXXEvent,设计上为什么要严格分开,不能混用?)
[9. PatchWindow 里封装 MessageBox 对象池,为什么不直接 Instantiate/Destroy 弹窗?](#9. PatchWindow 里封装 MessageBox 对象池,为什么不直接 Instantiate/Destroy 弹窗?)
[10. 游戏热更时卡在 "请求版本号" 无响应,从这套框架出发,分模块排查思路?](#10. 游戏热更时卡在 “请求版本号” 无响应,从这套框架出发,分模块排查思路?)
[11. 用户反馈重复下载资源,每次启动游戏都全量更新,结合框架和 YooAsset 分析 3 种可能原因?](#11. 用户反馈重复下载资源,每次启动游戏都全量更新,结合框架和 YooAsset 分析 3 种可能原因?)
[12. 弱网环境下,单个 Bundle 下载失败后弹窗重试,但是重试后依旧全部重新下载,是什么问题?如何改造?](#12. 弱网环境下,单个 Bundle 下载失败后弹窗重试,但是重试后依旧全部重新下载,是什么问题?如何改造?)
[13. 现有框架只支持单分包热更,如果要求实现多分包并行热更(主包 + DLC 分包),这套架构哪些地方需要改造?](#13. 现有框架只支持单分包热更,如果要求实现多分包并行热更(主包 + DLC 分包),这套架构哪些地方需要改造?)
[14. 现有框架缺少「下载暂停 / 继续 / 取消」功能,基于现有 FSM + 事件架构,如何最小改动扩展?](#14. 现有框架缺少「下载暂停 / 继续 / 取消」功能,基于现有 FSM + 事件架构,如何最小改动扩展?)
[游戏 / 资源热更场景常见 FSM 状态机设计模式?](#游戏 / 资源热更场景常见 FSM 状态机设计模式?)
我们先来解析看看官方给的案例如何资源管理的吧!
Editor

具体来看看官方都做了哪些操作吧,也当作复习上篇文章的知识点
彻底搞懂YooAsset!(资源管理!热更新框架!)-CSDN博客

-
Show Package
是否展示资源包列表视图。
-
Show Editor Alias
是否显示为中文模式。
-
Unique Bundle Name
资源包名追加PackageName作为前缀。
它这里都是默认空着,确实项目中也只有一个packages

- Enable Addressable(启用可寻址资源定位系统)
它开启了,意思是可以用短地址 / 别名加载(按你配置的寻址规则)
按你配置的寻址规则的意思就是按照如下 ,你配置的 
- Location To Lower (资源定位地址大小写不敏感)
它没开
Include Asset GUID (资源清单里包含资源 GUID 信息)
它没开:
- 只靠路径 / 别名匹配,改名就找不到。
- Auto Collect Shaders(将所有着色器构建到独立的资源包内)
它开了
- File Ignore Rule(文件全局忽略规则)
它选的是NormalIgnoreRule(适配常规资源构建管线)




一个包它分为了四个组,都启用了分组激活------
它决定了这个分组在打包时是否生效,控制整个 Group 里的资源要不要被收集、打包。
EnableGroup:启用分组
- 打包时,这个 Group 下的所有 Collector 都会被执行,资源会被收集并打包。
CollectorType收集器类型:
- MainAssetCollector 主资源收集器
- StaticAssetCollector 静态资源收集器
- DependAssetCollector 依赖资源收集器
AddressRuleName可寻址规则(配合 Enable Addressable 开关):
- AddressDisable 禁用可寻址,只能用完整资源路径加载。
- AddressByFileName(新手首选) 用不带后缀的文件名当寻址名,例:Assets/UI/Icon.png → 寻址名 Icon 代码直接 LoadAsset<Sprite>("Icon"),写法最简。
- AddressByGroupAndFileName 分组名 + 文件名,例:ui_Icon,适合多分组重名资源防冲突。
- AddressByFolderAndFileName 文件夹名 + 文件名,层级区分更细。
PackRule 打包规则(决定文件如何合并成 AB 包)
- PackSeparately 每个文件单独打成一个包。 适用:独立大资源、极少改动的资源。
- PackDirectory 按子文件夹分包,一个子目录一个包。
- PackTopDirectory(常用) 按顶级目录分包,收集路径下第一层文件夹各自成包。
- PackCollector 当前收集器下所有文件合并成一个大包。 适用:整体更新的模块,不建议大目录使用(改动一个文件全包更新)。
- PackGroup 整个 Group 下所有收集器的资源合并为一个包。
- PackRawFile 原生文件打包(.json/.csv/.txt 等),搭配 RawFileIgnoreRule 使用。
- PackShader / PackShaderVariants 专门打包着色器 / 着色器变种,配合全局 Auto Collect Shaders 使用。
FilterRule 过滤规则(筛选要收集的文件)
- CollectAll :收集目录内所有资源(通用默认)
- CollectScene :仅收集场景
.unity文件 - CollectPrefab :仅收集预制体
.prefab - CollectSprite:仅收集精灵图
- CollectShader:仅收集着色器
- CollectShaderVariants
我们来探索一下官方的配置吧:
特效和实体和UI面板:
MainAssetCollector 主资源收集器
AddressByFileName
PackSeparately 每个文件单独打成一个包
CollectPrefab :仅收集预制体
.prefab
音乐和图集:
MainAssetCollector 主资源收集器
AddressByFileName
PackSeparately 每个文件单独打成一个包
CollectAll :收集目录内所有资源
特效材质和实体材质和UI面板美术和UI面板字体:
DependAssetCollector 依赖资源收集器
AddressByFileName
PackDirectory 按子文件夹分包,一个子目录一个包。
CollectAll :收集目录内所有资源
着色器
StaticAssetCollector 静态资源收集器
AddressByFileName
PackShader
CollectShader:仅收集着色器
着色器变体
StaticAssetCollector 静态资源收集器
AddressByFileName
PackShaderVariants
CollectShaderVariants
场景
StaticAssetCollector 静态资源收集器
AddressByFileName
PackSeparately 每个文件单独打成一个包
CollectScene :仅收集场景
.unity文件
场景美术
DependAssetCollector 依赖资源收集器
AddressByFileName
PackDirectory 按子文件夹分包,一个子目录一个包。
CollectAll :收集目录内所有资源
UI精灵图片
StaticAssetCollector 静态资源收集器
AddressByFileName
PackDirectory 按子文件夹分包,一个子目录一个包。
CollectSprite:仅收集精灵图
1. MainAssetCollector(主资源收集器)
资源写入资源清单 Manifest ,运行时可以用 LoadAssetAsync("短地址") 直接加载。 游戏里你业务代码主动加载的东西全部放这里:UI 面板、特效预制体、实体角色、音乐、图集、场景文件。
2. DependAssetCollector(依赖资源收集器)
不写入清单,不能直接代码加载,只会被主资源自动依赖引用打包。 专门放材质、贴图、场景美术素材这类「被 Prefab / 场景间接引用的资源」,避免主包重复冗余。 只有当某个 Main 资源引用它,它才会打进包;没任何主资源引用则自动剔除不打包。
3. StaticAssetCollector(静态资源收集器)
不写入清单,不能直接代码加载,强制参与打包,不会被自动剔除。 专门放 Shader、Shader 变体、UI 精灵图这类全局共用底层资源,不管有没有引用都强制打包。
PackRule 分包规则(怎么打成 AB 包)
- PackSeparately:每个文件单独一个 AB 包
- PackDirectory:按子文件夹分包,一个文件夹合并成一个 AB
- PackShader:所有 Shader 统一打包到专用着色器包
接下来,我为你逐段解释你这份配置的设计思路
1. 特效、实体、UI 面板、音乐、图集 ------ MainAssetCollector + PackSeparately
- 为什么用 Main :这些是业务代码主动加载的核心资源(UI 面板、角色预制体、特效、背景音乐、图集),必须写入清单,支持短地址加载。
- 为什么 PackSeparately 单文件分包 :
- UI、活动面板、角色、特效经常局部更新,玩家只需要下载改动的单个 AB,不用整目录大包;
- 资源粒度细,卸载精准,不用加载整个文件夹一堆不用的资源,减少内存占用。
- CollectPrefab:只抓预制体,过滤掉里面的贴图、材质(交给 Depend 收集器统一管理);音乐图集用 CollectAll,目录内音频、图集全部纳入主资源。
2. 特效材质、实体材质、UI 字体、场景美术 ------ DependAssetCollector + PackDirectory
- 为什么 Depend :材质、字体、场景贴图不会在代码直接 Load,只会被 UI / 角色 / 场景预制体自动依赖;放到 Depend 里,YooAsset 自动分析依赖,自动打包,不会重复复制到每个主包造成冗余。
- 为什么 PackDirectory 按文件夹合并分包: 同一模块材质高度复用(比如 UI 全部字体、怪物全部材质),合并成一个包,减少 AB 文件数量,降低 IO 开销; 且材质很少单独更新,整目录打包不影响热更体积。
- 不会写入清单,代码不能
LoadAssetAsync("mat_skin")加载材质,只能加载预制体自动带出材质。
3. 着色器、着色器变体 ------ StaticAssetCollector + PackShader
- 为什么 Static :Shader 是全局底层资源,所有材质都依赖,强制打包,哪怕没任何资源引用也要打进包;不写入清单,不能代码直接加载。
- 专用 PackShader 规则:Unity Shader 打包有特殊要求,统一合并到独立着色器大包,避免多包重复 Shader、渲染报错、变体丢失。
- 全局常驻资源,游戏启动就加载,几乎不更新,单独打包方便整包缓存。
4. 场景文件 ------ StaticAssetCollector + PackSeparately
- 场景用 Static 而非 Main:场景体积巨大,独立静态打包;也可以用 Main,这里是项目选型(场景不做短地址直接加载,用场景加载接口)。
- PackSeparately 单场景分包:关卡独立更新,切换关卡只加载对应场景 AB,不加载其他关卡资源。
5. UI 精灵图片 ------ StaticAssetCollector + PackDirectory
- 精灵图是图集 / UI 面板的底层依赖贴图,全局共用,强制打包,放 Static;
- 按文件夹合并分包:同 UI 模块的精灵合并,减少包数量,贴图极少单独更新。
分层分工(行业标准最优分层)
- 业务可加载层(Main):玩家代码主动加载的预制体、UI、音乐、图集 → 单文件分包,方便热更、精准卸载,支持短地址寻址。
- 依赖素材层(Depend):材质、场景贴图、字体 → 自动跟随主资源打包,文件夹合并分包,去冗余。
- 底层基础层(Static):Shader、精灵贴图、场景 → 强制全局打包,按资源特性选择合并 / 单独分包,不暴露给代码直接加载。
分包策略选择规律
- 频繁单独更新、需要精准卸载 → PackSeparately(UI 面板、角色、特效、场景)
- 高度复用、极少单独更新 → PackDirectory(材质、贴图、字体)
- 特殊底层渲染资源 → PackShader(着色器专用打包规则)
避坑:
- 改动任意收集器规则后,必须重新收集 + 构建资源包,新寻址、分包规则才会生效;
- Main 收集器内不能存在同名文件(AddressByFileName 会地址冲突,打包报错)
接下来我们看他的代码怎么写的吧!
Runtime
我尽量就不给大家贴大篇幅代码了,说一下思路吧
Boot 点火 → PatchManager 指挥 → FSM 状态机推进 → YooAsset 干活 → Event 广播 → PatchWindow 翻译给玩家看

Boot ------ 一切的起点
Boot(程序启动入口 Mono,游戏第一个执行脚本)
执行顺序(Start 方法)
- 设置帧率、后台运行、永不销毁自身
- 初始化 UniEvent 事件框架、YooAsset 资源框架
- 加载并实例化 PatchWindow 热更 UI
- 创建 PatchManager 状态机、启动热更流程
帧更新
Update()每帧驱动PatchManager.Update(),让 FSM 持续运转
FSM 状态机 ------ Patch 的骨架
所有节点实现
IStateNode,由PatchManager创建并管理状态切换,状态之间通过黑板 Blackboard 共享数据 流转顺序(标准热更流程):FsmInitializePackage 异步初始化资源包,运行模式选择对应的文件系统策略
↓
FsmRequestPackageVersion 本状态的唯一职责:问服务器:"当前最新的资源版本号是多少?"
↓
FsmUpdatePackageManifest 从服务器下载并加载"资源清单(Manifest)
↓
FsmCreateDownloader 计算需要下载的资源 这一步不做下载,只做"算账"
├─ 无更新 → FsmStartGame
└─ 有更新 → 等待 UI 确认
↓(用户点击)
FsmDownloadPackageFiles 这个状态负责"真正开始下载资源"
↓
FsmDownloadPackageOver "资源包下载完成"时的一个状态节点
↓
FsmClearCacheBundle 异步清理 YooAsset 中未被使用的缓存 Bundle
↓
FsmStartGame 进入启动游戏状态每个 FSM 职责与对外交互
- FsmInitializePackage
- 职责:根据
PlayMode区分编辑器 / 单机 / 热更 / 微信小游戏 / WebGL,初始化 YooAsset 资源包- 输出:初始化失败发送
PatchInitializeFailedEvent;成功切下一状态- 依赖:黑板
PackageName、PlayMode;调用 YooAsset 初始化 API- FsmRequestPackageVersion
- 职责:请求 CDN 上最新资源版本号,存入黑板
- 输出:请求失败发送
PatchPackageVersionRequestFailedEvent;成功切更新清单状态- FsmUpdatePackageManifest
- 职责:用远端版本号下载资源清单 Manifest
- 输出:下载失败发送
PatchPackageManifestUpdateFailedEvent;成功进入计算下载列表- 依赖:
GameManager提供协程运行器执行异步下载- FsmCreateDownloader
- 职责:对比本地缓存和 Manifest,生成下载器,统计需下载文件数量大小
- 输出:有更新则发送
PatchFoundUpdateFilesEvent(UI 弹窗询问玩家);无更新直接跳清理缓存- 数据输出:下载器存入黑板供下载状态使用
- FsmDownloadPackageFiles
- 职责:真正执行资源下载,绑定下载进度 / 错误回调
- 回调绑定:
- 下载进度 →
PatchDownloadUpdatedEvent- 单文件下载失败 →
PatchWebFileDownloadFailedEvent- 依赖:黑板的 Downloader;GameManager 协程运行下载逻辑
- FsmDownloadPackageOver
- 职责:下载完成过渡节点,打印完成提示,直接跳转清理缓存
- FsmClearCacheBundle
- 职责:异步清理本地无用旧 Bundle 缓存,释放存储空间
- 完成后进入启动游戏状态
- FsmStartGame
- 职责:热更全部流程结束,把资源包交给 GameManager,发送切主城场景事件
Event 系统 ------ 真正的"解耦灵魂"
两类 Event(一定要分清)
| 类型 | 命名特征 | 谁发 | 谁听 |
|---|---|---|---|
| 系统事件 | PatchXXXEvent |
FSM / YooAsset | PatchWindow |
| 用户事件 | UserTryXXXEvent |
UI / Retry | PatchManager |
事件流向示意
FSM
└─PatchFoundUpdateFilesEvent
↓
PatchWindow
└─ 用户点 OK
↓
UserBeginDownloadWebFilesEvent
↓
PatchManager
↓
FSM
FSM 永远不直接碰 UI
UI 永远不直接碰 FSM
看一下在代码中如何实现的吧!!!
PatchFoundUpdateFilesEvent

在这里监听着,只要监听到有东西发送了

具体执行的是以下逻辑:告诉弹窗,你需要给用户什么提示

UserBeginDownloadWebFilesEvent

监听事件


PatchWindow ------ 玩家眼中的"热更系统"
(UGI 热更 UI 窗口,Mono)
PatchWindow 的职责边界
只做三件事:
监听 PatchXXXEvent
****显示进度 / 弹窗
把玩家操作转成 UserTryXXXEvent
绝不:
操作 YooAsset
切换 FSM
执行业务逻辑
单向通信逻辑
- 接收(FSM→UI) :
EventGroup监听所有 Patch 开头的流程反馈事件
- 收到进度事件:更新 Slider 进度、文字提示
- 收到各类失败 / 发现更新事件:弹出 MessageBox 弹窗,展示提示文字
- 发送(UI→PatchManager) :弹窗点击 OK 后,发送对应
UserTryXXX事件,通知 PatchManager 切换 FSM 状态内部封装
MessageBox弹窗对象池,复用弹窗物体,避免频繁实例化销毁;OnDestroy自动清空所有事件监听,防内存泄漏
我们来看一下我们的
GameManager 对外唯一话事人
两大核心能力
-
资源包统一管理:缓存主ResourcePackage,对外提供加载场景接口
-
协程托管:持有 MonoBehaviour 运行器,给 FSM 提供协程执行能力(YooAsset 异步操作需要协程)
事件监听
内部EventGroup监听SceneChangeToHomeEvent/SceneChangeToBattleEvent,收到事件后调用资源包加载对应场景
依赖注入
Boot理论上需要调用GameManager.Instance.SetBehaviour(this)注入协程载体(代码里注释了);FsmUpdatePackageManifest等状态直接调用GameManager.Instance.StartCoroutine执行异步逻辑
UI / 玩法系统
↓(只认识 GameManager)
GameManager
↓
YooAsset / Coroutine / Event
↓
Patch / 资源 / 场景
来解释以下四个疑问点,你就都懂了
SetGamePackage()------ 为什么不让 GameManager 自己创建?
表面作用
把 已经初始化完成的 Package 交到 GameManager 手里
深层设计原因(非常重要)
职责分离(SRP)
模块 职责 PatchManager 负责"怎么拿到 Package" FSM / YooAsset 负责"初始化 Package" GameManager 负责"以后用 Package 干什么" Package 是"战略资源"
创建一次
全局唯一
生命周期长
不能乱建
所以:
谁创建,谁负责;GameManager 只负责"接盘"
SetBehaviour()------ 为什么 MonoBehaviour 要单独注入?
Unity 的铁律
只有 MonoBehaviour 才能 StartCoroutine
如果没有这个设计会怎样?
GameManager 自己不是 MonoBehaviour
想开协程 → 编译不过
强耦合一个 GameObject → 架构脏了
这是非常专业的解耦手法,很多项目都做不到这一点。
StartCoroutine()------ 为什么要包一层?
调用方 不用关心协程怎么跑
以后你可以换成:
UniTask
CoroutineUtility自己的 Task 系统
这一层是"未来可替换层"
没有这一层时,世界是什么样?
必须是 MonoBehaviour,否则编译不过
有了这一层后,世界变成什么样?
调用方脑子里只有一句话:
"我想异步执行一段逻辑,剩下的别让我管"
它不知道、也不需要知道:
是不是
MonoBehaviour.StartCoroutine是不是
UniTask是不是自定义 Task 调度器
是不是在主线程
为什么说「未来可以换」?
今天(Unity Coroutine)
明天(换成 UniTask)
public UniTask StartCoroutine(IEnumerator enumerator) { return _behaviour.StartCoroutineAsUniTask(enumerator); }
或者public UniTask StartAsync(Func<CancellationToken, UniTask> task)
{
return task(default);
}
调用方一行都不用改
GameManager.Instance.StartCoroutine(LoadSomething());
GameManager.StartCoroutine 不是"为了方便调用",而是为了"让调用方永远不用知道协程是怎么跑的"。
这就是经典的 Adapter 模式
Adapter(适配器)模式 的本质
我不改原来的东西,但我包一层,让它"长得像我想要的那样"
原始问题(Unity 的限制)
// 只有 MonoBehaviour 能用
StartCoroutine(LoadSomething());
但你的 GameManager 不是 MonoBehaviour
写的 Adapter
public void StartCoroutine(IEnumerator enumerator)
{
_behaviour.StartCoroutine(enumerator);
}
它干了什么?
角色 是谁 被适配者(Adaptee) MonoBehaviour.StartCoroutine目标接口(Target) GameManager.StartCoroutineAdapter GameManager这一层你没改 Unity
也没强迫 GameManager 继承 MonoBehaviour
你只是"包了一下"
这就是 100% 标准 Adapter 模式
什么叫「对外的话事人」?
一个模块如果是对外的话事人,必须满足这 4 件事:
标准 是否满足 对外暴露稳定 API 满足 屏蔽底层实现细节 满足 所有"业务系统"只认它 满足 底层改动不影响业务 满足 哪些事「必须」经过 GameManager?
业务需求 正确入口 加载主界面 SceneChangeToHomeEvent→ GameManager进入战斗 SceneChangeToBattleEvent→ GameManager加载 YooAsset 资源 GameManager.Instance.GamePackage启动协程 GameManager.Instance.StartCoroutine()获取当前 Package GameManager.Instance.GamePackage预加载 / 卸载资源 GameManager 包装
那我们来总结一下
热更框架全流程:
四层架构总览(从底层资源→调度→UI 入口)
1. 底层:YooAsset 资源库(第三方)
所有资源初始化、版本请求、Manifest 下载、文件下载、缓存清理均由 YooAsset 提供 API,业务代码只做封装调度。
2. 核心调度层(热更流程中枢)
PatchManager:静态总控制器,FSM 状态机入口、事件分发中枢- 一批
FsmXXX状态节点:有限状态机,拆分每一步热更逻辑(初始化包→请求版本→更新清单→计算下载→下载文件→清理缓存→进入游戏) - 全部
XXXEvent事件类:解耦通信载体 ,分两类:- 用户操作事件(UI→PatchManager):
UserTryXXXEvent - 流程反馈事件(FSM→UI 窗口):
PatchXXXEvent、场景切换事件SceneChangeXXXEvent
- 用户操作事件(UI→PatchManager):
3. 全局业务管理层
GameManager:游戏全局单例,持有主资源包、提供协程运行器、监听场景切换事件、负责场景加载
4. 表现层 UI
PatchWindow:热更进度 UI,只监听流程反馈事件,弹窗 / 进度条展示,点击按钮发送用户操作事件
5. 程序入口
Boot:启动脚本,游戏第一执行脚本,初始化 UniEvent/YooAsset、实例化 UI、创建并启动热更流程
游戏启动全链路
阶段 1:游戏初始化(Boot)
- 打开游戏 → 执行Boot.Awake → Boot.Start
- 初始化 UniEvent、YooAssets
- 生成 PatchWindow UI 窗口
- PatchManager.Create("DefaultPackage", PlayMode)
- 创建状态机,注册全部 FSM 节点
- 黑板存入包名、运行模式
- 监听所有 User 操作事件
- PatchManager.Start() → FSM 进入FsmInitializePackage
阶段 2:资源包初始化(FsmInitializePackage)
- 根据 PlayMode 区分编辑器 / 单机 / 热更 / 小游戏,初始化 Package
- 初始化失败:发送PatchInitializeFailedEvent
- PatchWindow 收到事件,弹窗提示初始化失败,按钮发送UserTryInitializePackageEvent
- PatchManager 接收 User 事件,切回初始化状态重试
- 初始化成功 → 切换FsmRequestPackageVersion
阶段 3:请求远端版本号(FsmRequestPackageVersion)
- 调用 YooAsset 请求 CDN 版本文件
- 请求失败:发送PatchPackageVersionRequestFailedEvent,弹窗重试
- 请求成功:版本号存入黑板 → 切换FsmUpdatePackageManifest
阶段 4:下载资源清单 Manifest(FsmUpdatePackageManifest)
- 用 GameManager 协程执行异步下载 Manifest
- 下载失败:发送PatchPackageManifestUpdateFailedEvent,弹窗重试
- 成功 → 切换FsmCreateDownloader
阶段 5:统计需要更新的资源(FsmCreateDownloader)
- 创建 Downloader,对比本地与远端资源
- 无更新文件:直接跳转FsmClearCacheBundle
- 存在更新:发送PatchFoundUpdateFilesEvent
- PatchWindow 弹窗展示文件数量、总大小
- 玩家点击确认 → 发送UserBeginDownloadWebFilesEvent
- PatchManager 接收事件,切换FsmDownloadPackageFiles开始下载
阶段 6:资源下载(FsmDownloadPackageFiles)
- 绑定下载进度、下载失败回调
- 每帧下载进度变化:发送PatchDownloadUpdatedEvent → UI 刷新进度条
- 单个文件下载失败:发送PatchWebFileDownloadFailedEvent → 弹窗,可发送UserTryDownloadWebFilesEvent重试下载
- 全部文件下载完成 → 切换FsmDownloadPackageOver
阶段 7:下载收尾 + 清理缓存
- FsmDownloadPackageOver:打印完成提示,直接切FsmClearCacheBundle
- FsmClearCacheBundle:异步删除本地废弃 Bundle,完成后切FsmStartGame
阶段 8:进入游戏主流程(FsmStartGame)
- 将资源包交给 GameManager 统一管理
- 发送SceneChangeToHomeEvent
- GameManager 监听场景事件,加载主城场景,热更流程全部结束
阶段 9:场景切换(游戏内)
玩家主城 / 战斗切换时,外部逻辑发送SceneChangeToBattleEvent,GameManager 接收后加载对应场景
为了大家加深印象,我想了几个问答题!大家可以自行读一读
1. 简述这套「UniEvent + FSM + YooAsset」热更新整体架构分层,每层职责是什么?
参考答案要点: 四层分层:启动层 Boot、调度层 PatchManager+FSM、资源管理层 GameManager、表现层 PatchWindow; 两套事件隔离:PatchXXXEvent(流程→UI)、UserXXXEvent(UI→流程); FSM 黑板 Blackboard 做状态间数据共享,全程单向依赖,UI 不碰底层资源逻辑。
2. 为什么要用 UniEvent 事件总线解耦?直接调用方法不好吗?说出 3 个优势。
参考答案:
- 完全解耦:UI 不需要持有 PatchManager、FSM 引用,底层流程不用操作 UI 控件;
- 一对多广播:一个流程进度事件可同时给 UI、日志、埋点多端消费;
- 生命周期统一管理:EventGroup 批量添加 / 移除监听,避免漏删监听造成重复回调、内存泄漏。
3. FSM 有限状态机相比 if/while 顺序流程写热更,优势在哪?
参考答案:
- 单一职责:每个热更步骤独立一个状态类,新增 / 删减流程只增删节点,不污染原有代码;
- 流程可回滚、可重试:出错时通过 User 事件切回对应前置状态,不用写大量重试分支;
- 数据隔离:黑板 Blackboard 存储流程临时数据,不污染全局静态变量,支持多分包并行热更;
- 结构清晰,流程链路标准化,便于维护、排查卡死问题。
4. YooAsset 四种运行模式(EPlayMode)分别适用什么场景?初始化有什么核心区别?
参考答案:
- EditorSimulateMode:编辑器开发调试,虚拟构建,不走真实 CDN 下载;
- OfflinePlayMode:单机离线包,无热更,只读取内置 AB;
- HostPlayMode:移动端 / PC 真机热更,内置清单 + 沙盒缓存双文件系统;
- WebPlayMode:WebGL / 微信小游戏,专用网络文件系统,微信需适配 WX 文件系统。
5. 看代码:FsmDownloadPackageFiles 里给 downloader 绑定 DownloadError、DownloadProgressChanged 委托,会存在什么隐患?如何修复?
考点:委托重复绑定、内存泄漏 参考答案: 隐患:多次进入该状态节点会重复绑定委托,下载一次触发多次弹窗、多次进度刷新;流程结束委托不会自动解绑,产生内存泄漏。 修复方案:
- 在 OnExit 回调解绑委托;
- 在下载完成 Completed 回调里即时解绑;
- 单次操作局部委托,不长期挂载。
6. 框架中为什么所有临时数据(PackageName、Downloader、PackageVersion)存在 FSM 黑板,不用静态全局变量?
考点:全局变量弊端、多包扩展设计 参考答案:
- 静态全局变量单例,无法同时支持多分包、多套独立热更流程;
- 流程重启、重试时全局脏数据残留,容易出现版本错乱、下载器失效;
- 黑板随状态机生命周期销毁,自动回收数据,无内存残留;
- 状态节点完全解耦,不需要互相持有引用传参。
7. 代码里 GameManager 统一托管所有协程,不把 Mono 传给 FSM,设计目的是什么?
参考答案:
- FSM 纯逻辑层,不依赖 MonoBehaviour 生命周期,代码复用性更强;
- 统一协程管理,方便全局暂停、清理所有异步下载协程;
- 资源加载、场景加载统一收口,方便后续加加载超时、埋点、异常拦截。
8. 区分两类事件 PatchXXXEvent / UserXXXEvent,设计上为什么要严格分开,不能混用?
考点:单向数据流设计思想 参考答案:
- PatchEvent:下行消息,FSM / 资源层推送流程状态、进度、报错,UI 只读展示;
- UserEvent:上行指令,玩家交互触发,驱动状态机切换流程;
- 职责隔离,阅读代码时一眼区分 "状态反馈" 和 "流程控制",不会出现 UI 直接下发下载指令、底层随意修改 UI 的反向耦合。
9. PatchWindow 里封装 MessageBox 对象池,为什么不直接 Instantiate/Destroy 弹窗?
参考答案:
- 频繁创建销毁 UGUI 造成 GC 卡顿,热更新界面高频弹窗场景卡顿明显;
- 对象池复用 GameObject、Button 绑定,减少 Transform 查找、组件获取开销;
- 统一管理弹窗生命周期,避免多弹窗层级错乱、重复弹出。
10. 游戏热更时卡在 "请求版本号" 无响应,从这套框架出发,分模块排查思路?
考点:排错逻辑、分层定位 参考答案:
- 查看日志,确认 FSM 是否正常进入 FsmRequestPackageVersion;
- 检查 CDN 地址 GetHostServerURL 平台适配是否正确;
- 网络拦截:是否无法访问.version 文件、http/https iOS ATS 配置;
- 事件监听:PatchPackageVersionRequestFailedEvent 是否正常广播,弹窗是否弹出;
- 状态机驱动:Boot.Update 是否每帧调用 PatchManager.Update,FSM 未轮询卡死。
11. 用户反馈重复下载资源,每次启动游戏都全量更新,结合框架和 YooAsset 分析 3 种可能原因?
参考答案:
- HostPlayMode 初始化未开启 CopyBuiltinPackageManifest,本地无内置清单无法对比版本;
- 清理缓存逻辑错误,使用 ClearAllBundleFiles 而非 ClearUnusedBundleFiles,每次启动删全部缓存;
- CDN 打包版本号固定,每次打包 version 未更新,本地和远端永远不一致。
12. 弱网环境下,单个 Bundle 下载失败后弹窗重试,但是重试后依旧全部重新下载,是什么问题?如何改造?
考点:Downloader 机制理解 参考答案: 原因:原有逻辑发送 UserTryDownloadWebFilesEvent 会重新 CreateDownloader,重新生成完整下载列表,不会断点续传失败文件。 优化:区分 "全部重新下载" 和 "仅重试失败文件",单独提供失败文件重试接口,复用原有下载器只重下失败资源。
13. 现有框架只支持单分包热更,如果要求实现多分包并行热更(主包 + DLC 分包),这套架构哪些地方需要改造?
参考答案:
- PatchManager 不要静态单例,改为实例化,每个分包独立 StateMachine、独立黑板;
- 事件增加 PackageName 标识,区分不同分包的进度、错误事件;
- GameManager 支持多 Package 缓存,对外提供指定分包加载接口;
- UI 层增加多分包进度展示面板,区分不同分包下载进度。
14. 现有框架缺少「下载暂停 / 继续 / 取消」功能,基于现有 FSM + 事件架构,如何最小改动扩展?
参考答案:
- 新增控制事件 UserPauseDownloadEvent、UserResumeDownloadEvent、UserCancelDownloadEvent;
- PatchManager 监听控制事件,传递给 FsmDownloadPackageFiles;
- 下载状态内持有 Downloader,调用 YooAsset Downloader 的 Pause/Resume 接口;
- 不改动原有流程节点,仅新增事件和少量分支判断,符合开闭原则。
加个彩蛋!
游戏 / 资源热更场景常见 FSM 状态机设计模式?
一、枚举 + If/Else 简陋状态机(最入门,新手 Demo)
结构
用
Enum定义所有状态类内一个
CurrentState变量Update 里大量
if else / switch判断执行逻辑
csenum PatchState { InitPackage, RequestVersion, DownloadManifest, DownloadFiles } PatchState _curState; void Update() { switch(_curState) { case PatchState.InitPackage: DoInit(); break; case PatchState.RequestVersion: DoRequestVer(); break; } } void ChangeState(PatchState newState) { // 手动执行退出逻辑、进入逻辑 }优点
零依赖、写得快,小 Demo 一两步流程够用
缺点
状态越多 switch 越臃肿,难以维护
没有统一的 OnEnter/OnExit/OnCreate 生命周期
状态切换逻辑散落在各处,无法统一管理
不支持状态间传参、黑板共享数据
适用
极小流程、一次性脚本,不适合你这种热更工程
二、基类虚函数 FSM(面向对象标准状态模式,GoF 经典 State Pattern)
核心设计(你当前项目用的就是这套)
抽象接口 / 基类
IStateNode,统一生命周期:OnCreate / OnEnter / OnUpdate / OnExit每个状态独立一个类(单一职责)
中央状态机
StateMachine统一管理:
持有当前状态、所有状态实例
提供
ChangeState<T>()切换接口内置黑板 Blackboard 跨状态共享数据
你的代码对应实现
FsmInitializePackage / FsmCreateDownloader全部独立类,实现IStateNode,由PatchManager的StateMachine调度。优点
单一职责,新增流程只加状态类,不修改原有代码(开闭原则)
标准生命周期,进入 / 退出可做资源清理、委托解绑
支持黑板隔离全局数据,支持多套独立状态机
流程可回滚、可重试,热更、登录、战斗流程首选
缺点
类数量多,流程步骤多会产生大量状态文件
适用
中型 / 大型游戏流程:热更新、登录流程、战斗 AI、剧情流程
三、有限状态机 + 事件驱动(事件 FSM,你整套架构核心组合)
在标准面向对象 FSM 基础上,用事件总线驱动状态切换,而非硬编码调用 ChangeState
分层
FSM 只负责执行流程逻辑,不接收玩家输入
外部输入(UI 点击、网络回调)通过事件发送
管理器(PatchManager)监听事件,收到后调用
ChangeState对应你的代码
UI 弹窗点重试 → 发送
UserTryXXXEvent
PatchManager全局监听事件,统一切换 FSM 状态优点
状态机与输入源完全解耦(UI、网络、定时器随便发事件)
一套状态机可以被多种输入驱动
便于埋点、日志拦截所有状态跳转
适用
需要 UI 交互、可重试、多触发源的流程(热更新、登录、商城)
四、分层嵌套 FSM(Super FSM / 复合状态机)
设计
大状态内部嵌套子状态机,解决多级流程 例: 总流程:热更新 FSM
子状态 1:资源初始化子 FSM(初始化→失败重试)
子状态 2:下载子 FSM(开始下载→暂停→失败重试)
优点
复杂流程拆分层级,避免顶层状态爆炸
缺点
调试链路变长,层级深容易混乱
适用
复杂 AI、大型新手引导、多阶段活动流程;你的热更目前单层级足够,不需要
五、状态表驱动 FSM(数据驱动 FSM,策划可配置)
设计
不用硬编码写死跳转,用配置表(ScriptableObject/Json)定义:
当前状态 + 触发事件 = 目标状态例:
当前状态 触发事件 跳转状态 CreateDownloader 用户确认下载 DownloadFiles DownloadFiles 下载失败 CreateDownloader 优点
跳转规则配置化,策划可调整流程,不用改代码
缺点
简单流程过度设计,复杂逻辑(协程、异步回调)不好塞进表格
适用
新手引导、剧情分支、活动流程;资源热更一般不用
六、栈式 FSM(状态栈,支持弹出回退)
设计
状态机内部用栈存储状态,不只是单一当前状态
PushState():压入新状态,保留旧状态
PopState():退出当前,回到上一个典型场景
弹窗系统、多层 UI、战斗暂停切设置界面
不适合
线性热更流程(你的热更是单向流水线,不需要回退上一步)
七、行为树混合 FSM(AI 专用)
把 FSM 作为行为树的节点,多用于角色 AI、怪物逻辑;资源热更完全不适用
八、协程 FSM(轻量流式 FSM,大量 YooAsset Demo 在用)
设计
不用拆分状态类,直接用多个
yield return分段写流程
csIEnumerator PatchFlow() { yield return InitPackage(); yield return RequestVersion(); yield return DownloadManifest(); }优点
代码集中,文件少,小型 Demo 快速实现
缺点
流程耦合严重,重试、分支逻辑会写大量嵌套
无统一生命周期,退出清理、委托解绑极难处理
无法拆分复用,不能多包并行热更
和你现有架构对比
你放弃了这种写法,选用类对象 FSM,就是为了工程可维护性。
精简总结
枚举 Switch 简陋 FSM:小型 Demo,耦合严重,不适合工程;
标准面向对象状态模式(IStateNode):项目主流,单状态单一职责,带完整生命周期(当前热更框架使用);
事件驱动 FSM:在标准 FSM 基础上用事件总线解耦输入与流程,适合带 UI 交互的热更、登录;
嵌套分层 FSM:多级复杂流程拆分;
数据表驱动 FSM:配置化流程,策划可控;
栈式 FSM:多层 UI、弹窗回退场景;
协程流式 FSM:简易 Demo 专用,不适合大型项目维护。

















