Files
zeling_v2/Docs/Design/43_AddressablesWorkflow.md
2026-05-08 11:04:00 +08:00

18 KiB
Raw Permalink Blame History

43 · Addressables 工作流指南

适用范围 所有系统
所属文档集 ← 返回索引 · 总览
依赖 UnityEngine.AddressableAssets · AddressKeysAssets/Scripts/Core/AddressKeys.cs


目录

  1. 为什么使用 Addressables
  2. 核心规则汇总
  3. 新增 Prefab 全流程
  4. 新增场景Room全流程
  5. 新增 ScriptableObject 资产
  6. 新增 VFX / 粒子资产
  7. 新增音频资产
  8. AddressKeys 维护规范
  9. 运行时加载 API 速查
  10. 释放规则
  11. Addressable Group 组织规范
  12. 常见错误排查
  13. 构建与发布流程

1. 为什么使用 Addressables

传统方式(禁止) Addressables必须
Resources.Load("Enemy/Bat") Addressables.LoadAssetAsync<GameObject>(AddressKeys.PrefabBat)
Instantiate(prefabField) 直接序列化引用 Addressables.InstantiateAsync(address)
SceneManager.LoadSceneAsync("Room_Forest_01") Addressables.LoadSceneAsync(AddressKeys.SceneForest01, ...)

收益

  • 细粒度资源加载,仅加载当前需要的内容
  • 场景异步加载不卡顿主线程
  • 支持热更新/内容分包DLC 扩展)
  • 编辑器与运行时行为一致

2. 核心规则汇总

规则 说明
禁止 Resources/ 文件夹 不在 Resources/ 下放置任何游戏资产
禁止裸 GameObject 字段做跨场景 Prefab 引用 使用 AssetReferenceGameObject 替代
禁止魔法字符串 所有 Address 字符串集中在 AddressKeys 静态类,禁止代码中直接写字符串
每个 Prefab 加入一个 Group 不混用"池化资产"与"一次性资产"Group
运行时 Handle 必须 Release LoadAssetAsync 产生的 Handle 必须在不再需要时调用 Release
场景用 Additive 模式 所有 Room 场景使用 LoadSceneMode.Additive,不用 Single

3. 新增 Prefab 全流程

以"新增一个名为 GiantSpider 的敌人 Prefab"为例,完整操作步骤:

Step 1 · 创建并存放 Prefab

Assets/Prefabs/Enemies/GiantSpider.prefab

命名规范:{类型}_{名称}.prefabPascalCase。

Step 2 · 标记为 Addressable

  1. 在 Project 窗口选中 GiantSpider.prefab
  2. Inspector 勾选 Addressable 复选框
  3. 修改自动生成的 Address 为规范格式:Prefabs/Enemies/GiantSpider

⚠️ 默认 Address 是文件路径,必须手动改为规范 Address,否则 AddressKeys 难以维护。

Step 3 · 分配到正确 Group

Addressables Groups 窗口(Window → Asset Management → Addressables → Groups

资产类型 目标 Group
可池化敌人 Prefab Enemies_Poolable
Boss Prefab Bosses
VFX Prefab VFX_Poolable
弹射物 Prefab Projectiles_Poolable
一次性世界物件 Prefab WorldObjects
UI Prefab UI

GiantSpider.prefab 拖入 Enemies_Poolable Group。

Step 4 · 在 AddressKeys 注册

打开 Assets/Scripts/Core/AddressKeys.cs,在对应区块添加:

public static class AddressKeys
{
    // ── Enemies ──────────────────────────────────────────
    public const string PrefabBat         = "Prefabs/Enemies/Bat";
    public const string PrefabGiantSpider = "Prefabs/Enemies/GiantSpider"; // ← 新增
    // ...
}

Step 5 · 在代码中使用

// 池化方式(推荐,敌人/VFX/弹射物均用池)
var handle = Addressables.InstantiateAsync(AddressKeys.PrefabGiantSpider, position, Quaternion.identity);
await handle.Task;
GameObject instance = handle.Result;

// 回池/释放
Addressables.ReleaseInstance(instance);

若通过 GlobalObjectPool 使用,只需注册池即可:

// 启动时预热(由 PoolInitializer 负责,无需手动调用)
GlobalObjectPool.Instance.Prewarm(AddressKeys.PrefabGiantSpider, prewarmCount: 5);

Step 6 · 验证

  1. Addressables Groups 窗口按 Build → New Build → Default Build Script 确认无报错
  2. 运行游戏,在 Addressables Event ViewerWindow → Asset Management → Addressables → Event Viewer)确认资产正常加载/释放,无 Handle 泄漏

