# 43 · Addressables 工作流指南 > **适用范围** 所有系统 > **所属文档集** [← 返回索引](./README.md) · [总览](./00_Overview.md) > **依赖** `UnityEngine.AddressableAssets` · `AddressKeys`(`Assets/Scripts/Core/AddressKeys.cs`) --- ## 目录 1. [为什么使用 Addressables](#1-为什么使用-addressables) 2. [核心规则汇总](#2-核心规则汇总) 3. [新增 Prefab 全流程](#3-新增-prefab-全流程) 4. [新增场景(Room)全流程](#4-新增场景room全流程) 5. [新增 ScriptableObject 资产](#5-新增-scriptableobject-资产) 6. [新增 VFX / 粒子资产](#6-新增-vfx--粒子资产) 7. [新增音频资产](#7-新增音频资产) 8. [AddressKeys 维护规范](#8-addresskeys-维护规范) 9. [运行时加载 API 速查](#9-运行时加载-api-速查) 10. [释放规则](#10-释放规则) 11. [Addressable Group 组织规范](#11-addressable-group-组织规范) 12. [常见错误排查](#12-常见错误排查) 13. [构建与发布流程](#13-构建与发布流程) --- ## 1. 为什么使用 Addressables | 传统方式(禁止)| Addressables(必须)| |---------------|-------------------| | `Resources.Load("Enemy/Bat")` | `Addressables.LoadAssetAsync(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 ``` 命名规范:`{类型}_{名称}.prefab`,PascalCase。 ### 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`,在对应区块添加: ```csharp public static class AddressKeys { // ── Enemies ────────────────────────────────────────── public const string PrefabBat = "Prefabs/Enemies/Bat"; public const string PrefabGiantSpider = "Prefabs/Enemies/GiantSpider"; // ← 新增 // ... } ``` ### Step 5 · 在代码中使用 ```csharp // 池化方式(推荐,敌人/VFX/弹射物均用池) var handle = Addressables.InstantiateAsync(AddressKeys.PrefabGiantSpider, position, Quaternion.identity); await handle.Task; GameObject instance = handle.Result; // 回池/释放 Addressables.ReleaseInstance(instance); ``` 若通过 `GlobalObjectPool` 使用,只需注册池即可: ```csharp // 启动时预热(由 PoolInitializer 负责,无需手动调用) GlobalObjectPool.Instance.Prewarm(AddressKeys.PrefabGiantSpider, prewarmCount: 5); ``` ### Step 6 · 验证 1. 在 **Addressables Groups** 窗口按 `Build → New Build → Default Build Script` 确认无报错 2. 运行游戏,在 **Addressables Event Viewer**(`Window → 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. 拖入 Group:`Scenes_Forest` ### Step 4 · 在 AddressKeys 注册 ```csharp 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. 标记为 Addressable,Address 规范:`Data/{类型}/{名称}`,如 `Data/Charms/Charm_QuickSlash` 2. 加入 `Data_{类型}` Group,如 `Data_Charms` 3. 在 `AddressKeys` 注册(或使用 Label 批量加载) **Label 批量加载示例**(加载全部 CharmSO): ```csharp // 使用 Label 一次加载全部魅力 SO(适合数量少时预热) var handle = Addressables.LoadAssetsAsync( 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 - Address:`Prefabs/VFX/Skills/VFX_DeathSoulHit` - Group:`VFX_Poolable` ### Step 3 · 注册到 VFXCatalogSO 打开 `Assets/ScriptableObjects/VFX/VFXCatalog.asset`,在 `HitFxType` → `AssetReferenceGameObject` 映射表中添加: ``` HitFxType.DeathSoulHit → VFX_DeathSoulHit (AssetReferenceGameObject) ``` 若 `HitFxType` 枚举中还没有 `DeathSoulHit`,在枚举定义中添加: ```csharp // Assets/Scripts/VFX/HitFxType.cs public enum HitFxType { NailHit, SoulHit, ParrySuccess, DeathSoulHit, // ← 新增 // ... } ``` ### Step 4 · 在技能代码中触发 ```csharp // 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(短音效)/ Streaming(BGM)| | | Compression Format | Vorbis(Quality 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` 是**全项目唯一的地址字符串来源**: ```csharp // 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(key)` | `AsyncOperationHandle` | `Addressables.Release(handle)` | | 实例化 Prefab | `Addressables.InstantiateAsync(key, pos, rot)` | `AsyncOperationHandle` | `Addressables.ReleaseInstance(instance)` | | 加载场景(Additive)| `Addressables.LoadSceneAsync(key, LoadSceneMode.Additive)` | `AsyncOperationHandle` | `Addressables.UnloadSceneAsync(handle)` | | 批量加载(Label)| `Addressables.LoadAssetsAsync(label, callback)` | `AsyncOperationHandle>` | `Addressables.Release(handle)` | | 预检地址是否有效 | `Addressables.LoadResourceLocationsAsync(key)` | `AsyncOperationHandle>` | `Addressables.Release(handle)` | ### UniTask 集成(推荐写法) ```csharp // 使用 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 | 保留 Handle,GameManager.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 Leaked(Event 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: LZ4,Switch: LZMA) ### 构建命令 ```csharp // 编辑器菜单 Zeling/Build/Build Addressables [MenuItem("Zeling/Build/Build Addressables")] static void BuildAddressables() { AddressableAssetSettings.BuildPlayerContent(); Debug.Log("[Addressables] Build complete."); } ```