docs: 添加资源加载最佳实践文档
This commit is contained in:
@@ -0,0 +1,501 @@
|
||||
# 资源加载最佳实践指南
|
||||
|
||||
> AIBIS Dream 项目资源加载规范
|
||||
|
||||
---
|
||||
|
||||
## 快速参考表
|
||||
|
||||
| 场景 | 推荐方式 | 备注 |
|
||||
|-----|---------|------|
|
||||
| 场景初始化 | `ResourceSystem.LoadAsync` + yield return | 配合 PreloadByLabel 可减少 IO 等待 |
|
||||
| 运行时加载 | `ResourceSystem.LoadAsync` | SceneResourceLoader 自动缓存同 Key 资源 |
|
||||
| 常驻资源 | `ResourceSystem.LoadPersistentAsync` | 加载到 Persistent Loader,不随场景卸载 |
|
||||
| Timeline 等大资源 | `LoadUncachedAsync` + `EarlyRelease` | 播放完立即释放 |
|
||||
| 配置数据 | `PreloadByLabel` 批量预加载 | 在 Loading 界面期间完成,预热 Bundle 缓存 |
|
||||
| 禁止 | `Resources.Load` / 直接调用 `Addressables` | 统一走 ResourceSystem |
|
||||
|
||||
---
|
||||
|
||||
## 1. 基本原则
|
||||
|
||||
### 1.1 统一入口
|
||||
|
||||
所有资源加载通过 `ResourceSystem` 静态门面,禁止直接使用 `Resources.Load` 或 `Addressables` API。
|
||||
|
||||
**例外:场景加载/卸载**。`SceneLoader` 内部直接调用 `Addressables.LoadSceneAsync` / `UnloadSceneAsync`,因为场景操作有独立的生命周期语义(返回 `SceneInstance`),不适合纳入通用资源缓存体系。除此之外的资源加载均应走 `ResourceSystem`。
|
||||
|
||||
旧的 `ResourceKit` 仅在过渡期保留,新代码不应使用。
|
||||
|
||||
### 1.2 异步优先
|
||||
|
||||
运行时资源加载使用异步方式(协程 yield return 或回调),避免主线程阻塞。
|
||||
|
||||
### 1.3 场景作用域管理
|
||||
|
||||
资源 Handle 由 `SceneResourceLoader` 统一持有,场景卸载时自动批量释放。开发者无需手动管理大多数资源的生命周期。
|
||||
|
||||
### 1.4 两级 Loader
|
||||
|
||||
- **Persistent Loader**:挂在 Persistence 场景,管理全局常驻资源(Actor prefab、通用 UI 等),游戏退出时释放
|
||||
- **Scene Loader**:每个业务场景各一个,场景卸载前由 `SceneLoader` 主动释放
|
||||
|
||||
### 1.5 大资源提前释放
|
||||
|
||||
Timeline 等大资源应使用 `LoadUncachedAsync` 加载,播放完后通过 `EarlyRelease` 立即释放,不必等到场景卸载。
|
||||
|
||||
---
|
||||
|
||||
## 2. API 使用指南
|
||||
|
||||
### 2.1 三种路由方式
|
||||
|
||||
| 方法 | 目标 Loader | 适用场景 |
|
||||
|-----|-----------|---------|
|
||||
| `ResourceSystem.LoadAsync<T>(key)` | 自动(场景优先,回退持久化) | 大部分业务代码的默认选择 |
|
||||
| `ResourceSystem.LoadPersistentAsync<T>(key)` | 持久化 Loader | Actor prefab、Bubble prefab 等常驻资源 |
|
||||
| `ResourceSystem.LoadSceneAsync<T>(key)` | 场景 Loader | 明确只属于当前场景的资源 |
|
||||
|
||||
### 2.2 注意事项
|
||||
|
||||
**ActiveLoader 回退风险**:`LoadAsync`(自动路由)在 Scene Loader 不存在时会静默回退到 Persistent Loader。这意味着在场景切换间隙(旧场景已卸载、新场景 `SceneResourceLoader.Awake` 尚未执行)调用 `LoadAsync`,资源会被加载到 Persistent Loader 中,**永远不会随场景卸载释放**。避免在此时间窗口内调用 `LoadAsync`;若有明确场景归属,优先使用 `LoadSceneAsync` 或 `LoadPersistentAsync`。
|
||||
|
||||
**同一 Key 只能以一种类型加载**:`SceneResourceLoader` 的缓存以 Key 字符串为索引。如果先调用 `LoadAsync<Sprite>(key)` 再调用 `LoadAsync<Texture2D>(key)`,第二次会返回已缓存的 Handle 并尝试类型转换,可能导致 `InvalidCastException`。确保同一 Key 在整个场景生命周期内只以一种类型加载。
|
||||
|
||||
### 2.3 协程风格(推荐)
|
||||
|
||||
```csharp
|
||||
IEnumerator Example()
|
||||
{
|
||||
var handle = ResourceSystem.LoadAsync<Sprite>("Memory/mem_001");
|
||||
yield return handle;
|
||||
if (handle.Status == AsyncOperationStatus.Succeeded)
|
||||
_image.sprite = handle.Result;
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 回调风格
|
||||
|
||||
```csharp
|
||||
ResourceSystem.LoadAsync<Sprite>("Memory/mem_001",
|
||||
sprite => _image.sprite = sprite,
|
||||
error => Debug.LogError(error));
|
||||
```
|
||||
|
||||
### 2.5 Timeline 等大资源
|
||||
|
||||
```csharp
|
||||
// 加载(不走缓存,获得独立 Handle)
|
||||
var handle = ResourceSystem.CurrentSceneLoader.LoadUncachedAsync<PlayableAsset>(timelineKey);
|
||||
yield return handle;
|
||||
director.playableAsset = handle.Result;
|
||||
|
||||
// 播放完后立即释放
|
||||
ResourceSystem.EarlyRelease(handle);
|
||||
```
|
||||
|
||||
### 2.6 批量预加载
|
||||
|
||||
```csharp
|
||||
var handle = ResourceSystem.PreloadByLabel<Sprite>("scene_clinic");
|
||||
yield return handle;
|
||||
// PreloadByLabel 会预热 Addressables 底层 Bundle 缓存(Bundle 已加载到内存),
|
||||
// 后续 LoadAsync 同 Bundle 内的资源时无需再等 IO,但仍会经过一次 Addressables
|
||||
// 内部解析。注意:预加载的结果不会写入 SceneResourceLoader 的 _cache 字典,
|
||||
// 后续 LoadAsync 会创建新的 Handle 并缓存。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. SceneResourceLoader 配置
|
||||
|
||||
在场景中挂载 `SceneResourceLoader` 组件:
|
||||
|
||||
- **Persistence 场景**:勾选 `isPersistent`,在 Awake 中自动注册为 Persistent Loader
|
||||
- **业务场景**:不勾选 `isPersistent`,在 Awake 中自动注册为 Scene Loader,OnDestroy 时自动注销
|
||||
|
||||
`SceneLoader` 会在场景卸载前主动调用 `ReleaseAll()`,确保在 `UnloadSceneAsync` 之前完成资源释放,避免 OnDestroy 执行顺序不确定的问题。
|
||||
|
||||
**ReleaseAll 幂等性**:`SceneLoader` 先显式调用 `ReleaseAll()`,随后场景卸载时 `SceneResourceLoader.OnDestroy()` 会再次调用。这是安全的——第二次调用时 `_cache` 和 `_uncachedHandles` 已被清空,不会重复释放。这一设计是有意为之:显式调用保证时序正确,OnDestroy 兜底保证即使漏调也不泄漏。
|
||||
|
||||
---
|
||||
|
||||
## 4. Addressable 命名与分类规范
|
||||
|
||||
> **核心思路**
|
||||
>
|
||||
> - **Group** 对齐加载生命周期 — 同一场景/同一时机加载的资源放进同一个 Group(= 同一个 Bundle)。
|
||||
> - **Key** 保证可预测可构造 — 固定 `Category/Path` 格式,代码中可拼接得出。
|
||||
> - **Label** 仅用于跨 Group 聚合 — 当预加载批次横跨多个 Group 时才创建 Label;若某场景的动态资源全在一个 Group 内,不需要额外 Label。
|
||||
|
||||
---
|
||||
|
||||
### 4.1 Addressable Group
|
||||
|
||||
#### 命名格式
|
||||
|
||||
`{分类}_{PascalCase标识}`
|
||||
|
||||
#### 分类体系
|
||||
|
||||
| 分类 | 命名格式 | 说明 |
|
||||
|------|----------|------|
|
||||
| 核心常驻 | `Core` | 游戏全程常驻的资源,对应 Persistent Loader |
|
||||
| 场景专属 | `Scene_{场景名}` | 某场景独占的资源,场景卸载时释放 |
|
||||
| 功能模块 | `Feature_{模块名}` | 可在多个场景复用的游戏功能模块资源 |
|
||||
| 共享资源 | `Shared_{类别名}` | 多场景共用、按需加载的资源 |
|
||||
| 本地化 | `Localization-*` | 由 Localization 包自动管理,不手动修改 |
|
||||
| 内建 | `Built In Data` | Unity 系统管理,不手动修改 |
|
||||
|
||||
#### 分组原则
|
||||
|
||||
1. **按加载时机而非资源类型分组**:同一场景的动画、精灵、Timeline 放在同一个 Group,而非按类型拆分到不同 Group。这样一次 IO 即可加载该场景所需的全部资源。
|
||||
2. **从不同时加载的资源必须分开**:例如火山场景、酒吧场景、梦境场景不要放进同一个 Group,否则进入火山时会加载酒吧和梦境的无用资源。
|
||||
3. **可复用的玩法逻辑资源独立为 Feature 组**:如 `BlockPuzzle/BlockShape` Prefab 可能在多个维修场景复用,放入 `Feature_BlockPuzzle`;但该模块在石头维修场景中使用的缩略图、部件图等美术资源,放入 `Scene_StoneFix`。
|
||||
|
||||
#### 当前项目分组方案
|
||||
|
||||
| Group | 包含内容 | 依据 |
|
||||
|-------|----------|------|
|
||||
| `Core` | ActorAnima/ActorAnimaEx 等常驻 Prefab、BubbleSprite、SceneSO、PipelinePrefab | 在 Persistence 场景加载后常驻,全程不释放 |
|
||||
| `Scene_ClinicOut` | ClinicOut 场景、诊所外角色动画、Mobile 对象 | 主场景,进入频率最高,资源量适中 |
|
||||
| `Scene_SubWay` | SubWay/Bridge 场景及窗外背景 | 地铁+天桥紧密关联,总是一起出现 |
|
||||
| `Scene_PeipeiFix` | PeipeiFixScene、OpenFixScene、佩佩动画/精灵 | 佩佩维修线专属 |
|
||||
| `Scene_StoneFix` | ExpressFixScene、石头动画、BlockPuzzle 美术资源(缩略图/部件图)、石头线 Timeline | 石头维修线专属;BP 美术只在此场景使用 |
|
||||
| `Scene_HuoShanFix` | HuoShanFixScene 及火山专属资源 | 火山维修线专属 |
|
||||
| `Scene_Bar` | 酒吧场景及专属角色动画 | 酒吧场景专属 |
|
||||
| `Scene_Dream` | 梦境场景及专属资源 | 梦境场景专属 |
|
||||
| `Shared_Actor` | 在多个场景出现的角色动画(如医生在诊所外/酒吧/地铁均出现) | 不属于单一场景,按需加载 |
|
||||
| `Shared_CG` | CG/插图(由 Yarn 对话触发,可能跨场景使用) | 体积较大,独立管理 |
|
||||
| `Feature_BlockPuzzle` | BlockPuzzle/BlockShape Prefab、BlockPuzzleValidator | 纯玩法逻辑资源,可跨场景复用 |
|
||||
| `Feature_WhackMole` | WhackMoleData | 打地鼠关卡数据 |
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Addressable Key
|
||||
|
||||
#### 格式
|
||||
|
||||
```
|
||||
{Category}/{Path}
|
||||
```
|
||||
|
||||
- **Category**:资源类型前缀,PascalCase
|
||||
- **Path**:资源路径,PascalCase,用 `/` 分隔层级,不超过 3 级
|
||||
|
||||
**例外 — 场景 Key 使用裸名**:场景通过 `Addressables.LoadSceneAsync` 加载,Key 直接使用 PascalCase 场景名,不加类型前缀。原因:场景名被 `TalkSceneSO.firstSceneName`、Yarn `<<load_scene>>` 命令、`GameManager` 硬编码等深度引用,保持裸名是最务实的选择。
|
||||
|
||||
#### Category 列表
|
||||
|
||||
| Category | 资源类型 | Key 示例 | 代码构造方式 |
|
||||
|----------|----------|----------|-------------|
|
||||
| `Animation` | AnimatorController / OverrideController | `Animation/Peipei` | `$"Animation/{actorName}"` |
|
||||
| `AnimationConfig` | 动画配置 ScriptableObject | `AnimationConfig/Peipei` | `$"AnimationConfig/{actorName}"` |
|
||||
| `Sprite` | 精灵图(含子对象) | `Sprite/Peipei[Normal]` | `$"Sprite/{actorName}[{state}]"` |
|
||||
| `CG` | CG / 插图 | `CG/Opening1` | `$"CG/{imagePath}"` |
|
||||
| `Timeline` | PlayableAsset | `Timeline/BlockPuzzle/PanelOpen` | `$"Timeline/{module}/{action}"` |
|
||||
| `BubbleSprite` | 对话气泡精灵 | `BubbleSprite/Normal` | `$"BubbleSprite/{name}"` |
|
||||
| `Prefab` | 预制体 | `Prefab/ActorAnima` | `$"Prefab/{name}"` |
|
||||
| `BlockPuzzle` | 拼图专属资源 | `BlockPuzzle/BlockShape` | `$"BlockPuzzle/{path}"` |
|
||||
| `WhackMole` | 打地鼠数据 | `WhackMole/Level1` | `$"WhackMole/{levelName}"` |
|
||||
| `Mobile` | 移动物体 | `Mobile/Car` | `$"Mobile/{name}"` |
|
||||
| `Config` | 配置数据 / SO | `Config/SceneSO` | 直接引用 |
|
||||
|
||||
#### 命名规则
|
||||
|
||||
| 规则 | 正确 | 错误 | 依据 |
|
||||
|------|------|------|------|
|
||||
| Key 允许中英文混合 | `Animation/Peipei`、`Animation/石头` | — | 团队习惯优先,UTF-8 编码无技术障碍 |
|
||||
| 角色名用英文代号 | `Animation/ClinicOutPeipei` | `Animation/诊所外佩佩` | 统一使用项目角色代号 |
|
||||
| 不含空格 | `Prefab/TaskItem` | `Task Item Temp` | 空格在 URL、命令行中易出错 |
|
||||
| 不含文件扩展名 | `WhackMole/Level1` | `WhackMoleData/Level1.asset` | Key 是逻辑地址,不是文件路径 |
|
||||
| 层级不超过 3 级 | `Timeline/BlockPuzzle/PanelOpen` | `Timeline/Fix/Stone/BP/PanelOpen` | 层级过深增加拼接复杂度 |
|
||||
| 子对象用 `[]` 语法 | `Sprite/Peipei[Normal]` | `Sprite/Peipei/Normal` | Addressables 原生子对象语法 |
|
||||
| 场景 Key 裸名 PascalCase | `ClinicOut` | `Scene/ClinicOut` | 兼容 TalkSceneSO 和 LoadSceneAsync |
|
||||
|
||||
#### 角色命名参考
|
||||
|
||||
中英文均可,建议同一类别保持一致:
|
||||
|
||||
| 中文 | 英文 |
|
||||
|------|------|
|
||||
| 佩佩 | Peipei |
|
||||
| 石头 | Stone |
|
||||
| 火山 | HuoShan |
|
||||
| 医生 | Doctor |
|
||||
| 英理 | Yingli |
|
||||
| 调酒妹 | Bartender |
|
||||
|
||||
**命名建议**:若某类别已存在中文命名(如 `Animation/石头`),新增资源保持中文风格(`Animation/医生`);若已存在英文命名,保持英文风格。不要在同一类别中混用。
|
||||
|
||||
#### Timeline Key 约定
|
||||
|
||||
`DirectorHandler` 对 Timeline Key 会自动添加 `Timeline/` 前缀。传入的 ref 格式为 `{Module}/{Action}`,实际 Addressable Key 为 `Timeline/{Module}/{Action}`。
|
||||
|
||||
| Module | 中文对应 | Key 示例 |
|
||||
|--------|----------|----------|
|
||||
| `Snap` | 卡扣 | `Timeline/Snap/Lock`, `Timeline/Snap/Unlock` |
|
||||
| `BlockPuzzle` | 引擎仓面板 | `Timeline/BlockPuzzle/PanelOpen` |
|
||||
| `Cable` | 插线面板 | `Timeline/Cable/PanelOpen` |
|
||||
| `Engine` | 引擎模块 | `Timeline/Engine/Open` |
|
||||
| `Clinic` | 维修间 | `Timeline/Clinic/Enter` |
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Addressable Label
|
||||
|
||||
#### 设计原则
|
||||
|
||||
- **Group** 决定打包粒度(哪些资源进同一个 Bundle)。
|
||||
- **Label** 决定预加载批次(哪些资源在同一时机一起预热)。
|
||||
- **Label 的价值在于横切 Group 边界**:把来自不同 Bundle、但需要在同一时刻预热的资源聚合起来。
|
||||
- 若某场景的全部动态资源恰好都在同一个 Group 内,**不需要**为它创建 Label。
|
||||
|
||||
#### Label 格式
|
||||
|
||||
全小写,单词间用连字符连接。仅在确实需要跨 Group 聚合时才创建。
|
||||
|
||||
#### 当前项目 Label
|
||||
|
||||
| Label | 横切的 Group | 用途 | 预加载时机 |
|
||||
|-------|-------------|------|-----------|
|
||||
| `persistent` | `Core` + `Shared_Actor`(常驻角色) | 启动时预加载所有常驻资源 | 游戏启动 Loading |
|
||||
| `fix-common` | `Core`(维修通用 Timeline)+ 各 `Scene_*Fix` 共用的维修 UI | 进入任意维修场景前预加载通用维修资源 | 维修场景 Loading |
|
||||
| `preload:stone-fix` | `Scene_StoneFix` + `Feature_BlockPuzzle` | 石头维修的动态资源跨两个 Group | 进入 ExpressFixScene 前 |
|
||||
| `preload:huoshan-fix` | `Scene_HuoShanFix` + `Feature_WhackMole`(如有) | 火山维修的动态资源可能跨 Group | 进入 HuoShanFixScene 前 |
|
||||
| `SceneSO` | `Core`(保持兼容) | `GameManager` 启动时用 `LoadAssetsAsync("SceneSO")` 加载所有章节配置 | 游戏启动 |
|
||||
|
||||
#### 什么时候不需要 Label
|
||||
|
||||
| 场景 | 原因 |
|
||||
|------|------|
|
||||
| ClinicOut | 动态资源全在 `Scene_ClinicOut` 一个 Group 内 |
|
||||
| SubWay / Bridge | 动态资源全在 `Scene_SubWay` 内 |
|
||||
| Dream | 动态资源全在 `Scene_Dream` 内 |
|
||||
| Bar | 动态资源全在 `Scene_Bar` 内 |
|
||||
|
||||
这些场景若需要预加载,直接在 `SceneLoader.PreloadSceneAssets` 中按 Key 列表逐个加载即可,无需 Label。
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Key / Label / Group 对照表
|
||||
|
||||
| 用途 | Key | Group | Label(仅跨 Group 时) |
|
||||
|------|-----|-------|----------------------|
|
||||
| 常驻 Actor Prefab | `Prefab/ActorAnima`(旧:`ActorAnima`) | `Core` | `persistent` |
|
||||
| 对话气泡精灵 | `BubbleSprite/Normal`(旧:`BubbleSprite/大对话框`) | `Core` | `persistent` |
|
||||
| 章节配置 | `Config/SceneSO`(旧:`SceneSO`) | `Core` | `SceneSO`(兼容) |
|
||||
| 诊所外场景 | `ClinicOut` | `Scene_ClinicOut` | — |
|
||||
| 诊所外角色动画 | `Animation/ClinicOutPeipei` | `Scene_ClinicOut` | — |
|
||||
| 地铁场景 | `SubWay` | `Scene_SubWay` | — |
|
||||
| 佩佩维修场景 | `PeipeiFixScene` | `Scene_PeipeiFix` | — |
|
||||
| 佩佩动画 | `Animation/Peipei` | `Scene_PeipeiFix` | — |
|
||||
| 石头维修场景 | `ExpressFixScene` | `Scene_StoneFix` | `preload:stone-fix` |
|
||||
| 石头动画 | `Animation/Stone` | `Scene_StoneFix` | `preload:stone-fix` |
|
||||
| 石头线 Timeline | `Timeline/BlockPuzzle/PanelOpen` | `Scene_StoneFix` | `preload:stone-fix` |
|
||||
| BP Prefab | `BlockPuzzle/BlockShape` | `Feature_BlockPuzzle` | `preload:stone-fix` |
|
||||
| BP Validator | `BlockPuzzle/Validator` | `Feature_BlockPuzzle` | `preload:stone-fix` |
|
||||
| 打地鼠数据 | `WhackMole/Level1` | `Feature_WhackMole` | `preload:huoshan-fix`(如有) |
|
||||
| 跨场景角色(医生) | `Animation/BarDoctor` | `Shared_Actor` | `persistent` 或按需 |
|
||||
| CG 插图 | `CG/Opening1` | `Shared_CG` | — |
|
||||
|
||||
---
|
||||
|
||||
### 4.5 历史资源兼容策略
|
||||
|
||||
#### 现状问题
|
||||
|
||||
- **裸 Key**:`ActorAnima`、`NormalBubble`、`ClinicOut`、`BlockPuzzleValidtor` 等
|
||||
- **含空格**:`Task Item Temp`、`Screen Text`(需修正)
|
||||
- **含扩展名**:`WhackMoleData/xxx.asset`(需修正)
|
||||
- **拼写错误**:`ScriptalObjects`、`BlockPuzzleValidtor`(需修正)
|
||||
|
||||
#### 迁移优先级
|
||||
|
||||
| 优先级 | 动作 | 涉及范围 | 依据 |
|
||||
|--------|------|----------|------|
|
||||
| **P0 立即** | 为资源打 Label,重组 Group | 仅改 Addressable 配置,零代码改动 | 立即启用预加载能力 |
|
||||
| **P1 短期** | 新增资源严格按规范命名 Key | 新资源 | 防止问题扩大 |
|
||||
| **P2 中期** | 迁移 ConstRef 中的裸 Key(添加 `Prefab/` 前缀) | ~15 个常量 + 引用处 | 改动集中在 ConstRef.cs |
|
||||
| **无需迁移** | Timeline、Animation 的中文 Key | — | 规范已允许中文命名 |
|
||||
| **无需迁移** | 场景裸 Key(`ClinicOut`、`SubWay` 等) | — | 裸名本身符合场景 Key 规范 |
|
||||
|
||||
#### 迁移步骤
|
||||
|
||||
1. 重组 Group:将现有组重命名/合并为 `Core`、`Scene_*`、`Feature_*`、`Shared_*`。
|
||||
2. 打 Label:为需要跨 Group 预加载的资源打上 `persistent`、`fix-common`、`preload:*` 等 Label。
|
||||
3. 新资源强制新规:所有新增资源必须使用 `Category/Path` 格式,禁止裸 Key。
|
||||
4. 代码全面切到 `ResourceSystem` 后,再评估旧 Key 重命名。
|
||||
|
||||
### 4.6 中文 Key 使用建议
|
||||
|
||||
1. **同一类别保持一致**:若 `Animation/石头` 已存在,新增动画也用中文(`Animation/医生`),不要混用 `Animation/Doctor`
|
||||
2. **避免特殊字符**:仅限中英文、数字、下划线、斜杠、方括号。避免空格、`!@#$%^&*()` 等符号
|
||||
3. **代码中引用**:若某资源频繁在代码中引用,建议在 `ConstRef` 中定义英文常量,如 `ConstRef.TimelineSnapLock = "Timeline/卡扣锁定"`
|
||||
4. **Group 命名保持英文**:Bundle 文件名使用英文 Group 名,避免在某些文件系统/CI 环境出现编码问题
|
||||
|
||||
#### 示例与反例
|
||||
|
||||
**符合规范:**
|
||||
|
||||
- `Animation/Peipei` — 类型前缀 + 英文角色名
|
||||
- `Timeline/BlockPuzzle/PanelOpen` — 不超过 3 级
|
||||
- `Sprite/Peipei[Normal]` — 子对象用 `[]` 语法
|
||||
- `ClinicOut` — 场景裸名,符合场景 Key 例外规则
|
||||
- `WhackMole/Level1` — 不含扩展名
|
||||
|
||||
**反例:**
|
||||
|
||||
- `BlockShape` — 裸 Key,缺类型前缀
|
||||
- `Task Item Temp` — 含空格(应改为 `TaskItemTemp`)
|
||||
- `WhackMoleData/Level1.asset` — 不应包含扩展名(应改为 `WhackMole/Level1`)
|
||||
- `Scene/ClinicOut` — 场景 Key 不需要前缀(直接用 `ClinicOut`)
|
||||
|
||||
---
|
||||
|
||||
## 5. 性能优化建议
|
||||
|
||||
| 问题 | 后果 | 解决方案 |
|
||||
|-----|------|---------|
|
||||
| 同步加载大资源 | 卡顿 | 使用 `ResourceSystem.LoadAsync` |
|
||||
| 重复加载相同资源 | 内存浪费 | `SceneResourceLoader` 自动缓存同 Key |
|
||||
| 不释放 Handle | 内存泄漏 | 场景卸载时 Loader 自动批量释放 |
|
||||
| 大资源持有到场景结束 | 内存浪费 | 用 `LoadUncachedAsync` + `EarlyRelease` |
|
||||
| 混用多种加载方式 | 管理混乱 | 统一使用 `ResourceSystem` |
|
||||
|
||||
---
|
||||
|
||||
## 6. 错误处理与降级策略
|
||||
|
||||
### 6.1 协程风格的错误处理
|
||||
|
||||
```csharp
|
||||
IEnumerator LoadWithFallback()
|
||||
{
|
||||
var handle = ResourceSystem.LoadAsync<Sprite>("Sprite/Peipei[Normal]");
|
||||
yield return handle;
|
||||
|
||||
if (handle.Status == AsyncOperationStatus.Succeeded)
|
||||
{
|
||||
_image.sprite = handle.Result;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[MySystem] 加载失败: {handle.OperationException}");
|
||||
_image.sprite = _fallbackSprite; // 使用预设的占位资源
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 处理原则
|
||||
|
||||
| 情况 | 策略 |
|
||||
|-----|------|
|
||||
| UI 图片加载失败 | 显示占位图/透明,不影响交互流程 |
|
||||
| Prefab 加载失败 | 记录错误日志,跳过实例化,避免 NullReferenceException |
|
||||
| Timeline 加载失败 | 跳过播放,直接推进到下一个流程节点 |
|
||||
| 配置数据加载失败 | 视为致命错误,中断当前流程并提示 |
|
||||
| 批量预加载部分失败 | 记录警告,允许后续按需加载时重试(Addressables 会重新发起请求) |
|
||||
|
||||
### 6.3 避免静默失败
|
||||
|
||||
`SceneResourceLoader` 内部已在加载失败时输出 `LogError`。业务代码不应忽略 `handle.Status`,至少要在失败时 log 上下文信息(哪个系统、为什么需要这个资源),方便排查。
|
||||
|
||||
---
|
||||
|
||||
## 7. Prefab 加载:LoadAsync vs InstantiateAsync
|
||||
|
||||
### 7.1 两种方式对比
|
||||
|
||||
| | `LoadAsync<GameObject>` + `Instantiate` | `Addressables.InstantiateAsync` |
|
||||
|---|---|---|
|
||||
| 加载结果 | 获得 Prefab 引用,需手动 Instantiate | 直接返回实例化的 GameObject |
|
||||
| 引用计数 | 由 SceneResourceLoader 的 Handle 管理 | 由 Addressables 内部管理,需调用 `ReleaseInstance` |
|
||||
| 多次实例化 | 加载一次 Prefab,多次 Instantiate(推荐) | 每次调用产生独立 Handle |
|
||||
| 与本系统集成 | 完全兼容,走 ResourceSystem 统一管理 | 绕过 ResourceSystem,需自行管理释放 |
|
||||
| 适用场景 | 大部分 Prefab(UI 面板、Actor 等) | 不推荐,除非有特殊的引用计数需求 |
|
||||
|
||||
### 7.2 推荐用法
|
||||
|
||||
本项目统一使用 `LoadAsync<GameObject>` + 手动 `Instantiate`,配合 `SceneResourceLoader` 缓存和自动释放:
|
||||
|
||||
```csharp
|
||||
IEnumerator SpawnActor()
|
||||
{
|
||||
var handle = ResourceSystem.LoadPersistentAsync<GameObject>("Prefab/ActorAnima");
|
||||
yield return handle;
|
||||
if (handle.Status == AsyncOperationStatus.Succeeded)
|
||||
{
|
||||
var instance = Object.Instantiate(handle.Result, parent);
|
||||
// Prefab Handle 由 Persistent Loader 管理,无需手动释放
|
||||
// 实例化的 GameObject 需在不用时 Destroy
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
不推荐直接使用 `Addressables.InstantiateAsync`,因为它绕过了 `ResourceSystem` 的统一管理。
|
||||
|
||||
---
|
||||
|
||||
## 8. 调试与内存监控
|
||||
|
||||
### 8.1 运行时查看资源持有量
|
||||
|
||||
`SceneResourceLoader` 提供了以下属性,可在 Inspector 或自定义调试面板中查看:
|
||||
|
||||
| 属性 | 含义 |
|
||||
|-----|------|
|
||||
| `CachedCount` | 当前缓存的资源 Handle 数量(按 Key 去重) |
|
||||
| `UncachedCount` | 未缓存的独立 Handle 数量(LoadUncachedAsync / PreloadByLabel) |
|
||||
| `TotalHandleCount` | 总持有 Handle 数 |
|
||||
|
||||
可在开发阶段写一个简单的 Debug UI 显示这些数值:
|
||||
|
||||
```csharp
|
||||
void OnGUI()
|
||||
{
|
||||
var scene = ResourceSystem.CurrentSceneLoader;
|
||||
var persistent = ResourceSystem.PersistentLoader;
|
||||
GUILayout.Label($"Scene: {scene?.TotalHandleCount ?? 0} handles");
|
||||
GUILayout.Label($"Persistent: {persistent?.TotalHandleCount ?? 0} handles");
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 Addressables Event Viewer
|
||||
|
||||
Unity 编辑器中可通过 **Window > Asset Management > Addressables > Event Viewer** 查看:
|
||||
- 每个 Handle 的加载/释放时间线
|
||||
- 当前活跃的 Handle 数量
|
||||
- Bundle 加载/卸载事件
|
||||
|
||||
需要在 **AddressableAssetSettings** 中启用 **Send Profiler Events** 选项。
|
||||
|
||||
### 8.3 常见泄漏排查
|
||||
|
||||
| 现象 | 可能原因 | 排查方式 |
|
||||
|-----|---------|---------|
|
||||
| 场景切换后内存不降 | 资源被加载到 Persistent Loader(ActiveLoader 回退) | 检查 `PersistentLoader.CachedCount` 是否异常增长 |
|
||||
| 同一场景内存持续增长 | 重复调用 `LoadUncachedAsync` 未释放 | 检查 `UncachedCount` 是否持续增长 |
|
||||
| 退出场景后 Addressables 报 Handle 泄漏警告 | `SceneResourceLoader` 未被正确销毁 | 确认场景中挂载了 `SceneResourceLoader` 且 `SceneLoader` 正确调用了卸载流程 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 代码审查清单
|
||||
|
||||
- [ ] 没有使用 `Resources.Load`
|
||||
- [ ] 没有直接调用 `Addressables` API(应通过 ResourceSystem,场景加载除外)
|
||||
- [ ] 运行时资源加载使用异步方式
|
||||
- [ ] 加载结果检查了 `handle.Status`,失败时有降级处理或日志(§6)
|
||||
- [ ] Timeline 等大资源使用 `LoadUncachedAsync` + `EarlyRelease`
|
||||
- [ ] 常驻资源通过 `LoadPersistentAsync` 加载
|
||||
- [ ] Prefab 使用 `LoadAsync<GameObject>` + `Instantiate`,不使用 `InstantiateAsync`(§7)
|
||||
- [ ] 同一 Key 未以不同类型重复加载(§2.2)
|
||||
- [ ] 新增 Addressable Key 符合 `Category/Path` 命名规范(§4.2)
|
||||
- [ ] 新增资源放入了正确的 Addressable Group(§4.1)
|
||||
- [ ] 跨 Group 预加载的资源已打上对应 Label(§4.3)
|
||||
- [ ] 业务场景中挂载了 `SceneResourceLoader` 组件
|
||||
|
||||
---
|
||||
|
||||
*文档版本: 3.1*
|
||||
*更新日期: 2026-03-11*
|
||||
Reference in New Issue
Block a user