热更新-首包资源策略
篇章 :08-核心技术-热更新系统 状态 :已完成 阅读时间:约 20 分钟
一、引言
1.1 本章在系列中的定位
首包资源策略是 YooAsset 热更新架构设计中最关键的一环。它决定了玩家第一次下载游戏时,游戏包有多大、进入游戏需要等多长时间、后续更新要下载多少内容。
如果说版本管理是"物流系统的单号系统",那么首包资源策略就是"仓库的初始库存":放太多会撑爆仓库(包体积超大),放太少又会缺货(玩家进入游戏就要等下载)。
1.2 本章要解决的核心问题
- 首包应该放什么资源?什么资源应该走热更?
- 三种主流策略(零资源、全量、混合)如何选择?
- 资源分包如何规划?按场景?按功能?
- 首包大小如何控制?业内标准是什么?
1.3 阅读前置要求
- 了解 YooAsset 的构建流程(参考 04-源码深度-编辑器模块)
- 了解 YooAsset 的运行模式(EditorPlayMode、HostPlayMode)
- 了解 Unity 出包流程(Android / iOS 包体限制)
二、零资源安装包
2.1 零资源安装包的概念
零资源安装包 (也称"裸包"、"超小包")是指游戏的安装包内只包含最核心的启动资源,其他所有资源都通过热更下载。
python
零资源包内容:
├── 启动代码(C# DLL)
├── YooAsset Runtime
├── 启动场景(最小化)
├── 启动资源(Logo、Loading、配置)
└── 初始清单(PackageManifest_1.0.0.bytes)
2.2 零资源安装包的实现
2.2.1 核心资源识别
首包中必须包含的资源:
| 类别 | 内容 | 必要性 |
|---|---|---|
| 启动代码 | YooAsset Runtime、Hotfix DLL | 必须 |
| 启动场景 | Splash、Loading | 必须 |
| 启动 UI | Logo、Loading 界面 | 必须 |
| 核心配置 | 版本配置、玩家配置 | 必须 |
| 必要资源 | 字体、默认 UI、主城极简资源 | 必须 |
| 业务资源 | 关卡、角色、UI 等 | 不必 |
2.2.2 YooAsset 的"零资源"配置
在 YooAsset 的 IBuildPipeline 中,可以通过 PackRule 控制资源的归属:
csharp
public class ZeroResourceBuildPipeline : IBuildPipeline
{
public void CreateBuild()
{
// 1. 启动 Bundle 放首包
var startupRule = new PackRule
{
Name = "StartupPack",
Filter = "Assets/Startup/**/*",
PackTo = EBundlePackTarget.InstallPackage // 打包到首包
};
// 2. 其他所有资源都走热更
var normalRule = new PackRule
{
Name = "NormalPack",
Filter = "Assets/**/*",
PackTo = EBundlePackTarget.HotUpdatePackage // 打包到热更包
};
}
}
2.2.3 启动流程
csharp
public class ZeroResourceStartup
{
public async Task StartupAsync()
{
// 1. 加载首包中的启动资源
var logoOp = YooAssets.LoadAssetAsync<Sprite>("Assets/Startup/UI/Logo.png");
await logoOp.Task;
ShowLogo(logoOp.Result);
// 2. 初始化 YooAsset(连接 CDN)
await YooAssets.InitializeAsync();
// 3. 检查版本
var checkOp = YooAssets.RequestCheckVersionAsync();
await checkOp.Task;
if (checkOp.Result.IsNewVersion)
{
// 4. 下载热更资源
var downloadOp = YooAssets.DownloadUpdateAsync();
ShowDownloadProgress(downloadOp);
await downloadOp.Task;
}
// 5. 进入主城
var mainCityOp = YooAssets.LoadSceneAsync("Assets/Scenes/MainCity.unity");
await mainCityOp.Task;
}
}
2.3 零资源安装包的挑战
2.3.1 启动等待
| 玩家网络 | 启动资源大小 | 预计下载时间 |
|---|---|---|
| 4G | 200MB | 2-3 分钟 |
| WiFi | 200MB | 30-60 秒 |
| 弱网 | 200MB | 5-10 分钟 |
启动等待是零资源包最大的痛点,必须有良好的 Loading 体验:
- 进度可视化:明确告诉玩家"正在下载什么"
- 动画过渡:用精致的动画掩盖等待感
- 后台下载:在 Loading 期间同时下载多个资源
- 可取消:允许玩家取消(不推荐强引导下载)
2.3.2 网络依赖
零资源包强依赖网络,离线状态下游戏完全无法启动。应对方案:
csharp
public class OfflineFallback
{
public void OnStartupFailed()
{
// 如果热更下载失败
if (!NetworkManager.IsAvailable())
{
// 检查本地是否有任何可用清单
if (HasLocalManifest())
{
// 使用本地清单进入游戏(资源可能不完整)
YooAssets.ForceChangeVersion(GetLocalVersion());
EnterGame();
}
else
{
// 完全没有资源,提示玩家联网
ShowOfflineHint();
}
}
}
}
2.3.3 用户体验挑战
- 首次启动慢:玩家期望"秒开",零资源包无法满足
- 心理预期管理:在应用商店描述、买量素材中说明游戏大小
- 玩家流失风险:启动等待期间的流失率明显高于全量包
三、全量包
3.1 全量包的概念
全量包 (也称"完整包"、"大包")是指游戏的安装包内包含所有资源,玩家下载安装后无需热更即可完整体验游戏。
全量包内容:
├── 启动代码
├── YooAsset Runtime
├── 启动场景
├── 所有 UI
├── 所有关卡
├── 所有角色
├── 所有音频
└── 完整初始清单
3.2 全量包的优势
3.2.1 体验最佳
| 维度 | 全量包 | 零资源包 |
|---|---|---|
| 启动时间 | 秒开 | 几分钟 |
| 离线游玩 | 完全支持 | 不支持 |
| 玩家预期 | 与商店描述一致 | 需额外说明 |
| 留存率 | 高 | 略低 |
3.2.2 适用场景
- 休闲单机游戏:如消除、卡牌、单机 RPG
- 小游戏(微信、抖音):对包体极度敏感,资源总量小
- 离线优先游戏:玩家可能在飞行模式下玩
- Demo 版本:让玩家完整体验后再决定是否深入
3.2.3 YooAsset 全量包配置
csharp
public class FullResourceBuildPipeline : IBuildPipeline
{
public void CreateBuild()
{
// 所有资源都打包到首包
var fullRule = new PackRule
{
Name = "FullPack",
Filter = "Assets/**/*",
PackTo = EBundlePackTarget.InstallPackage
};
// 仍可保留热更通道(用于后续更新)
var hotUpdateRule = new PackRule
{
Name = "HotUpdatePack",
Filter = "Assets/HotUpdate/**/*",
PackTo = EBundlePackTarget.HotUpdatePackage
};
}
}
3.3 全量包的挑战
3.3.1 包体积过大
| 平台 | 包体限制 | 后果 |
|---|---|---|
| iOS App Store | 4GB(蜂窝下载限制 200MB) | 超过 200MB 必须 WiFi 下载 |
| Google Play | 150MB(APK) | 超过需用 AAB + 扩展包 |
| Android(国内) | 渠道差异较大 | 小米 100MB、华为 200MB |
| 微信小游戏 | 20MB(首包) | 资源必须全部走 CDN |
| 抖音小游戏 | 10MB(首包) | 资源必须全部走 CDN |
3.3.2 更新成本高
每次发版需要重新下载整个包:
- 1.0.5 → 1.0.6 改动 50MB 资源
- 全量包:必须下载完整新包(可能是几 GB)
- 增量包:只下载 50MB 差异
3.3.3 存储占用大
玩家设备上需要保留所有资源,对于存储空间紧张的低端机不友好。
四、混合策略
4.1 混合策略的概念
混合策略是商业项目最常用的方案:核心资源放首包,扩展资源走热更。
markdown
混合包内容:
├── 启动代码
├── 核心资源(必须)
│ ├── Logo、Loading
│ ├── 主城基础场景
│ ├── 玩家创建角色资源
│ ├── 主线关卡 1-5
│ └── 字体、默认 UI
│
└── 扩展资源(热更)
├── 支线关卡 6+
├── 活动资源
├── 商城皮肤
└── 远期 DLC
4.2 混合策略的实现
4.2.1 资源分类原则
判断一个资源是否应该放首包:
| 原则 | 含义 | 示例 |
|---|---|---|
| 启动必需 | 没有它游戏无法启动 | Logo、Loading、字体 |
| 早期必须 | 玩家前 10 分钟会用到 | 主城、初始角色、新手关 |
| 网络无法访问 | 玩家可能完全没网 | 关键剧情资源 |
| 体积关键 | 资源较小 | 小 UI、配置表 |
| 大型可选 | 资源大但不急需 | 高清材质、远期皮肤 |
4.2.2 首包大小控制
业内参考标准:
| 平台 | 推荐首包大小 | 极限首包大小 |
|---|---|---|
| iOS | < 200MB | < 1GB |
| Android | < 150MB | < 500MB |
| 小游戏 | < 10MB | < 20MB |
| 海外 | < 100MB | < 300MB |
4.2.3 YooAsset 的混合配置
csharp
public class HybridBuildPipeline : IBuildPipeline
{
public List<PackRule> GetPackRules()
{
return new List<PackRule>
{
// 1. 启动必备:打包到首包
new PackRule
{
Name = "StartupPack",
Filter = "Assets/Startup/**/*",
PackTo = EBundlePackTarget.InstallPackage
},
// 2. 主线关卡:打包到首包
new PackRule
{
Name = "MainLevelPack",
Filter = "Assets/Levels/Main/**/*.unity",
PackTo = EBundlePackTarget.InstallPackage
},
// 3. 主城基础:打包到首包
new PackRule
{
Name = "MainCityPack",
Filter = "Assets/City/MainCity/**/*",
PackTo = EBundlePackTarget.InstallPackage
},
// 4. 其他资源:打包到热更包
new PackRule
{
Name = "HotUpdatePack",
Filter = "Assets/**/*",
PackTo = EBundlePackTarget.HotUpdatePackage,
ExcludeFilters = new[] { "Assets/Startup/**/*", "Assets/Levels/Main/**/*", "Assets/City/MainCity/**/*" }
}
};
}
}
4.3 混合策略的优化
4.3.1 核心资源识别
精确识别"核心资源"是混合策略的关键。常见方法:
csharp
public class CoreResourceAnalyzer
{
/// <summary>
/// 通过玩家行为日志识别核心资源
/// </summary>
public List<string> IdentifyCoreResources()
{
var stats = LoadPlayerBehaviorStats();
var coreResources = stats.Resources
.Where(r => r.FirstSessionUsageRate > 0.8) // 80% 玩家首次会话就用到
.Where(r => r.First10MinUsageRate > 0.5) // 50% 玩家在 10 分钟内用到
.Select(r => r.ResourcePath)
.ToList();
return coreResources;
}
}
4.3.2 首包大小控制技巧
| 技巧 | 说明 | 效果 |
|---|---|---|
| 资源压缩 | LZ4 / LZMA | 减少 30-50% |
| 图片压缩 | Crunch、ASTC | 减少 50%+ |
| 音频压缩 | Vorbis、OPus | 减少 70%+ |
| 网格简化 | LOD、减面 | 减少 50%+ |
| 剔除冗余 | 资源去重 | 减少 10-30% |
| 拆分初始 | 启动最简,其他延迟 | 减少 50%+ |
4.3.3 渐进式加载
csharp
public class ProgressiveLoad
{
public async Task ProgressiveLoadMainCity()
{
// 1. 先加载最核心的(主城框架 + 玩家自己)
await LoadBundles(new[] { "maincity_framework", "player_self" });
// 2. 显示主城,玩家可以开始移动
EnterMainCity();
// 3. 后台加载其他玩家、NPC、装饰
await LoadBundlesBackground(new[] { "maincity_others", "maincity_npc", "maincity_decoration" });
}
}
五、分包策略
5.1 分包策略的概念
分包策略 是指将热更资源按某种规则拆分为多个包,按需下载。它的本质是"更细粒度的混合策略"。
css
分包示例:
├── 首包(200MB)
│ ├── 启动资源
│ └── 主线前 5 关
│
├── 热更包 A(活动包,200MB)
│ ├── 春节活动
│ ├── 圣诞活动
│ └── 节日 UI
│
├── 热更包 B(DLC 包,500MB)
│ ├── 资料片 1
│ ├── 资料片 2
│ └── 新角色
│
└── 热更包 C(语音包,300MB)
├── 中文语音
├── 英文语音
└── 日文语音
5.2 分包策略的实现
5.2.1 按功能分包
csharp
public class FunctionalPackRule
{
public List<PackRule> GetRules()
{
return new List<PackRule>
{
new PackRule
{
Name = "ActivityPack",
Filter = "Assets/Activities/**/*",
PackTo = EBundlePackTarget.HotUpdatePackage,
Tag = "activity" // 标签:活动包
},
new PackRule
{
Name = "DLCPack",
Filter = "Assets/DLC/**/*",
PackTo = EBundlePackTarget.HotUpdatePackage,
Tag = "dlc" // 标签:DLC 包
},
new PackRule
{
Name = "LanguagePack",
Filter = "Assets/Audio/Localized/**/*",
PackTo = EBundlePackTarget.HotUpdatePackage,
Tag = "language" // 标签:语言包
}
};
}
}
5.2.2 按场景分包
csharp
public class ScenePackRule
{
public List<PackRule> GetRules()
{
var rules = new List<PackRule>();
var scenes = GetAllScenes();
foreach (var scene in scenes)
{
rules.Add(new PackRule
{
Name = $"{scene.Name}Pack",
Filter = $"Assets/Scenes/{scene.Name}/**/*",
PackTo = EBundlePackTarget.HotUpdatePackage,
Tag = $"scene_{scene.Name}"
});
}
return rules;
}
}
5.2.3 按类型分包
csharp
public class TypePackRule
{
public List<PackRule> GetRules()
{
return new List<PackRule>
{
new PackRule
{
Name = "TexturePack",
Filter = "Assets/Textures/**/*",
Tag = "texture"
},
new PackRule
{
Name = "AudioPack",
Filter = "Assets/Audio/**/*",
Tag = "audio"
},
new PackRule
{
Name = "PrefabPack",
Filter = "Assets/Prefabs/**/*",
Tag = "prefab"
}
};
}
}
5.3 分包策略的挑战
5.3.1 包管理复杂
分包越多,包管理越复杂。YooAsset 通过 Package 概念管理多个包:
csharp
// 在 YooAssets.InitializeAsync 时配置多个包
public async Task InitializeMultiplePackages()
{
// 1. 初始化主包
await YooAssets.InitializeAsync();
// 2. 按需加载其他包
var activityOp = YooAssets.LoadPackageAsync("ActivityPackage");
await activityOp.Task;
var dlcOp = YooAssets.LoadPackageAsync("DLCPackage");
await dlcOp.Task;
}
5.3.2 依赖关系管理
包之间存在依赖关系,需要精心设计:
css
包 A(活动) 依赖 包 X(公共 UI)
包 B(DLC) 依赖 包 X(公共 UI)
如果只下载包 A 而不下载包 X,会导致 UI 缺失
csharp
public class PackageDependency
{
public List<string> GetDependencies(string packageName)
{
return packageName switch
{
"ActivityPackage" => new[] { "CommonUIPackage" },
"DLCPackage" => new[] { "CommonUIPackage" },
"LanguagePackage" => new[] { "CommonAudioPackage" },
_ => new List<string>()
};
}
public async Task LoadPackageWithDependencies(string packageName)
{
var deps = GetDependencies(packageName);
foreach (var dep in deps)
{
await YooAssets.LoadPackageAsync(dep);
}
await YooAssets.LoadPackageAsync(packageName);
}
}
5.3.3 下载调度
多包场景下,需要一个统一的下载调度器:
csharp
public class MultiPackageDownloadScheduler
{
public async Task DownloadAll(List<PackageInfo> packages)
{
// 1. 构建全局下载队列
var allBundles = new List<BundleInfo>();
foreach (var pkg in packages)
{
allBundles.AddRange(pkg.PendingBundles);
}
// 2. 去重(多个包可能依赖同一个 Bundle)
allBundles = allBundles.Distinct().ToList();
// 3. 排序(共享资源优先)
allBundles = allBundles
.OrderByDescending(b => b.IsShared)
.ThenBy(b => b.Priority)
.ToList();
// 4. 执行下载
var op = YooAssets.DownloadUpdateAsync(bundles: allBundles);
await op.Task;
}
}
六、首包规划的实战建议
6.1 首包资源清单模板
markdown
首包资源清单 (项目: MyGame)
========================
[启动核心] 5MB
- Logo (3MB)
- Loading UI (0.5MB)
- 启动场景 (1.5MB)
[字体] 8MB
- 默认字体 (5MB)
- 多语言字体 (3MB)
[主城] 30MB
- 主城场景 (15MB)
- 主城 UI (5MB)
- 主城 NPC (5MB)
- 主城特效 (5MB)
[新手关卡] 50MB
- 关卡 1-1 (15MB)
- 关卡 1-2 (15MB)
- 关卡 1-3 (15MB)
- 新手教学 UI (5MB)
[通用配置] 2MB
- 角色配置表
- 关卡配置表
- 道具配置表
[音频核心] 10MB
- 背景音乐 1 首
- 通用音效 100 个
首包总计: ~105MB
6.2 决策流程图
css
[开始]
│
▼
[游戏是单机还是联机?]
│
├── 单机 → [是否需要离线游玩?]
│ │
│ ├── 是 → [资源总量是否 < 200MB?]
│ │ │
│ │ ├── 是 → 全量包
│ │ └── 否 → 混合策略
│ │
│ └── 否 → 零资源包
│
└── 联机 → [玩家流失敏感度?]
│
├── 高 → 混合策略
│
└── 低 → 零资源包
6.3 实践经验
- 首包要"刚好够用":放太多浪费玩家流量,放太少启动等待长
- 首包决策要做 AB 测试:同样游戏,不同首包大小,看哪个留存高
- 热更资源要分批上线:新版本分多次热更,避免一次下载量太大
- 下载要可恢复:网络中断后下次启动继续,不从头开始
- 监控先行:上线后必须监控首包下载成功率、平均时长
七、总结
6.1 本章要点回顾
- 零资源包:包体最小,但启动慢、强依赖网络
- 全量包:体验最好,但包体大、更新成本高
- 混合策略:商业项目首选,平衡包体与体验
- 分包策略:混合策略的延伸,按需拆分资源
6.2 与前后章节的关联
- 前章:08-03 边玩边下载解决"运行时下载"
- 本章:解决"什么应该预下载、什么应该运行时下载"
- 后章:08-05 完整热更新流程实战串联所有知识点
6.3 实践建议
- 首包大小控制在 100-200MB:兼顾玩家体验与平台限制
- 核心资源识别要数据驱动:用玩家行为日志辅助决策
- 混合策略是商业项目首选,零资源/全量包只在特殊场景使用
- 分包要按"业务可独立"原则:每个分包都应该有明确的业务边界
- 首包决策要做 AB 测试:不同首包大小对留存的影响显著
下一篇 :完整热更新流程实战