08-05-热更新-完整热更新流程实战

热更新-完整热更新流程实战

篇章 :08-核心技术-热更新系统 状态 :已完成 阅读时间:约 30 分钟


一、引言

1.1 本章在系列中的定位

本章是 08 章的实战收官。我们已经从原理上理解了 YooAsset 的版本管理、差量更新、边玩边下载、首包资源策略,但理解原理不等于能落地。本章将所有知识点串联起来,给出一条端到端的"构建→上传→下载→安装→切换"完整流程,并提供可直接落地的代码。

这是本系列文章中最贴近实战的一章,建议配合第十、十一章的实战项目一起阅读。

1.2 本章要解决的核心问题

  • 一条完整的热更新流水线包含哪些步骤?
  • 每个步骤的关键配置和最佳实践是什么?
  • 如何串联这些步骤形成可用的代码框架?
  • 实战中常见的坑和应对方案是什么?

1.3 阅读前置要求

  • 了解前四章的所有内容(08-01 到 08-04)
  • 了解 YooAsset 的 Editor 构建脚本
  • 了解 CDN / OSS / FTP 等服务器部署方式

二、构建(Build)

2.1 构建流程总览

css 复制代码
[配置 Collector 规则]
    │
    ▼
[配置 Build Pipeline]
    │
    ▼
[调用 IBuildPipeline.Build]
    │
    ├── [收集资源]  AssetBundleCollector
    │       └── 输出:BuildMap
    │
    ├── [分析依赖]  BuildMap
    │       └── 输出:包含所有 Bundle 信息
    │
    ├── [构建 Bundle]  BuildTaskBuilder
    │       └── 输出:bundle 文件
    │
    ├── [生成 Manifest]  BuildTaskManifest
    │       └── 输出:PackageManifest_X.Y.Z.bytes
    │
    └── [生成报告]  BuildReport
            └── 输出:BuildReport.html

2.2 构建脚本示例

YooAsset 的构建脚本通常放在 Assets/Editor/Build/BuildPipeline.cs:

csharp 复制代码
using UnityEditor;
using YooAsset.Editor;

public static class YooAssetBuilder
{
    public static void BuildAndroid()
    {
        var buildParams = new BuildParameters
        {
            // 基础配置
            BuildOutputRoot = "D:/Builds/MyGame",
            BuildVersion = "1.0.5",
            BuildPipeline = EBuildPipeline.ScriptableBuildPipeline,
            
            // 压缩配置
            CompressOption = ECompressOption.LZ4,
            
            // 加密配置(可选)
            EncryptionOption = EEncryptionOption.AES,
            EncryptionSecret = "MyGame@2024",
            
            // 平台
            BuildTarget = BuildTarget.Android,
            
            // 输出格式
            OutputNameStyle = EOutputNameStyle.HashName,
            
            // 共享资源
            EnableSharedPackRule = true,
            
            // 拷贝到 StreamingAssets
            CopyToStreamingAssets = true
        };
        
        var pipeline = new ScriptableBuildPipeline();
        var manifest = pipeline.Build(buildParams);
        
        if (manifest.Success)
        {
            Debug.Log($"构建成功: {manifest.OutputRoot}");
        }
        else
        {
            Debug.LogError($"构建失败: {manifest.ErrorInfo}");
        }
    }
}

2.3 构建配置详解

2.3.1 压缩配置
算法 压缩率 速度 选择建议
Uncompressed 1.0x 最快 不推荐(除非有特殊需求)
LZ4 ~1.5x 极快 推荐:商业项目首选
LZMA ~2.5x 慢 不推荐(构建慢、解压慢)
2.3.2 加密配置
csharp 复制代码
public enum EEncryptionOption
{
    None,        // 不加密
    AES,         // AES 加密(推荐)
    ChaCha20,    // ChaCha20 加密
    Offset,      // 字节偏移加密(不安全)
    Xor,         // 异或加密(不安全)
}

加密的最佳实践:

  • 加密粒度:建议按 Bundle 加密,而不是全包加密
  • 密钥管理:密钥从远程获取,本地不留明文
  • 解密性能:移动端避免 AES 全文件解密,影响加载速度