4. 新增场景Room全流程

以"新增森林区域第 3 个房间 Room_Forest_03"为例:

Step 1 · 创建场景文件

Assets/Scenes/Forest/Room_Forest_03.unity

命名规范:Room_{区域名}_{序号}.unity(区域名 PascalCase序号两位数字

Step 2 · 场景基础配置

新场景必须包含:

Room_Forest_03 [Scene]
├── Room_Forest_03 [GameObject]        ← 根对象,挂 RoomDataSO 引用
│   ├── Tilemap_Ground                 ← 地面 Tilemap
│   ├── Tilemap_Background             ← 背景 Tilemap遮挡地面之后
│   ├── Tilemap_Foreground             ← 前景 Tilemap角色之前
│   ├── Enemies [空对象]               ← 敌人放置处
│   ├── Interactables [空对象]         ← 可交互物件
│   ├── RoomTransitionPoints [空对象]  ← 房间过渡点
│   └── CinemachineConfiner [Collider] ← 镜头约束边界

Step 3 · 标记为 Addressable 并分配 Group

  1. 选中 Room_Forest_03.unity → Inspector 勾选 Addressable
  2. 修改 Address 为:Scenes/Forest/Room_Forest_03
  3. 拖入 GroupScenes_Forest

Step 4 · 在 AddressKeys 注册

public static class AddressKeys
{
    // ── Scenes ────────────────────────────────────────────
    public const string SceneForest01 = "Scenes/Forest/Room_Forest_01";
    public const string SceneForest02 = "Scenes/Forest/Room_Forest_02";
    public const string SceneForest03 = "Scenes/Forest/Room_Forest_03"; // ← 新增
}

Step 5 · 注册到 RoomRegistry

打开 Assets/ScriptableObjects/World/RoomRegistry.asset,在 rooms 列表中添加新 RoomDataSO

RoomDataSO:
  sceneAddress : "Scenes/Forest/Room_Forest_03"
  regionId     : "Forest"
  displayName  : "森林 · 深处"
  mapGridPos   : (3, 1)          ← 在大地图上的格子坐标
  connections  : [Room_Forest_02(East), Room_Forest_04(South)]

Step 6 · 配置房间过渡点

在场景中的 RoomTransitionPoints 下添加 RoomTransitionTrigger 组件,配置:

transitionTo : "Scenes/Forest/Room_Forest_04"   ← 目标场景 Address
spawnPointId : "SpawnWest"                       ← 目标场景的出生点 ID
direction    : East                              ← 过渡方向(决定淡入/淡出方向)

Step 7 · 验证

运行游戏并从相邻房间过渡进入,确认:

  • 镜头正常约束在新房间边界
  • 不出现"场景未找到"加载错误
  • Event Viewer 中场景 Handle 在离开时正常 Release

5. 新增 ScriptableObject 资产

SO 资产CharmSO、FormSkillSO、AttackPatternSO 等)通常不需要加入 Addressables因为

  • SO 是配置数据,一般由 MonoBehaviour Inspector 直接引用(直接序列化)
  • 运行时不会动态加载/卸载

