实战-热更新流程实现篇章10-实战篇-从零搭建状态已完成阅读时间约 25 分钟一、引言1.1 本章在系列中的定位前面的章节系统讲解了 YooAsset 的原理、构建、加载。本章将这些知识串联起来给出端到端的 YooAsset 热更新流程实现。我们会实现一个完整的HotUpdateManager覆盖所有关键节点处理异常与边界场景提供可直接运行的代码1.2 本章要解决的核心问题一条完整的热更新流程包含哪些步骤每个步骤如何实现异常如何处理怎样与游戏主流程衔接1.3 阅读前置要求完成 10-01 到 10-06了解 C# async/await 编程了解 Unity 协程二、热更新流程设计2.1 整体流程[游戏启动] │ ▼ [初始化 YooAsset] │ ▼ [检查强制更新] (来自运营后台) │ ├── 需强制更新 → 弹出更新对话框 → 退出 │ ▼ [检查资源版本] │ ├── 已是最新 → 直接进入游戏 │ ▼ [下载差异资源] │ ├── 下载成功 → 进入游戏 └── 下载失败 → 重试 / 降级2.2 状态机设计public enum EHotUpdateState { Idle, Initializing, CheckingForcedUpdate, InitializingYooAsset, CheckingVersion, DownloadingManifest, ConfirmingUpdate, DownloadingBundles, Completed, Failed } public class HotUpdateStateMachine { private EHotUpdateState _state EHotUpdateState.Idle; public event ActionEHotUpdateState StateChanged; public void ChangeState(EHotUpdateState newState) { if (_state newState) return; _state newState; StateChanged?.Invoke(_state); } }三、完整的 HotUpdateManager 实现3.1 核心类using System; using System.Threading.Tasks; using UnityEngine; using YooAsset; public class HotUpdateManager : MonoBehaviour { [Header(UI References)] [SerializeField] private GameObject updatePanel; [SerializeField] private UpdateProgressUI progressUI; [SerializeField] private UpdateConfirmDialog confirmDialog; [SerializeField] private ForceUpdateDialog forceUpdateDialog; [Header(Config)] [SerializeField] private float confirmThresholdMB 50; // 超过 50MB 询问 [SerializeField] private int maxRetryCount 3; [SerializeField] private int downloadTimeout 60; [SerializeField] private int maxConcurrent 10; private HotUpdateStateMachine _stateMachine new(); public async Taskbool StartHotUpdate() { try { _stateMachine.ChangeState(EHotUpdateState.CheckingForcedUpdate); if (!await CheckForcedUpdate()) return false; _stateMachine.ChangeState(EHotUpdateState.InitializingYooAsset); if (!await InitializeYooAsset()) return false; _stateMachine.ChangeState(EHotUpdateState.CheckingVersion); var versionInfo await CheckVersion(); if (versionInfo null) return false; if (!versionInfo.NeedUpdate) { _stateMachine.ChangeState(EHotUpdateState.Completed); return true; } _stateMachine.ChangeState(EHotUpdateState.ConfirmingUpdate); if (!await ConfirmUpdate(versionInfo)) return false; _stateMachine.ChangeState(EHotUpdateState.DownloadingBundles); if (!await DownloadUpdates()) return false; _stateMachine.ChangeState(EHotUpdateState.Completed); return true; } catch (Exception ex) { Debug.LogError($[HotUpdate] 异常: {ex.Message}); _stateMachine.ChangeState(EHotUpdateState.Failed); return false; } } }3.2 阶段 1检查强制更新public partial class HotUpdateManager { async Taskbool CheckForcedUpdate() { try { var config await GameApi.GetForceUpdateConfig(); if (config null) { Debug.LogWarning([HotUpdate] 获取强制更新配置失败跳过); return true; } if (config.NeedForceUpdate) { forceUpdateDialog.Show(config.UpdateUrl, () Application.Quit()); return false; } return true; } catch (Exception ex) { Debug.LogError($[HotUpdate] 检查强制更新异常: {ex.Message}); return true; // 出错时不阻止玩家 } } }3.3 阶段 2初始化 YooAssetpublic partial class HotUpdateManager { async Taskbool InitializeYooAsset() { ShowUpdateUI(正在初始化...); var initOp YooAssets.InitializeAsync(); await initOp.Task; if (initOp.Status ! EOperationStatus.Succeed) { ShowError($初始化失败: {initOp.Error}); return false; } Debug.Log([HotUpdate] YooAsset 初始化成功); return true; } }3.4 阶段 3检查版本public partial class HotUpdateManager { class VersionInfo { public bool NeedUpdate; public long DownloadSize; public string LatestVersion; } async TaskVersionInfo CheckVersion() { ShowUpdateUI(正在检查版本...); var checkOp YooAssets.RequestCheckVersionAsync(downloadTimeout); await checkOp.Task; if (checkOp.Status ! EOperationStatus.Succeed) { ShowError($检查版本失败: {checkOp.Error}); return null; } var info new VersionInfo { NeedUpdate checkOp.Result.IsNewVersion, DownloadSize checkOp.Result.DownloadSize, LatestVersion checkOp.Result.LatestVersion }; Debug.Log($[HotUpdate] 版本检查: NeedUpdate{info.NeedUpdate}, Size{info.DownloadSize}); return info; } }3.5 阶段 4用户确认public partial class HotUpdateManager { async Taskbool ConfirmUpdate(VersionInfo versionInfo) { // 小更新直接下载 if (versionInfo.DownloadSize confirmThresholdMB * 1024 * 1024) return true; // 大更新询问 bool accepted await confirmDialog.ShowAndWait( title: 发现新版本, message: $需要下载 {versionInfo.DownloadSize / 1024 / 1024} MB 资源, updateButton: 立即更新, cancelButton: 稍后 ); if (!accepted) { Debug.Log([HotUpdate] 用户取消更新); return false; } return true; } }3.6 阶段 5下载资源public partial class HotUpdateManager { async Taskbool DownloadUpdates() { ShowUpdateUI(正在下载资源...); // 1. 获取资源清单 ShowUpdateUI(获取资源清单...); var manifestOp YooAssets.RequestPackageManifestAsync(); await manifestOp.Task; if (manifestOp.Status ! EOperationStatus.Succeed) { ShowError($获取清单失败: {manifestOp.Error}); return false; } // 2. 下载所有 Bundle var downloadOp YooAssets.DownloadUpdateAsync( downloadingMaxNum: maxConcurrent, failedTryAgain: maxRetryCount, timeout: downloadTimeout ); // 绑定进度 BindProgress(downloadOp); // 等待完成 await downloadOp.Task; if (downloadOp.Status ! EOperationStatus.Succeed) { ShowError($下载失败: {downloadOp.Error}); return false; } Debug.Log([HotUpdate] 下载完成); return true; } void BindProgress(DownloadUpdateOperation op) { progressUI.gameObject.SetActive(true); // 进度更新协程 StartCoroutine(UpdateProgressCoroutine(op)); } System.Collections.IEnumerator UpdateProgressCoroutine(DownloadUpdateOperation op) { while (!op.IsDone) { progressUI.UpdateProgress(op.Progress, op.CurrentSpeed, op.CurrentDownloadBytes, op.TotalDownloadBytes); yield return null; } progressUI.gameObject.SetActive(false); } }四、UI 组件4.1 进度 UIusing UnityEngine; using UnityEngine.UI; using TMPro; public class UpdateProgressUI : MonoBehaviour { public Slider progressBar; public TMP_Text progressText; public TMP_Text speedText; public TMP_Text sizeText; public TMP_Text fileText; public TMP_Text statusText; public void UpdateProgress(float progress, long speed, long currentBytes, long totalBytes) { progressBar.value progress; progressText.text ${progress * 100:F1}%; speedText.text ${speed / 1024:F1} KB/s; sizeText.text ${currentBytes / 1024 / 1024}MB / {totalBytes / 1024 / 1024}MB; } public void SetStatus(string status) { statusText.text status; } public void SetCurrentFile(string fileName) { fileText.text fileName; } }4.2 确认对话框public class UpdateConfirmDialog : MonoBehaviour { public TMP_Text titleText; public TMP_Text messageText; public Button updateButton; public Button cancelButton; private TaskCompletionSourcebool _tcs; public Taskbool ShowAndWait(string title, string message, string updateBtn, string cancelBtn) { _tcs new TaskCompletionSourcebool(); gameObject.SetActive(true); titleText.text title; messageText.text message; var updateText updateButton.GetComponentInChildrenTMP_Text(); var cancelText cancelButton.GetComponentInChildrenTMP_Text(); updateText.text updateBtn; cancelText.text cancelBtn; updateButton.onClick.AddListener(() OnClick(true)); cancelButton.onClick.AddListener(() OnClick(false)); return _tcs.Task; } void OnClick(bool accepted) { gameObject.SetActive(false); updateButton.onClick.RemoveAllListeners(); cancelButton.onClick.RemoveAllListeners(); _tcs?.SetResult(accepted); } }4.3 强制更新对话框public class ForceUpdateDialog : MonoBehaviour { public Button downloadButton; public TMP_Text messageText; private Action _onQuit; public void Show(string downloadUrl, Action onQuit) { _onQuit onQuit; gameObject.SetActive(true); messageText.text 当前版本过低请更新到最新版本; downloadButton.onClick.AddListener(() { Application.OpenURL(downloadUrl); Application.Quit(); }); } }五、错误处理5.1 错误分类错误类型表现处理网络错误下载失败重试 / 等待清单错误Manifest 下载失败切换 CDN资源错误校验失败清除缓存重试服务器错误5xx 响应重试客户端错误4xx 响应提示用户超时错误长时间无响应切换网络5.2 错误恢复public class HotUpdateErrorHandler { public async Taskbool HandleError(Exception ex, EHotUpdateState state) { switch (state) { case EHotUpdateState.InitializingYooAsset: return await RetryInitialize(); case EHotUpdateState.CheckingVersion: return await RetryCheckVersion(); case EHotUpdateState.DownloadingBundles: return await RetryDownload(); default: return false; } } async Taskbool RetryInitialize() { Debug.Log([HotUpdate] 重试初始化 YooAsset); await Task.Delay(2000); // 等待 2 秒 return true; } async Taskbool RetryCheckVersion() { Debug.Log([HotUpdate] 重试检查版本); await Task.Delay(2000); return true; } async Taskbool RetryDownload() { Debug.Log([HotUpdate] 清除损坏文件后重试下载); YooAssets.ClearCache(); return true; } }5.3 CDN 切换public class CDNFailover { private readonly Liststring _cdnUrls new() { https://cdn1.example.com, https://cdn2.example.com, https://cdn3.example.com }; public async Taskbool DownloadWithFailover(string path, string savePath) { foreach (var cdn in _cdnUrls) { try { var url ${cdn}/{path}; // 尝试下载 return await TryDownload(url, savePath); } catch { Debug.LogWarning($CDN {cdn} 失败尝试下一个); } } return false; } }六、测试与调试6.1 单元测试[Test] public async Task TestHotUpdate_AlreadyLatest() { // Mock 场景玩家已是最新版本 MockServer.SetLatestVersion(PlayerSettings.bundleVersion); var result await hotUpdateManager.StartHotUpdate(); Assert.IsTrue(result); } [Test] public async Task TestHotUpdate_RequiresUpdate() { // Mock 场景需要更新 MockServer.SetLatestVersion(2.0.0); MockServer.SetBundleSize(100 * 1024 * 1024); var result await hotUpdateManager.StartHotUpdate(); Assert.IsTrue(result); } [Test] public async Task TestHotUpdate_NetworkError() { // Mock 场景网络错误 MockServer.SimulateNetworkError(true); var result await hotUpdateManager.StartHotUpdate(); Assert.IsFalse(result); }6.2 集成测试public class HotUpdateIntegrationTest { [UnityTest] public IEnumerator TestFullFlow() { // 1. 启动热更新 var task hotUpdateManager.StartHotUpdate(); // 2. 等待完成 while (!task.IsCompleted) yield return null; // 3. 验证结果 Assert.IsTrue(task.Result); // 4. 验证后续加载 var loadTask YooAssets.LoadAssetAsyncSprite(test_icon); yield return new WaitUntil(() loadTask.IsDone); Assert.IsTrue(loadTask.Status EOperationStatus.Succeed); } }6.3 灰度测试public class GrayScaleTest { /// summary /// 灰度发布前的小流量验证 /// /summary public async Taskbool TestOnGrayUsers() { // 1. 仅 5% 用户 var grayUsers GetGrayUsers(percent: 5); // 2. 监控关键指标 var monitor new HotUpdateMonitor(); // 3. 观察 24 小时 await Task.Delay(TimeSpan.FromHours(24)); // 4. 检查通过率 var passRate monitor.GetPassRate(); return passRate 0.95; // 95% 通过 } }七、性能监控7.1 上报指标public class HotUpdateMonitor { public void ReportStart() { Analytics.Track(hot_update_start); } public void ReportEnd(TimeSpan duration, bool success, long downloadSize) { Analytics.Track(hot_update_end, new { duration duration.TotalSeconds, success, download_size downloadSize, network_type Application.internetReachability }); } public void ReportError(string stage, string error) { Analytics.Track(hot_update_error, new { stage, error }); } }7.2 关键指标指标计算方式告警阈值启动到完成总时长end - start 60s启动成功率成功 / 总尝试 95%失败阶段分布各阶段失败率任意 5%平均下载速度大小 / 耗时 100KB/s资源重试率重试 / 总下载 10%八、完整的 GameEntrypublic class GameEntry : MonoBehaviour { [SerializeField] private HotUpdateManager hotUpdateManager; async void Start() { // 1. 启动热更新 bool success await hotUpdateManager.StartHotUpdate(); if (success) { // 2. 进入游戏 EnterGame(); } else { // 3. 启动失败 ShowFatalError(); } } void EnterGame() { // 加载主场景 var op YooAssets.LoadSceneAsync(MainCity); } void ShowFatalError() { // 显示错误对话框 } }九、常见问题9.1 下载卡 99%原因某个大文件下载失败Hash 校验不通过。解决// 增加超时和重试 var downloadOp YooAssets.DownloadUpdateAsync( timeout: 30, // 单文件 30 秒超时 failedTryAgain: 5 // 重试 5 次 );9.2 启动黑屏原因启动 UI 资源不在首包。解决// 确保启动资源在首包 [StartupPack] Filter: Assets/Startup/**/*9.3 切场景后资源丢失原因引用计数管理不当。解决// 切场景时不要强制释放 // 让 YooAsset 自动管理 public async Task LoadNewScene(string sceneName) { var op YooAssets.LoadSceneAsync(sceneName); await op.Task; // 旧场景的资源会在引用计数为 0 时自动释放 }十、总结10.1 本章要点回顾流程设计状态机 分阶段处理HotUpdateManager核心管理器串联所有步骤UI 组件进度、确认、强制更新对话框错误处理分类、重试、降级、CDN 切换测试与监控单元测试、集成测试、灰度测试10.2 与前后章节的关联前章10-06 介绍了资源加载与使用本章聚焦热更新流程的完整实现后章10-08 将介绍调试与性能分析10.3 实践建议状态机管理用状态机清晰划分流程UI 解耦所有 UI 通过事件回调不硬耦合错误必处理每个阶段都要有错误处理监控先行没有监控的热更新是裸奔灰度发布代码热更必须灰度上一篇资源加载与使用下一篇调试与性能分析