2.3.3 平台配置
csharp 复制代码
public static void BuildForAllPlatforms()
{
    var platforms = new[]
    {
        (BuildTarget.Android, "android"),
        (BuildTarget.iOS, "ios"),
        (BuildTarget.StandaloneWindows64, "windows"),
        (BuildTarget.WebGL, "webgl"),
    };
    
    foreach (var (target, name) in platforms)
    {
        Debug.Log($"开始构建 {name}");
        var buildParams = GetBuildParams(target);
        var pipeline = new ScriptableBuildPipeline();
        pipeline.Build(buildParams);
    }
}

2.4 构建验证

2.4.1 构建报告

YooAsset 会在构建目录生成 BuildReport.html:

javascript 复制代码
D:/Builds/MyGame/
├── BuildReport.html      ← 构建报告
├── BuildLog.txt          ← 构建日志
├── Android/
│   ├── PackageManifest_1.0.5.bytes
│   ├── PackageManifest_1.0.5.hash
│   └── bundles/
└── iOS/
    └── ...

报告内容:

  • 收集的资源数量
  • 构建的 Bundle 数量
  • 总文件大小(含压缩前后)
  • 警告和错误信息
  • 资源依赖图
2.4.2 冗余检测
csharp 复制代码
public class RedundancyDetector
{
    public static void CheckRedundancy()
    {
        var buildMap = AssetBundleCollector.GetBuildMap();
        var allAssets = buildMap.GetAllAssets();
        
        // 查找同名资源
        var duplicates = allAssets
            .GroupBy(a => a.AssetPath)
            .Where(g => g.Count() > 1);
        
        foreach (var dup in duplicates)
        {
            Debug.LogWarning($"冗余资源: {dup.Key}");
        }
    }
}
2.4.3 依赖验证
csharp 复制代码
public class DependencyValidator
{
    public static void ValidateDependencies()
    {
        var buildMap = AssetBundleCollector.GetBuildMap();
        var allBundles = buildMap.GetAllBundles();
        
        foreach (var bundle in allBundles)
        {
            // 检查循环依赖
            if (HasCyclicDependency(bundle, new HashSet<string>()))
            {
                Debug.LogError($"循环依赖: {bundle.BundleName}");
            }
            
            // 检查缺失依赖
            foreach (var dep in bundle.Dependencies)
            {
                if (!allBundles.Any(b => b.BundleName == dep))
                {
                    Debug.LogError($"缺失依赖: {dep}(被 {bundle.BundleName} 引用)");
                }
            }
        }
    }
}

三、上传(Upload)

3.1 上传流程

css 复制代码
[构建完成]
    │
    ▼
[遍历构建输出目录]
    │
    ▼
[上传到 CDN/OSS]
    │   ├── 全量文件(所有 Bundle)
    │   ├── Manifest 文件(PackageManifest_X.Y.Z.bytes/.hash)
    │   └── 报告文件
    │
    ▼
[更新版本索引]
    │   └── /cdn/version_index.json
    │       { "latest": "1.0.5", "stable": "1.0.4" }
    │
    ▼
[通知业务服务器]
        └── HTTP POST /api/release
            { "version": "1.0.5", "cdn_url": "..." }

3.2 上传脚本示例

csharp 复制代码
public class YooAssetUploader
{
    public static async Task UploadToCDN(string buildOutputDir, string cdnBaseUrl)
    {
        // 1. 读取版本号
        var versionFile = Path.Combine(buildOutputDir, "version.txt");
        string version = File.ReadAllText(versionFile).Trim();
        
        // 2. 遍历所有平台目录
        var platformDirs = Directory.GetDirectories(buildOutputDir);
        foreach (var platformDir in platformDirs)
        {
            string platform = Path.GetFileName(platformDir);
            string targetUrl = $"{cdnBaseUrl}/{platform}/{version}/";
            
            // 3. 上传所有文件
            await UploadDirectory(platformDir, targetUrl);
        }
        
        // 4. 更新版本索引
        await UpdateVersionIndex(cdnBaseUrl, version);
        
        // 5. 通知业务服务器
        await NotifyBusinessServer(version, cdnBaseUrl);
    }
    
