chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -0,0 +1,505 @@
# 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<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
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. 标记为 AddressableAddress 规范:`Data/{类型}/{名称}`,如 `Data/Charms/Charm_QuickSlash`
2. 加入 `Data_{类型}` Group`Data_Charms`
3.`AddressKeys` 注册(或使用 Label 批量加载)
**Label 批量加载示例**(加载全部 CharmSO
```csharp
// 使用 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`,在枚举定义中添加:
```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短音效/ 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` 是**全项目唯一的地址字符串来源**
```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<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 集成(推荐写法)
```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 | 保留 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
### 构建命令
```csharp
// 编辑器菜单 Zeling/Build/Build Addressables
[MenuItem("Zeling/Build/Build Addressables")]
static void BuildAddressables()
{
AddressableAssetSettings.BuildPlayerContent();
Debug.Log("[Addressables] Build complete.");
}
```