18 KiB
43 · Addressables 工作流指南
适用范围 所有系统
所属文档集 ← 返回索引 · 总览
依赖UnityEngine.AddressableAssets·AddressKeys(Assets/Scripts/Core/AddressKeys.cs)
目录
- 为什么使用 Addressables
- 核心规则汇总
- 新增 Prefab 全流程
- 新增场景(Room)全流程
- 新增 ScriptableObject 资产
- 新增 VFX / 粒子资产
- 新增音频资产
- AddressKeys 维护规范
- 运行时加载 API 速查
- 释放规则
- Addressable Group 组织规范
- 常见错误排查
- 构建与发布流程
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
命名规范:{类型}_{名称}.prefab,PascalCase。
Step 2 · 标记为 Addressable
- 在 Project 窗口选中
GiantSpider.prefab - Inspector 勾选 Addressable 复选框
- 修改自动生成的 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 · 验证
- 在 Addressables Groups 窗口按
Build → New Build → Default Build Script确认无报错 - 运行游戏,在 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
- 选中
Room_Forest_03.unity→ Inspector 勾选 Addressable - 修改 Address 为:
Scenes/Forest/Room_Forest_03 - 拖入 Group:
Scenes_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:
- 标记为 Addressable,Address 规范:
Data/{类型}/{名称},如Data/Charms/Charm_QuickSlash - 加入
Data_{类型}Group,如Data_Charms - 在
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
- 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,在枚举定义中添加:
// 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(短音效)/ 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 是全项目唯一的地址字符串来源:
// 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 | 保留 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。
排查:
- 在
Addressables Groups窗口搜索资产名,确认已加入并 Address 正确 - 对比
AddressKeys中的字符串与 Groups 窗口 Address 列值是否完全一致
Error: UnloadSceneAsync 报 Scene Handle 无效
原因:场景 Handle 已被提前 Release,或 Handle 对象被 GC 回收。
修复:将场景 Handle 存储为字段(不是局部变量),Scene 卸载后再 Release。
Warning: Handle Leaked(Event Viewer 中 Handle 未归零)
排查:
- 在 Event Viewer 中找到对应资产的 Load 记录
- 追查调用
LoadAssetAsync的位置,确认是否有对应的Release调用 - 检查是否在异常路径(
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)
构建命令
// 编辑器菜单 Zeling/Build/Build Addressables
[MenuItem("Zeling/Build/Build Addressables")]
static void BuildAddressables()
{
AddressableAssetSettings.BuildPlayerContent();
Debug.Log("[Addressables] Build complete.");
}