    private static async Task UploadDirectory(string localDir, string remoteUrl)
    {
        var files = Directory.GetFiles(localDir, "*", SearchOption.AllDirectories);
        var semaphore = new SemaphoreSlim(10);  // 10 个并发上传
        
        var tasks = files.Select(async file =>
        {
            await semaphore.WaitAsync();
            try
            {
                string relativePath = Path.GetRelativePath(localDir, file).Replace('\\', '/');
                string uploadUrl = $"{remoteUrl}/{relativePath}";
                await UploadFile(file, uploadUrl);
            }
            finally
            {
                semaphore.Release();
            }
        });
        
        await Task.WhenAll(tasks);
    }
}

3.3 上传策略

3.3.1 全量上传 vs 增量上传
csharp 复制代码
public class IncrementalUploader
{
    public async Task<bool> ShouldUpload(string filePath, string remoteUrl)
    {
        // 1. 查询远端文件 ETag
        string remoteEtag = await GetRemoteETag(remoteUrl);
        
        // 2. 计算本地文件 MD5
        string localMd5 = ComputeFileMD5(filePath);
        
        // 3. 对比决定是否需要上传
        return remoteEtag != localMd5;
    }
}
3.3.2 多 CDN 备份
csharp 复制代码
public class MultiCDNUploader
{
    private readonly List<string> _cdnUrls = new()
    {
        "https://cdn1.example.com",
        "https://cdn2.example.com",
        "https://cdn3.example.com"
    };
    
    public async Task UploadToAllCDNs(string filePath, string relativePath)
    {
        var tasks = _cdnUrls.Select(cdn =>
            UploadFile(filePath, $"{cdn}/{relativePath}")
        );
        await Task.WhenAll(tasks);
    }
}

3.4 上传验证

csharp 复制代码
public class UploadVerifier
{
    public async Task<bool> VerifyUpload(string url, string localPath)
    {
        // 1. 下载远端文件
        string tempPath = Path.GetTempFileName();
        await DownloadFile(url, tempPath);
        
        // 2. 对比 Hash
        string localHash = ComputeFileMD5(localPath);
        string remoteHash = ComputeFileMD5(tempPath);
        
        File.Delete(tempPath);
        return localHash == remoteHash;
    }
}

四、下载(Download)

4.1 下载流程

css 复制代码
[游戏启动]
    │
    ▼
[读取 YooAssetSetting]
    │
    ▼
[初始化 YooAsset]
    │   ├── 创建文件系统
    │   ├── 加载本地缓存清单
    │   └── 设置下载管理器
    │
    ▼
[检查版本]
    │   ├── 下载 .hash 文件
    │   ├── 与本地对比
    │   └── 是否需要更新?
    │
    ├── 是 ─→ [下载更新]
    │           ├── 下载完整 Manifest
    │           ├── 计算差异
    │           ├── 加入下载队列
    │           └── 等待完成
    │
    └── 否 ─→ [直接进入游戏]

4.2 下载实现

csharp 复制代码
public class HotUpdateManager : MonoBehaviour
{
    public async Task<bool> StartHotUpdate()
    {
        try
        {
            // 1. 初始化 YooAsset
            var initOp = YooAssets.InitializeAsync();
            await initOp.Task;
            
            if (initOp.Status != EOperationStatus.Succeed)
            {
                Debug.LogError($"YooAsset 初始化失败: {initOp.Error}");
                return false;
            }
            
            // 2. 获取版本信息
            var versionOp = YooAssets.RequestVersionAsync();
            await versionOp.Task;
            
            if (versionOp.Status != EOperationStatus.Succeed)
            {
                Debug.LogError($"获取版本失败: {versionOp.Error}");
                return false;
            }
            
            // 3. 检查更新
            var checkOp = YooAssets.RequestCheckVersionAsync();
            await checkOp.Task;
            
            if (checkOp.Status != EOperationStatus.Succeed)
            {
                Debug.LogError($"检查更新失败: {checkOp.Error}");
                return false;
            }
            
            // 4. 是否有更新
            if (checkOp.Result.IsNewVersion)
            {
                Debug.Log($"发现新版本,需要下载 {checkOp.Result.DownloadSize} 字节");
                
                // 5. 获取资源清单
                var manifestOp = YooAssets.RequestPackageManifestAsync();
                await manifestOp.Task;
                
                if (manifestOp.Status != EOperationStatus.Succeed)
                {
                    Debug.LogError($"获取清单失败: {manifestOp.Error}");
                    return false;
                }
                
                // 6. 下载差异
                var downloadOp = YooAssets.DownloadUpdateAsync(
                    downloadingMaxNum: 10,
                    failedTryAgain: 3,
                    timeout: 60
                );
                
                // 显示进度
                ShowDownloadProgress(downloadOp);
                await downloadOp.Task;
                
                if (downloadOp.Status != EOperationStatus.Succeed)
                {
                    Debug.LogError($"下载失败: {downloadOp.Error}");
                    return false;
                }
            }
            
            return true;
        }
        catch (Exception ex)
        {
            Debug.LogError($"热更新异常: {ex.Message}");
            return false;
        }
    }
}

