506 lines
18 KiB
Markdown
506 lines
18 KiB
Markdown
# 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. 标记为 Addressable,Address 规范:`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(短音效)/ 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<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 | 保留 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.");
|
||
}
|
||
```
|