以下情况例外(需要加入 Addressables

场景 原因
SO 数量极多(如 100+ CharmSO 按需加载,避免初始内存峰值
DLC 扩展内容的 SO 需要热更新时
VFXCatalogSO 中引用的 Prefab 已通过 AssetReferenceGameObject 字段引用,无需单独处理 SO 本身

若 SO 需要 Addressable

  1. 标记为 AddressableAddress 规范:Data/{类型}/{名称},如 Data/Charms/Charm_QuickSlash
  2. 加入 Data_{类型} GroupData_Charms
  3. AddressKeys 注册(或使用 Label 批量加载)

Label 批量加载示例(加载全部 CharmSO

// 使用 Label 一次加载全部魅力 SO适合数量少时预热
var handle = Addressables.LoadAssetsAsync<CharmSO>(
    AddressKeys.LabelCharms,
    charm => _allCharms.Add(charm));
await handle.Task;

6. 新增 VFX / 粒子资产

以"新增命魂形态魂技能命中特效 VFX_DeathSoulHit"为例:

Step 1 · 创建粒子 Prefab

Assets/Prefabs/VFX/Skills/VFX_DeathSoulHit.prefab

命名规范:VFX_{触发时机/位置}_{名称}.prefab

Step 2 · 标记 Addressable分配 Group

  • AddressPrefabs/VFX/Skills/VFX_DeathSoulHit
  • GroupVFX_Poolable

Step 3 · 注册到 VFXCatalogSO

打开 Assets/ScriptableObjects/VFX/VFXCatalog.asset,在 HitFxTypeAssetReferenceGameObject 映射表中添加:

HitFxType.DeathSoulHit → VFX_DeathSoulHit (AssetReferenceGameObject)

HitFxType 枚举中还没有 DeathSoulHit,在枚举定义中添加:

// Assets/Scripts/VFX/HitFxType.cs
public enum HitFxType
{
    NailHit,
    SoulHit,
    ParrySuccess,
    DeathSoulHit,  // ← 新增
    // ...
}

Step 4 · 在技能代码中触发

// FormSkillSO.castFeedback 中的 FeedbackPresetSO 配置 HitFxType = DeathSoulHit
// VFXPool 自动处理加载与回池,无需手动调用 Addressables
_vfxPool.Spawn(HitFxType.DeathSoulHit, hitPosition);

Step 5 · 预热配置

PoolInitializer 资产的 vfxPrewarmList 中添加:

{ address: "Prefabs/VFX/Skills/VFX_DeathSoulHit", count: 3 }

7. 新增音频资产

音频资产通过 AudioEventSO 间接引用,不直接使用 Addressable API 加载

Step 1 · 导入音频文件

Assets/Audio/SFX/Skills/SFX_DeathSoulHit.wav

Step 2 · 导入配置Inspector

设置 说明
Load Type Decompress On Load短音效/ StreamingBGM
Compression Format VorbisQuality 70
Force To Mono SFX/ ✗BGM 立体声)

Step 3 · 创建 AudioEventSO

右键 Assets/ScriptableObjects/Audio/SFX/Skills/Create → Audio/SFX Event

SFX_DeathSoulHit.asset:
  clip     : SFX_DeathSoulHit.wav
  volume   : 0.85
  pitch    : [0.95, 1.05](随机范围)
  mixerGroup : SFX

Step 4 · 在技能 SO 或 FeedbackPresetSO 中引用

音频 SO 通过 Inspector 直接序列化引用(无需 Addressable音频文件小且不动态加卸


8. AddressKeys 维护规范

AddressKeys.cs全项目唯一的地址字符串来源

// Assets/Scripts/Core/AddressKeys.cs
public static class AddressKeys
{
    // ── 场景 ─────────────────────────────────────────────
    public const string ScenePersistent  = "Scene_Persistent";
    public const string SceneMainMenu    = "Scene_MainMenu";
    // 区域场景按区域前缀分组注释
    // [Forest]
    public const string SceneForest01    = "Scenes/Forest/Room_Forest_01";
    // ...

    // ── Prefabs / Enemies ────────────────────────────────
    public const string PrefabBat        = "Prefabs/Enemies/Bat";
    // ...

    // ── Prefabs / VFX ────────────────────────────────────
    public const string PrefabVFXNailHit = "Prefabs/VFX/Hit/VFX_NailHit";
    // ...

    // ── Labels ───────────────────────────────────────────
    public const string LabelEnemy       = "Enemy";
    public const string LabelPoolable    = "Poolable";
    public const string LabelBGM         = "BGM";
    public const string LabelCharms      = "Charms";
}

规范

  • 所有 const string 均以类型前缀命名(Prefab / Scene / Label / Data
  • 新增 Addressable 资产时必须同步添加 AddressKeys 条目PR 不通过禁止合并
  • 旧资产删除时同步删除 AddressKeys 条目并全局搜索引用

9. 运行时加载 API 速查

用途 API 返回值 释放方式
加载资产(不实例化) Addressables.LoadAssetAsync<T>(key) AsyncOperationHandle<T> Addressables.Release(handle)
实例化 Prefab Addressables.InstantiateAsync(key, pos, rot) AsyncOperationHandle<GameObject> Addressables.ReleaseInstance(instance)
加载场景Additive Addressables.LoadSceneAsync(key, LoadSceneMode.Additive) AsyncOperationHandle<SceneInstance> Addressables.UnloadSceneAsync(handle)
批量加载Label Addressables.LoadAssetsAsync<T>(label, callback) AsyncOperationHandle<IList<T>> Addressables.Release(handle)
预检地址是否有效 Addressables.LoadResourceLocationsAsync(key) AsyncOperationHandle<IList<IResourceLocation>> Addressables.Release(handle)

UniTask 集成(推荐写法)

// 使用 UniTask 替代 await handle.Task支持取消令牌
using Cysharp.Threading.Tasks;

async UniTaskVoid LoadEnemyAsync(CancellationToken ct)
{
    var handle = Addressables.InstantiateAsync(
        AddressKeys.PrefabGiantSpider, spawnPos, Quaternion.identity);

    // UniTask 扩展:直接 await AsyncOperationHandle
    GameObject instance = await handle.WithCancellation(ct);

    // 注册到池(由 GlobalObjectPool 接管释放)
    GlobalObjectPool.Instance.Register(instance, handle);
}

10. 释放规则

加载方式 释放时机 释放方法
LoadAssetAsync(普通资产) 不再需要时立即 Release Addressables.Release(handle)
LoadAssetAsync(常驻 SO 游戏整个生命周期内不 Release 保留 HandleGameManager.OnDestroy 统一 Release
InstantiateAsync(单次生成) Destroy 前 Addressables.ReleaseInstance(go)
InstantiateAsync(对象池) 池销毁时 GlobalObjectPool 内部统一 Release
LoadSceneAsync 场景卸载前 Addressables.UnloadSceneAsync(handle)

Handle 泄漏检查:在 Addressables Event Viewer 中,退出游戏后所有 Handle 应回到 0。若仍有残留按 Viewer 中的资产名追查未 Release 的调用方。


11. Addressable Group 组织规范

Group 列表

Group 名 内容 Bundle 模式 说明
Default_LocalGroup 核心启动资产Persistent 场景、InputReaderSO 等) Pack Together 首次加载全量
Scenes_Forest 森林区域所有场景 Pack Separately 按场景分包,按需加载
Scenes_Ruins 废墟区域所有场景 Pack Separately
Enemies_Poolable 所有可池化敌人 Prefab Pack Together 进入区域前预加载
Bosses Boss Prefab Pack Separately 进入 Boss 前加载
VFX_Poolable 所有 VFX 粒子 Prefab Pack Together 游戏启动时全量预热
Projectiles_Poolable 弹射物 Prefab Pack Together 游戏启动时全量预热
UI UI Prefab Pack Together 常驻内存
Audio_BGM BGM AudioClip Pack Separately Streaming按区域切换
Audio_SFX 所有 SFX AudioClip Pack Together 压缩后常驻
Data_Charms 所有 CharmSO若走 Addressable 路线) Pack Together 按需

Bundle 模式说明

模式 适用场景
Pack Together 同类小资产批量打包,减少请求数
Pack Separately 大资产场景、BGM各自独立包按需加载
Pack Together By Label 跨 Group 相同 Label 打到同一包

12. 常见错误排查

Error: InvalidKeyException: No Asset found with key "xxx"

原因Address 字符串与 AddressKeys 中的值不一致,或资产未标记为 Addressable。

排查

  1. Addressables Groups 窗口搜索资产名,确认已加入并 Address 正确
  2. 对比 AddressKeys 中的字符串与 Groups 窗口 Address 列值是否完全一致

Error: UnloadSceneAsync 报 Scene Handle 无效

原因:场景 Handle 已被提前 Release或 Handle 对象被 GC 回收。

修复:将场景 Handle 存储为字段不是局部变量Scene 卸载后再 Release。

Warning: Handle LeakedEvent Viewer 中 Handle 未归零)

排查

  1. 在 Event Viewer 中找到对应资产的 Load 记录
  2. 追查调用 LoadAssetAsync 的位置,确认是否有对应的 Release 调用
  3. 检查是否在异常路径(catch 分支)中跳过了 Release

编辑器里正常Build 后找不到资产

原因:资产未被打入 Bundle或 Build 时 Group 设置为 Use Asset Database (fastest)

修复:正式 Build 前执行 Build → New Build → Default Build Script,确认输出目录有 Bundle 文件。


13. 构建与发布流程

本地开发

阶段 Group Play Mode 说明
日常开发 Use Asset Database (fastest) 跳过打包,直接从 AssetDatabase 加载,速度最快
集成测试 Simulate Groups (advanced) 模拟 Bundle 分包逻辑,不实际打包
发布前验证 Use Existing Build (requires built groups) 使用真实 Bundle完整验证加载流程

Build 前检查清单

  • 所有新增资产已加入正确 Group
  • AddressKeys 中的字符串与 Groups Address 列一致
  • Addressables Groups → Analyze → Check Duplicate Bundle Dependencies 无重复
  • Check Scene to Addressable Duplicate Dependencies 无冲突
  • Event Viewer 退出时 Handle 全为 0
  • 目标平台PC/Switch的 Bundle Compression 设置正确PC: LZ4Switch: LZMA

构建命令

// 编辑器菜单 Zeling/Build/Build Addressables
[MenuItem("Zeling/Build/Build Addressables")]
static void BuildAddressables()
{
    AddressableAssetSettings.BuildPlayerContent();
    Debug.Log("[Addressables] Build complete.");
}