4.3 进度显示

csharp 复制代码
public class DownloadProgressUI : MonoBehaviour
{
    public Slider progressBar;
    public TMP_Text progressText;
    public TMP_Text speedText;
    public TMP_Text fileText;
    public TMP_Text sizeText;
    
    public void ShowDownloadProgress(DownloadUpdateOperation op)
    {
        gameObject.SetActive(true);
        StartCoroutine(ProgressCoroutine(op));
    }
    
    private IEnumerator ProgressCoroutine(DownloadUpdateOperation op)
    {
        var wait = new WaitForSeconds(0.1f);
        while (!op.IsDone)
        {
            progressBar.value = op.Progress;
            progressText.text = $"{op.Progress * 100:F1}%";
            speedText.text = $"{op.CurrentSpeed / 1024:F1} KB/s";
            fileText.text = op.CurrentDownloadFileName;
            sizeText.text = $"{op.CurrentDownloadBytes / 1024 / 1024}MB / {op.TotalDownloadBytes / 1024 / 1024}MB";
            yield return wait;
        }
        gameObject.SetActive(false);
    }
}

4.4 下载错误处理

csharp 复制代码
public class DownloadErrorHandler
{
    public async Task<bool> HandleDownloadError(DownloadUpdateOperation op)
    {
        switch (op.ErrorCode)
        {
            case EDownloadErrorCode.NetworkError:
                // 等待网络恢复
                await WaitForNetwork();
                return true;  // 重试
                
            case EDownloadErrorCode.DiskFull:
                // 提示用户清理空间
                UIManager.Show<DiskFullDialog>();
                return false;  // 不重试
                
            case EDownloadErrorCode.HashMismatch:
                // 删除损坏文件
                File.Delete(op.CurrentDownloadFileName);
                return true;  // 重试
                
            case EDownloadErrorCode.VersionMismatch:
                // 版本不一致,强制重启
                YooAssets.ForceChangeVersion();
                return false;
                
            default:
                return false;
        }
    }
    
    private async Task WaitForNetwork()
    {
        while (!NetworkManager.IsAvailable())
        {
            await Task.Delay(1000);
        }
    }
}

五、安装(Install)

5.1 安装流程

YooAsset 中的"安装"并不是传统意义上的安装(拷贝文件到系统目录),而是指资源下载完成后写入本地缓存。

css 复制代码
[下载完成]
    │
    ▼
[校验所有文件 Hash]
    │
    ▼
[写入本地缓存]
    │   ├── Bundle 文件 → yooasset/bundles/
    │   ├── Manifest 文件 → yooasset/manifest/
    │   └── 断点信息文件清理
    │
    ▼
[更新本地版本索引]
    │
    ▼
[完成安装]

5.2 安装实现

YooAsset 的安装是自动的,开发者无需手动调用。内部流程:

