10-04-实战-资源组织与依赖管理实战

实战-资源组织与依赖管理实战

篇章 :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

依赖加载顺序:

  1. 先加载 Package_Default
  2. 再加载 Package_Common
  3. 最后加载业务 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 实践建议

  1. 资源组织从一开始就规划好:后期重构成本高
  2. 共享资源适度抽取:过度共享会反向影响
  3. 依赖深度控制在 5 层以内:避免加载慢
  4. 地址命名规范化:用 Location 而非文件路径
  5. 大型项目必须分 Package:单 Package 性能不可接受

上一篇 :Unity Addressable Assets 实战 下一篇 :资源配置与打包

相关推荐
浪浪山_大橙子2 天前
被1.4 MB的射击游戏震撼后,我认真拆了一遍Fable的实现
ai编程·游戏开发·three.js
SmalBox2 天前
10-02-实战-Unity原生AssetBundle实战
unity3d·游戏开发
SmalBox3 天前
10-01-实战-环境搭建与集成
unity3d·游戏开发
SmalBox4 天前
09-04-生态-UOS CDN与服务器部署
unity3d·游戏开发
️公子4 天前
可插拔 13 款实时棋牌对战平台实践:房间容器、Plugin SDK 与幂等账本
websocket·typescript·游戏开发·monorepo·插件架构·棋牌平台·实时对战
SmalBox5 天前
09-03-生态-与GameFramework-xAsset等框架集成
unity3d·游戏开发
SmalBox6 天前
09-02-生态-小游戏平台适配
unity3d·游戏开发
ZzT6 天前
vibe coding 做小游戏·玩法重做篇:只留自走棋、去掉羁绊,用电脑自动对局调难度
ai编程·游戏开发
SmalBox7 天前
09-01-生态-YooAsset+HybridCLR热更新方案
unity3d·游戏开发