实战-资源组织与依赖管理实战
篇章 :10-实战篇-从零搭建 状态 :已完成 阅读时间:约 25 分钟
一、引言
1.1 本章在系列中的定位
资源组织与依赖管理是 YooAsset 工程实践中的第一性原理。资源组织决定了"哪些资源放在一起",依赖管理决定了"资源之间如何引用"。两者共同决定了一个项目的:
- 包体大小:组织不合理会导致重复打包
- 加载性能:依赖混乱会导致加载慢
- 更新成本:依赖图复杂会导致每次更新量巨大
- 可维护性:资源关系清晰的项目更容易维护
本章通过具体的实战案例,讲解如何在 YooAsset 中组织资源、管理依赖。
1.2 本章要解决的核心问题
- 资源组织有哪些常见模式?
- 依赖关系如何识别?如何优化?
- 共享资源如何抽取?
- 大型项目如何分 Package?
1.3 阅读前置要求
- 完成 10-01 到 10-03
- 了解 Unity 资源类型(Prefab、Texture、Audio 等)
- 了解项目工程化基础
二、资源组织模式
2.1 按业务模块组织
最常见的组织方式:
bash
Assets/
├── Common/ # 公共资源
│ ├── Atlas/ # 公共图集
│ ├── Font/ # 字体
│ └── Sound/ # 通用音效
├── Login/ # 登录模块
│ ├── UI/ # 登录 UI
│ ├── Script/ # 登录逻辑
│ └── Scene/ # 登录场景
├── MainCity/ # 主城模块
│ ├── UI/
│ ├── Scene/
│ └── Script/
├── Battle/ # 战斗模块
│ ├── Hero/ # 角色
│ ├── Skill/ # 技能
│ ├── Effect/ # 特效
│ └── Scene/
├── Activity/ # 活动模块
└── DLC/ # 资料片
优点:
- 业务边界清晰
- 模块可独立打包
- 团队协作明确
缺点:
- 资源可能被多个模块共享
- 公共资源归类困难
2.2 按资源类型组织
bash
Assets/
├── Prefabs/ # 所有 Prefab
├── Textures/ # 所有贴图
├── Audio/ # 所有音频
├── Materials/ # 所有材质
├── Animations/ # 所有动画
└── Scenes/ # 所有场景
优点:同类资源集中管理
缺点:
- 不利于按业务打包
- 模块化困难
2.3 按加载时机组织(推荐)
bash
Assets/
├── Startup/ # 启动必需(首包)
│ ├── Logo/
│ ├── Loading/
│ └── Config/
├── Main/ # 主要资源(热更)
│ ├── UI/
│ ├── Audio/
│ └── Config/
├── LazyLoad/ # 按需加载(热更)
│ ├── Battle/
│ ├── Activity/
│ └── DLC/
└── Discard/ # 不打包
└── Editor/
优点:
- 与 YooAsset 的首包/热更设计完美匹配
- 性能优化直观
- 适合 YooAsset 的"按需加载"模型
2.4 按"包"组织
每个 YooAsset Package 对应一个目录:
bash
Assets/
├── Package_Default/ # 默认包
├── Package_Activity/ # 活动包
├── Package_DLC1/ # DLC1 包
└── Package_DLC2/ # DLC2 包
适用场景:项目有明确的"包"概念,如多个游戏模式、多个资料片。
三、依赖分析
3.1 依赖的产生
YooAsset 中的依赖来源于:
| 依赖类型 | 例子 |
|---|---|
| Prefab 引用 | Hero.prefab 引用了 SkillConfig.json |
| 材质引用 | Material.mat 引用了 Texture.png |
| 动画引用 | Animation.anim 引用了 Avatar.asset |
| 脚本引用 | HeroController.cs 引用了 SkillConfig |
| 场景引用 | Battle.unity 引用了多个 Prefab、Light、Audio |
3.2 依赖分析工具
3.2.1 构建报告
YooAsset 构建后,会生成详细的依赖报告:
erlang
BuildReport.html
├── Bundle 列表
│ ├── bundle_01
│ ├── bundle_02
│ └── ...
├── 资源依赖图
└── 重复资源检测
3.2.2 Editor 端依赖分析
csharp
public class DependencyAnalyzer : EditorWindow
{
[MenuItem("YooAsset/Analyze Dependencies")]
public static void Analyze()
{
var settings = AssetBundleCollectorSetting.GetDefaultCollector();
var buildMap = AssetBundleCollector.BuildMap();
// 1. 列出所有 Bundle 的依赖
foreach (var bundle in buildMap.BundleList)
{
Debug.Log($"Bundle: {bundle.BundleName}");
foreach (var dep in bundle.Dependencies)
{
Debug.Log($" 依赖: {dep}");
}
}
// 2. 检测循环依赖
var hasCycle = DetectCycle(buildMap);
if (hasCycle)
Debug.LogError("检测到循环依赖!");
}
static bool DetectCycle(BuildMap buildMap)
{
var visited = new HashSet<string>();
var recursionStack = new HashSet<string>();
foreach (var bundle in buildMap.BundleList)
{
if (HasCycleDFS(bundle, visited, recursionStack, buildMap))
return true;
}
return false;
}
}
3.3 依赖可视化
YooAsset 不直接提供依赖图的可视化,但可以通过其他工具:
| 工具 | 功能 |
|---|---|
| Unity Asset Bundle Browser | 官方工具,查看 AB 依赖 |
| AssetStudio | 第三方,解析 AB 内容 |
| Graphviz | 通用图可视化 |
| 自己实现 | 通过 BuildMap 输出 DOT 文件 |
csharp
public class DependencyVisualizer
{
public static void ExportToDOT(BuildMap buildMap)
{
var sb = new StringBuilder();
sb.AppendLine("digraph Dependencies {");
foreach (var bundle in buildMap.BundleList)
{
foreach (var dep in bundle.Dependencies)
{
sb.AppendLine($" \"{bundle.BundleName}\" -> \"{dep}\";");
}
}
sb.AppendLine("}");
File.WriteAllText("dependencies.dot", sb.ToString());
}
}
四、共享资源抽取
4.1 共享资源的概念
共享资源 (Shared Asset)是指被多个 Bundle 引用的资源。共享资源如果不抽离出来,会导致:
ini
Bundle_A 包含 SharedTexture.png
Bundle_B 包含 SharedTexture.png
Bundle_C 包含 SharedTexture.png
总大小 = 3 * SharedTexture.png = 浪费 2/3 空间
4.2 共享资源抽取策略
4.2.1 自动抽取(推荐)
YooAsset 在构建时会自动识别共享资源,并将其抽取为独立的 Bundle:
Bundle_A Bundle_B
↓ ↓
Shared_Bundle(自动生成)
SharedTexture.png
4.2.2 手动标记共享
在 Editor 中显式标记共享资源:
csharp
public class SharedAssetRule : IPackRule
{
public bool Match(AssetInfo info)
{
// 共享图集规则
return info.AssetPath.StartsWith("Assets/Common/Atlas/");
}
public string GetBundleName(AssetInfo info)
{
return "shared_atlas"; // 强制打到一个 Bundle
}
}
4.2.3 共享包的最佳实践
| 资源类型 | 是否共享 | 理由 |
|---|---|---|
| 公共 UI 图集 | ✅ | 多个 UI 共享 |
| 字体 | ✅ | 全局共享 |
| 通用配置 | ✅ | 多个系统读取 |
| 主城模型 | ❌ | 仅主城使用 |
| 战斗角色 | ❌ | 仅战斗使用 |
| 活动资源 | ❌ | 仅活动使用 |
4.3 避免过度共享
反例:
css
shared_bundle(包含 1000 个资源)
└── Bundle_A、B、C、D... 都依赖它
问题:
- shared_bundle 任何资源修改都会导致整个共享包重打
- 所有依赖它的 Bundle 都需重新下载
解决:按"使用频率"分类共享:
shared_hot(高频共享)
├── UI 公共图集
└── 字体
shared_warm(中频共享)
├── 角色基础模型
└── 通用特效
shared_cold(低频共享)
├── 历史活动图标
└── 远期功能 UI
五、依赖优化
5.1 依赖深度
依赖深度是指:从最顶层资源到最底层资源的依赖层数。
scss
Hero.prefab (深度 0)
↓ 引用
SkillConfig.json (深度 1)
↓ 引用
CharacterConfig.json (深度 2)
↓ 引用
GlobalConfig.json (深度 3)
依赖深度越深,加载时需要加载的中间层越多。
最佳实践:
- 核心资源:深度 ≤ 3
- 一般资源:深度 ≤ 5
- 边缘资源:深度 ≤ 8
5.2 减少依赖的方法
5.2.1 拆分大 Prefab
❌ 错误:一个大 Prefab 包含所有
Player_AllInOne.prefab
├── 模型
├── 动画
├── 技能
├── 装备
└── 特效
✅ 正确:拆分为多个小 Prefab
Player_Model.prefab
Player_Animation.prefab
Player_Skills.prefab
Player_Equipment.prefab
Player_Effects.prefab
5.2.2 使用 ScriptableObject
将配置数据抽离为 ScriptableObject:
csharp
// 数据类
[CreateAssetMenu(fileName = "HeroConfig", menuName = "Config/Hero")]
public class HeroConfig : ScriptableObject
{
public int HeroId;
public string Name;
public int HP;
public int Attack;
}
// Prefab 引用 ScriptableObject(不引用具体数值)
public class HeroController : MonoBehaviour
{
public HeroConfig Config; // 引用配置
}
5.2.3 异步加载链
避免"加载一个大 Prefab 必须先加载所有依赖",改为"按需加载":
csharp
public class OnDemandLoader
{
public async Task<T> LoadHero<T>(string heroAddress) where T : MonoBehaviour
{
// 1. 先加载核心 Prefab
var coreOp = YooAssets.LoadAssetAsync<GameObject>($"Hero/{heroAddress}_Core");
await coreOp.Task;
var hero = Instantiate(coreOp.Result);
// 2. 异步加载技能
var skillOp = YooAssets.LoadAssetAsync<SkillConfig>($"Hero/{heroAddress}_Skill");
// 不等 skill 加载完就让玩家看到 Hero
return hero.GetComponent<T>();
}
}
5.3 依赖检测脚本
csharp
public class DependencyOptimizer
{
[MenuItem("YooAsset/Optimize Dependencies")]
public static void Optimize()
{
var buildMap = AssetBundleCollector.BuildMap();
// 1. 检测深度过深的依赖
foreach (var bundle in buildMap.BundleList)
{
var depth = CalculateDepth(bundle, buildMap);
if (depth > 5)
{
Debug.LogWarning($"Bundle {bundle.BundleName} 依赖深度 {depth} 过深");
}
}
// 2. 检测被多次引用的资源
var refCount = new Dictionary<string, int>();
foreach (var bundle in buildMap.BundleList)
{
foreach (var asset in bundle.Assets)
{
if (!refCount.ContainsKey(asset.AssetPath))
refCount[asset.AssetPath] = 0;
refCount[asset.AssetPath]++;
}
}
foreach (var kvp in refCount)
{
if (kvp.Value > 3)
{
Debug.LogWarning($"资源 {kvp.Key} 被 {kvp.Value} 个 Bundle 引用,建议抽取为共享包");
}
}
}
}
六、大型项目的分 Package 策略
6.1 单 Package 的局限
当项目资源总量超过 10GB 时,单 Package 会有:
- 构建时间长(30+ 分钟)
- 清单文件大(>10MB)
- 加载速度慢(解析清单慢)
- 团队协作冲突(所有人修改同一个清单)
6.2 分 Package 模式
6.2.1 按业务分 Package
bash
Package_Default # 主包(必备资源)
Package_Battle # 战斗模块
Package_Activity # 活动模块
Package_DLC1 # DLC1
Package_Language # 多语言
6.2.2 按平台分 Package
bash
Package_PC # PC 平台专属
Package_Mobile # 移动平台专属
Package_WebGL # WebGL 专属
6.2.3 按加载时机分 Package
bash
Package_Startup # 启动时(首包)
Package_Login # 登录(启动后立即)
Package_MainCity # 主城(登录后)
Package_Battle # 战斗(按需)
6.3 多 Package 的依赖管理
Package_Default
↓ 依赖
Package_Common(共享 UI、配置)
Package_Battle
↓ 依赖
Package_Common
Package_Activity
↓ 依赖
Package_Common
依赖加载顺序:
- 先加载 Package_Default
- 再加载 Package_Common
- 最后加载业务 Package
csharp
public class MultiPackageLoader
{
public async Task InitializeAll()
{
// 1. 默认包
await YooAssets.InitializeAsync();
// 2. 公共包
var commonOp = YooAssets.LoadPackageAsync("Common");
await commonOp.Task;
// 3. 业务包(并行)
var tasks = new[]
{
YooAssets.LoadPackageAsync("Battle").Task,
YooAssets.LoadPackageAsync("Activity").Task,
YooAssets.LoadPackageAsync("DLC1").Task
};
await Task.WhenAll(tasks);
}
}
七、资源命名规范
7.1 命名规则
模块前缀 + 资源类型 + 资源名
例:
ui_button_normal ← UI 模块的普通按钮
ui_icon_home ← UI 模块的 Home 图标
hero_assassin ← 英雄:刺客
skill_fireball ← 技能:火球
audio_bgm_main ← 背景音乐:主城
config_character ← 角色配置
7.2 命名一致性
- 统一前缀 :按模块统一前缀(
ui_、hero_、skill_等) - 小写 + 下划线:避免大小写问题
- 避免中文:不同操作系统对中文路径处理不同
- 避免特殊字符:不要有空格、括号等
7.3 地址命名 vs 文件路径
csharp
// YooAsset 推荐:使用 Location 而非路径
// Location 类似于"逻辑名",与文件路径解耦
// ❌ 不推荐:用文件路径
var op = YooAssets.LoadAssetAsync<Sprite>("Assets/Art/UI/Button_Normal.png");
// ✅ 推荐:用 Location
var op = YooAssets.LoadAssetAsync<Sprite>("ui_button_normal");
通过 AssetBundleCollector 配置 Address:
csharp
public class AddressableConfig
{
public string AssetPath = "Assets/Art/UI/Button_Normal.png";
public string Address = "ui_button_normal"; // 显式指定
}
八、实战案例
8.1 案例:MMO 项目
makefile
项目: MMO Game
资源总量: 15GB
Package 数: 5 个
Package 划分:
├── Package_Default (2GB)
│ ├── 启动资源
│ ├── 登录资源
│ ├── 主城基础资源
│ └── 主线 1-3 章
│
├── Package_Common (1GB)
│ ├── 公共 UI
│ ├── 公共特效
│ ├── 字体
│ └── 通用配置
│
├── Package_DLC1 (5GB)
│ ├── DLC1 地图
│ ├── DLC1 角色
│ └── DLC1 任务
│
├── Package_Activity (2GB)
│ ├── 节日活动
│ ├── 限时副本
│ └── 商城资源
│
└── Package_Battle (5GB)
├── PVE 副本
├── PVP 战场
└── Boss 战
8.2 案例:休闲小游戏
makefile
项目: Casual Game
资源总量: 500MB
Package 数: 3 个
Package 划分:
├── Package_Startup (10MB)
│ ├── Logo
│ ├── Loading
│ └── 配置
│
├── Package_Main (200MB)
│ ├── 主玩法
│ ├── 主关卡 1-100
│ └── 主 UI
│
└── Package_DLC (290MB)
├── DLC 关卡 101-200
├── DLC 角色
└── DLC 皮肤
九、总结
9.1 本章要点回顾
- 资源组织模式:按业务、按类型、按加载时机、按 Package
- 依赖管理:识别依赖、抽取共享、减少深度
- 共享资源:自动 + 手动抽取,避免过度共享
- 多 Package:大型项目的必然选择
9.2 与前后章节的关联
- 前章:10-03 介绍了 Addressable
- 本章:聚焦资源组织和依赖管理
- 后章:10-05 将介绍资源配置与打包
9.3 实践建议
- 资源组织从一开始就规划好:后期重构成本高
- 共享资源适度抽取:过度共享会反向影响
- 依赖深度控制在 5 层以内:避免加载慢
- 地址命名规范化:用 Location 而非文件路径
- 大型项目必须分 Package:单 Package 性能不可接受
上一篇 :Unity Addressable Assets 实战 下一篇 :资源配置与打包