csharp 复制代码
public class InstallOperation
{
    public async Task InstallAsync(List<BundleInfo> downloadedBundles)
    {
        // 1. 验证所有 Bundle
        var invalidBundles = new List<BundleInfo>();
        foreach (var bundle in downloadedBundles)
        {
            if (!VerifyBundle(bundle))
            {
                invalidBundles.Add(bundle);
            }
        }
        
        // 2. 删除无效文件
        foreach (var bundle in invalidBundles)
        {
            File.Delete(GetBundlePath(bundle));
        }
        
        // 3. 更新清单索引
        var manifest = YooAssets.GetPackageManifest();
        manifest.UpdateBundleList(downloadedBundles);
        manifest.SaveToCache();
        
        // 4. 清理下载断点
        ClearDownloadResume();
        
        // 5. 触发回调
        OnInstallCompleted?.Invoke();
    }
}

六、切换(Switch)

6.1 切换流程

scss 复制代码
[下载完成]
    │
    ▼
[选择切换时机]
    │
    ├── 启动时切换(玩家首次进入游戏时)
    │
    └── 切场景时切换(业务层主动触发)
    │
    ▼
[强制切换版本]
    │   YooAssets.ForceChangeVersion(newVersion)
    │
    ▼
[重新初始化资源系统]
    │
    ▼
[验证切换成功]
    │   ├── 加载核心资源测试
    │   └── 错误则回滚
    │
    ▼
[继续游戏]

6.2 切换实现

csharp 复制代码
public class VersionSwitcher
{
    public async Task<bool> SwitchToVersion(string targetVersion)
    {
        // 1. 校验目标版本
        var manifest = LoadManifestFromCache(targetVersion);
        if (manifest == null)
        {
            Debug.LogError($"版本 {targetVersion} 不存在");
            return false;
        }
        
        // 2. 强制切换
        YooAssets.ForceChangeVersion(targetVersion);
        
        // 3. 重新初始化
        var reinitOp = YooAssets.InitializeAsync();
        await reinitOp.Task;
        
        if (reinitOp.Status != EOperationStatus.Succeed)
        {
            Debug.LogError("切换后初始化失败");
            return false;
        }
        
        // 4. 验证:加载一个关键资源
        var testOp = YooAssets.LoadAssetAsync<Sprite>("Assets/Startup/Logo.png");
        await testOp.Task;
        
        if (testOp.Status != EOperationStatus.Succeed)
        {
            Debug.LogError("切换后资源加载失败");
            return false;
        }
        
        // 5. 清理旧版本资源(可选)
        // await CleanupOldVersions();
        
        return true;
    }
}

6.3 切换失败回滚

csharp 复制代码
public class SafeVersionSwitch
{
    private string _lastGoodVersion;
    
    public async Task<bool> SafeSwitchTo(string targetVersion)
    {
        _lastGoodVersion = YooAssets.GetCurrentVersion();
        
        try
        {
            bool success = await SwitchToVersion(targetVersion);
            if (!success)
            {
                await RollbackToLastGood();
                return false;
            }
            return true;
        }
        catch (Exception ex)
        {
            Debug.LogError($"切换异常: {ex.Message}");
            await RollbackToLastGood();
            return false;
        }
    }
    
    private async Task RollbackToLastGood()
    {
        Debug.LogWarning($"回滚到上一稳定版本: {_lastGoodVersion}");
        await SwitchToVersion(_lastGoodVersion);
    }
}

七、完整代码示例

7.1 完整的热更新管理器

csharp 复制代码
using System;
using System.Threading.Tasks;
using UnityEngine;
using YooAsset;

public class HotUpdateManager : MonoBehaviour
{
    [SerializeField] private GameObject updateUI;
    [SerializeField] private UpdateProgressUI progressUI;
    [SerializeField] private UpdateConfirmDialog confirmDialog;
    [SerializeField] private ForceUpdateDialog forceUpdateDialog;
    
    public async Task<bool> InitializeAndUpdate()
    {
        // ========== 阶段 1:检查强制更新 ==========
        var forceUpdate = await GameApi.GetForceUpdateConfig();
        if (forceUpdate.needForceUpdate)
        {
            forceUpdateDialog.Show(forceUpdate.updateUrl);
            return false;
        }
        
        // ========== 阶段 2:初始化 YooAsset ==========
        updateUI.SetActive(true);
        progressUI.SetStatus("正在初始化...");
        
        var initOp = YooAssets.InitializeAsync();
        await initOp.Task;
        if (initOp.Status != EOperationStatus.Succeed)
        {
            progressUI.ShowError($"初始化失败: {initOp.Error}");
            return false;
        }
        
        // ========== 阶段 3:检查版本 ==========
        progressUI.SetStatus("正在检查版本...");
        
        var checkOp = YooAssets.RequestCheckVersionAsync();
        await checkOp.Task;
        if (checkOp.Status != EOperationStatus.Succeed)
        {
            progressUI.ShowError($"检查版本失败: {checkOp.Error}");
            return false;
        }
        
        // ========== 阶段 4:判断是否需要更新 ==========
        if (!checkOp.Result.IsNewVersion)
        {
            // 已经是最新版本,直接进入游戏
            updateUI.SetActive(false);
            return true;
        }
        
        // ========== 阶段 5:询问用户 ==========
        bool userAccepted = false;
        if (checkOp.Result.DownloadSize > 50 * 1024 * 1024)  // 超过 50MB 询问
        {
            userAccepted = await confirmDialog.ShowAndWait(
                $"发现新版本,需要下载 {checkOp.Result.DownloadSize / 1024 / 1024} MB"
            );
            
            if (!userAccepted)
            {
                // 用户选择稍后更新
                return TryEnterGameWithOldVersion();
            }
        }
        else
        {
            userAccepted = true;  // 小更新直接下载
        }
        
        // ========== 阶段 6:获取资源清单 ==========
        progressUI.SetStatus("正在获取资源清单...");
        
        var manifestOp = YooAssets.RequestPackageManifestAsync();
        await manifestOp.Task;
        if (manifestOp.Status != EOperationStatus.Succeed)
        {
            progressUI.ShowError($"获取清单失败: {manifestOp.Error}");
            return false;
        }
        
        // ========== 阶段 7:下载资源 ==========
        var downloadOp = YooAssets.DownloadUpdateAsync(
            downloadingMaxNum: 10,
            failedTryAgain: 3,
            timeout: 60
        );
        
        // 显示进度
        progressUI.BindDownloadOperation(downloadOp);
        await downloadOp.Task;
        
        if (downloadOp.Status != EOperationStatus.Succeed)
        {
            progressUI.ShowError($"下载失败: {downloadOp.Error}");
            return await HandleDownloadFailure(downloadOp);
        }
        
        // ========== 阶段 8:完成 ==========
        progressUI.SetStatus("更新完成");
        await Task.Delay(500);  // 让玩家看到"完成"
        updateUI.SetActive(false);
        
        return true;
    }
    
    private bool TryEnterGameWithOldVersion()
    {
        // 尝试用旧版本进入游戏(功能可能不完整)
        return true;
    }
    
    private async Task<bool> HandleDownloadFailure(DownloadUpdateOperation op)
    {
        // 询问用户:重试 / 取消
        bool retry = await confirmDialog.ShowRetryOrCancel();
        if (retry)
        {
            // 删除损坏文件后重试
            YooAssets.ClearCache();
            return await InitializeAndUpdate();
        }
        return false;
    }
}

7.2 自动启动

csharp 复制代码
public class GameEntry : MonoBehaviour
{
    [SerializeField] private HotUpdateManager hotUpdateManager;
    [SerializeField] private GameObject loginPanel;
    
    async void Start()
    {
        // 1. 启动热更新
        bool success = await hotUpdateManager.InitializeAndUpdate();
        
        if (success)
        {
            // 2. 进入登录
            loginPanel.SetActive(true);
        }
        else
        {
            // 3. 启动失败,提示玩家
            ShowFatalErrorDialog();
        }
    }
}

八、常见问题与解决方案

8.1 启动黑屏

现象:游戏启动后一直黑屏,Loading 界面不出现。

原因:

  • 启动 UI 资源没放在首包中
  • 启动场景缺失关键资源
  • YooAsset 初始化失败

解决:

csharp 复制代码
// 1. 确保启动 UI 资源在首包
// 2. 加日志
Debug.Log("[Startup] YooAsset 初始化开始");
var initOp = YooAssets.InitializeAsync();
await initOp.Task;
Debug.Log($"[Startup] YooAsset 初始化结果: {initOp.Status}");

// 3. 异步启动分阶段日志
public async Task StartupWithLogs()
{
    Debug.Log("[1/5] 初始化 YooAsset");
    await Step1_InitYooAsset();
    
    Debug.Log("[2/5] 检查版本");
    await Step2_CheckVersion();
    
    Debug.Log("[3/5] 下载资源");
    await Step3_Download();
    
    Debug.Log("[4/5] 加载主场景");
    await Step4_LoadMainScene();
    
    Debug.Log("[5/5] 进入游戏");
    await Step5_EnterGame();
}

8.2 下载卡 99%

现象:下载进度到 99% 卡住不动。

原因:

  • 某个大文件下载失败
  • Hash 校验失败但未自动重试
  • 线程死锁

解决:

csharp 复制代码
// 1. 增加超时
var downloadOp = YooAssets.DownloadUpdateAsync(timeout: 60);

// 2. 进度心跳监控
if (DateTime.Now - _lastProgressUpdate > TimeSpan.FromSeconds(30))
{
    Debug.LogWarning("下载超过 30 秒无进度,重试");
    YooAssets.ClearCache();
    await InitializeAndUpdate();
}

8.3 切场景后资源丢失

现象:从一个场景切到另一个场景后,原场景的资源变成"加载失败"。

原因:

  • 引用计数管理不当
  • 切场景时强制清空了缓存

解决:

csharp 复制代码
// 切场景时正确管理引用
public async Task LoadNewScene(string sceneName)
{
    // 1. 异步加载新场景
    var sceneOp = YooAssets.LoadSceneAsync(sceneName, LoadSceneMode.Single);
    await sceneOp.Task;
    
    // 2. 新场景加载完成后,原场景资源自动释放
    // YooAsset 内部会处理引用计数
}

九、总结

6.1 本章要点回顾

  • 构建 :通过 IBuildPipeline 一键完成 Bundle 构建、Manifest 生成、报告输出
  • 上传:支持全量和增量上传,建议多 CDN 备份
  • 下载 :使用 RequestCheckVersionAsync + DownloadUpdateAsync 完成
  • 安装:YooAsset 自动处理,开发者无需关心
  • 切换 :使用 ForceChangeVersion 强制切换版本,支持回滚

6.2 与前后章节的关联

  • 前章:08-01 ~ 08-04 解决了热更新的各个细节
  • 本章:将这些细节串联成一条端到端流水线
  • 后章:09-01 将介绍 YooAsset + HybridCLR 的代码热更方案

6.3 实践建议

  1. 构建脚本化:把构建脚本写进 CI,避免人工操作
  2. 上传分平台:每个平台独立目录,避免混淆
  3. 下载可视化:进度条、速度、剩余时间一应俱全
  4. 切换要回滚:切版本失败时一定要回滚
  5. 监控全覆盖:下载成功率、平均时长、失败原因都要监控

下一篇 :YooAsset+HybridCLR 热更新方案

相关推荐
只睡四小时12 小时前
Three.js Sub-tick:漏判 60.7%→0
开发语言·javascript·性能优化·ecmascript·游戏开发·three.js·碰撞检测
SmalBox1 天前
08-04-热更新-首包资源策略
unity3d·游戏开发
SmalBox2 天前
08-03-热更新-边玩边下载
unity3d·游戏开发
西风逍遥游2 天前
从 EveScript 到 B站 Toy:一个 WebGPU 游戏发布插件怎样设计
游戏开发
西风逍遥游3 天前
500 单位 RTS 为什么还没到 60 FPS:一次 WebGPU 游戏性能诊断
游戏开发
甲维斯3 天前
Codex,Zcode,KimiCode电脑操作能力对比!
人工智能·游戏开发
SmalBox3 天前
08-02-热更新-差量更新与断点续传
unity3d·游戏开发
UWA3 天前
多人战斗中Lua持续高耗时,是瞬时峰值还是高频调用在作祟
性能优化·lua·游戏开发
ZzT4 天前
vibe coding 做小游戏·问题排查篇:解决包体积、引擎适配与 iPhone 机型适配问题
ai编程·游戏开发