Merge remote changes: integrate MCP tools and resource refactor docs into framework

- Resolve directory rename conflict from old 03/06 dirs
- Move remote-added 资源加载重构方案.md into modules/frameworks/资源管理/
- Move remote-added 06_工具脚本/MCP工具 into modules/frameworks/编辑器工具/MCP工具/
- Keep Tools/CodexUnityMcp/ as MCP Server implementation
- Update README.md, modules/模块索引.md, and parent READMEs
- Add 文档关联 sections to new docs and fix old absolute paths

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lms
2026-07-07 19:07:27 +08:00
12 changed files with 1669 additions and 1 deletions

View File

@@ -0,0 +1,205 @@
# CrystalBattle Codex ↔ Unity MCP 使用规则
## 1. 目标与定位
MCPModel Context Protocol用于打通 Codex 与 Unity Editor。Codex 不直接操作 Unity 进程,而是通过本地 MCP Server 调用 Unity Editor 内的 HTTP Bridge再由 Bridge 在 Unity 主线程执行 Editor 命令。
当前首版定位:
- 只支持 Unity Editor 工具桥,不覆盖 Play Mode 或真机运行时调试。
- 只监听本机 `127.0.0.1`,不开放局域网访问。
- 优先服务 UI 生成、Prefab 导出、资源刷新、日志读取等 Editor 自动化流程。
- Proxima 暂不作为 MCP 主通道,后续如做运行时调试再单独规划。
## 2. 工程结构
本文档只说明 Codex 如何对接 Unity MCP不说明 Unity Editor 内部脚本目录结构,也不在本节维护数据目录。
Codex 需要知道的对接点:
```text
Unity MCP Server: UnityAI/Tools/CodexUnityMcp/
Codex config: .codex/config.toml
Unity Bridge URL: http://127.0.0.1:17777
```
## 3. 启动与连接
1. 确认 `.codex/config.toml` 中存在 `crystalbattle-unity` MCP server 配置。
2. 确认 MCP Server 入口指向 `UnityAI/Tools/CodexUnityMcp/src/server.mjs`
3. Codex 侧重启或新开线程后,会重新读取 MCP server 配置。
4. 每轮 Unity 自动化前,优先调用 `unity_health` 检查连通性。
5. 如果 `unity_health` 不通,只记录错误并停止后续 Unity 操作Unity 侧如何打开或重启 Bridge 不在本文档说明范围内。
禁止事项:
- 不允许把 Bridge 地址改成 `0.0.0.0` 或局域网 IP。
- 不允许绕过 MCP 直接让 Codex 执行 Unity Editor 侧业务逻辑。
- 不允许在 Unity 后台线程直接调用 Unity API所有 Unity API 必须回到 Editor 主线程执行。
## 4. Codex 可用工具
| 工具名 | 用途 | 规则 |
|---|---|---|
| `unity_health` | 检查 Unity Bridge 是否在线 | 每轮 Unity 自动化前优先调用 |
| `unity_refresh_assets` | 执行 `AssetDatabase.Refresh()` | 生成或导出资源后可调用 |
| `unity_execute_menu_item` | 执行白名单菜单项 | 只能执行 Bridge 内白名单 |
| `unity_get_console_logs` | 读取最近 Unity Console 日志 | 生成失败或验证结果时必须查看 |
| `unity_generate_ui_prefab` | 根据 JSON 生成 UI Prefab | 输入 JSON 必须使用 Unity 工程相对路径或绝对路径 |
| `unity_export_prefab_to_json` | 从 UI Prefab 导出 JSON | Prefab 路径必须是 Unity Asset 路径 |
## 5. MCP 数据目录规则
所有 MCP 数据必须按功能单独放在 Unity Editor 目录下,并直接按目标预制体名分文件夹;不再增加中间层目录。统一格式:
```text
Assets/Editor/MCP_{功能名}/<PrefabName>/
```
命名要求:
- `{功能名}` 只表示功能类别,不写具体窗口名、临时任务名或个人名。
- `<PrefabName>` 必须沿用目标预制体名称,和最终生成的 `<PrefabName>.prefab` 保持一致。
- UI 相关数据统一放在 `Assets/Editor/MCP_UI/<PrefabName>/`
- 其它功能按能力命名,例如 `Assets/Editor/MCP_Config/<PrefabName>/``Assets/Editor/MCP_Map/<PrefabName>/`
- 新增、导出、临时验证用 JSON 都必须遵守该目录规则。
整理旧数据时,应迁移到 `Assets/Editor/MCP_UI/<PrefabName>/` 或对应功能目录。
## 6. UI 生成规则
标准流程:
1. 先读取 `UnityAI/README.md` 中的 UI 相关索引。
2. 根据 README 的 UI 相关入口,优先阅读:
- `UnityAI/modules/frameworks/UI窗口系统/开发指南.md`
- `UnityAI/modules/frameworks/UI窗口系统/界面拼接.md`
3. 需要使用 JSON 与 Prefab 双向转换时,再阅读 README 中的 `编辑器工具脚本` 入口:
- `UnityAI/modules/frameworks/编辑器工具/UI工具/`
4. 按 UI 指南确认窗口命名、层级、资源路径和布局规则后,准备 UI JSON。
5. 调用 `unity_generate_ui_prefab`
6. 目标 Prefab 路径优先使用:
```text
Assets/Game/GameHFResource/CN/UIWindows/<WindowName>/<WindowName>.prefab
```
7. 调用 `unity_get_console_logs` 查看是否有 Error 或关键 Warning。
8. 如需要验证双向转换,再调用 `unity_export_prefab_to_json`
JSON 约束:
- 根对象使用 `UILayoutData` 结构:`name/type/layer/rectTransform/canvas/children`
- UI 节点使用 `UIElementData` 结构:`name/type/rectTransform/image/button/text/.../children`
- 资源路径使用 Unity Asset 路径,例如 `Assets/...`
- 测试 UI 命名必须带明确测试标识,例如 `CodexMcpTestWindow`
当前注意事项:
- `UnityEngine.JsonUtility` 解析递归 `children` 结构时可能出现 serialization depth warning。
- 如果 Prefab 已成功生成且没有 Error该 warning 暂不阻塞测试。
- 后续如大量使用 UI JSON应考虑把解析器替换为更适合递归结构的 JSON 库。
## 7. 安全与权限
MCP 工具可以触发 Unity Editor 行为,因此必须保持最小权限:
- `execute_menu_item` 必须使用白名单,禁止开放任意菜单执行。
- 只允许监听 `127.0.0.1`
- 不在 MCP Server 中直接读写 Unity 资源,资源变更统一交给 Unity Bridge 执行。
- 生成测试资源时使用独立测试目录或明确测试命名,避免覆盖正式 UI。
- 覆盖已有 Prefab 前必须确认目标路径是否为预期文件。
- 不通过 MCP 执行批量删除、批量移动、构建发布、Git 操作等高风险动作。
## 8. 排错流程
连接失败:
1. 调用 `unity_health` 获取错误信息。
2. 用浏览器或 PowerShell 检查:
```powershell
Invoke-WebRequest -UseBasicParsing "http://127.0.0.1:17777/health"
```
3. 如果连接仍失败,停止后续 Unity 操作并记录错误Unity 侧启动、重启、面板操作不在本文档说明范围内。
MCP 工具不可见:
1. 确认 `UnityAI/Tools/CodexUnityMcp/node_modules` 已安装。
2. 确认 `.codex/config.toml``command``args` 路径有效。
3. 重启 Codex让 MCP server 配置重新加载。
UI 生成失败:
1. 调用 `unity_get_console_logs` 查看 Unity Console。
2. 检查 JSON 文件是否存在。
3. 检查 JSON 是否位于 `Assets/Editor/MCP_UI/<PrefabName>/`
4. 检查 `rectTransform` 数组字段是否齐全。
5. 检查 Sprite、Font、Material 等资源路径是否存在。
6. 确认 Prefab 目标目录可写且未被外部程序锁定。
## 9. 测试基准
每次修改 MCP 或 Unity Bridge 后,至少验证:
- `unity_health` 返回 `ok=true`
- `unity_get_console_logs` 能返回日志。
- `unity_generate_ui_prefab` 能生成一个最小测试 Prefab。
- 生成的 Prefab 文件存在,并包含预期节点名。
- `unity_export_prefab_to_json` 能把测试 Prefab 导出 JSON。
- Unity Console 无新增 Error。
UI 测试样例推荐路径:
```text
Assets/Editor/MCP_UI/CodexMcpTestWindow/CodexMcpTestWindow.json
Assets/Game/GameHFResource/CN/UIWindows/CodexMcpTestWindow/CodexMcpTestWindow.prefab
Assets/Editor/MCP_UI/CodexMcpTestWindow/CodexMcpTestWindow.exported.json
```
## 10. 维护规则
新增 MCP tool 时必须同步更新:
- `UnityAI/Tools/CodexUnityMcp/src/server.mjs`
- Unity Bridge 侧对应命令代码
- 本文档的工具列表与测试基准
新增 Unity 命令时必须满足:
- HTTP 请求只负责收包和回包。
- Unity API 在 `EditorApplication.update` 主线程队列执行。
- 返回值使用统一结构:
```json
{
"ok": true,
"message": "done",
"data": {},
"logs": []
}
```
文档版本:
- v1.4
- 生效日期2026-07-03
- 适用项目CrystalBattle_Client
---
## 文档关联
> 当本文档发生变更时,应同步检查以下关联文档,避免信息不一致。
| 文档 | 关联原因 |
|---|---|
| [MCP 工具入口](README.md) | 本文件是 MCP 工具模块的子文档 |
| [UI 开发指南](../../UI窗口系统/开发指南.md) | UI 生成规则参考 |
| [UI 界面拼接设计指南](../../UI窗口系统/界面拼接.md) | UI 布局规则参考 |
| [UI 拼接工具](../UI拼接工具.md) | JSON/Prefab 双向转换工具说明 |
| [Tools/CodexUnityMcp](../../../../Tools/CodexUnityMcp/README.md) | MCP Server 实现 |
| [modules/模块索引.md](../../../模块索引.md) | 模块索引是项目知识总入口 |
| [rules/10-架构说明.md](../../../../rules/10-架构说明.md) | 编辑器工具层的架构说明 |
| [rules/30-模块登记规则.md](../../../../rules/30-模块登记规则.md) | 模块登记规则定义分类和状态口径 |

View File

@@ -0,0 +1,28 @@
# MCP工具
本目录用于沉淀 Codex 与 Unity Editor 打通相关的规则、工具说明和排错流程。
当前文档:
- [CrystalBattle Codex ↔ Unity MCP 使用规则](CrystalBattle_Codex_Unity_MCP使用规则.md)
当前实现位置:
- Unity Editor Bridge`CrystalBattle_Client/Assets/Editor/Command/`
- MCP Server`UnityAI/Tools/CodexUnityMcp/`
- Codex 配置:`.codex/config.toml`
---
## 文档关联
> 当本文档发生变更时,应同步检查以下关联文档,避免信息不一致。
| 文档 | 关联原因 |
|---|---|
| [编辑器工具入口](../README.md) | 本文件是编辑器工具模块的子文档 |
| [CrystalBattle Codex ↔ Unity MCP 使用规则](CrystalBattle_Codex_Unity_MCP使用规则.md) | 具体 MCP 使用规则 |
| [Tools/CodexUnityMcp](../../../../Tools/CodexUnityMcp/README.md) | MCP Server 实现 |
| [modules/模块索引.md](../../../模块索引.md) | 模块索引是项目知识总入口 |
| [rules/10-架构说明.md](../../../../rules/10-架构说明.md) | 编辑器工具层的架构说明 |
| [rules/30-模块登记规则.md](../../../../rules/30-模块登记规则.md) | 模块登记规则定义分类和状态口径 |

View File

@@ -41,6 +41,11 @@
- [通用框架](../通用框架/README.md)
- [资源管理](../资源管理/README.md)
## 子文档
- [UI 拼接工具](UI拼接工具.md)
- [MCP 工具](MCP工具/README.md)
---
## 文档关联
@@ -50,5 +55,7 @@
| 文档 | 关联原因 |
|---|---|
| [modules/模块索引.md](../../模块索引.md) | 模块索引是项目知识总入口 |
| [modules/frameworks/编辑器工具/UI拼接工具.md](UI拼接工具.md) | UI 预制体 JSON 双向转换工具 |
| [modules/frameworks/编辑器工具/MCP工具/README.md](MCP工具/README.md) | Codex ↔ Unity MCP 工具 |
| [rules/10-架构说明.md](../../../rules/10-架构说明.md) | 编辑器工具层的架构说明 |
| [rules/30-模块登记规则.md](../../../rules/30-模块登记规则.md) | 模块登记规则定义分类和状态口径 |

View File

@@ -120,6 +120,11 @@ SpriteAtlasManager.atlasRequested
- [实体系统](../实体系统/README.md)
- [场景系统](../场景系统.md)
## 子文档
- [资源管理使用指南](使用指南.md)
- [资源加载重构方案](资源加载重构方案.md)
---
## 文档关联
@@ -132,5 +137,6 @@ SpriteAtlasManager.atlasRequested
| [modules/frameworks/UI窗口系统/README.md](../UI窗口系统/README.md) | UI 资源加载依赖资源管理 |
| [modules/frameworks/实体系统/README.md](../实体系统/README.md) | 实体资源加载依赖资源管理 |
| [modules/frameworks/场景系统.md](../场景系统.md) | 场景资源加载依赖资源管理 |
| [modules/frameworks/资源管理/资源加载重构方案.md](资源加载重构方案.md) | AssetBundle 加载链路重构方案 |
| [rules/10-架构说明.md](../../../rules/10-架构说明.md) | 架构说明定义总体分层和依赖方向 |
| [rules/30-模块登记规则.md](../../../rules/30-模块登记规则.md) | 模块登记规则定义分类和状态口径 |

View File

@@ -0,0 +1,380 @@
# ESF_ 资源加载重构方案
## 一、目标与边界
本方案用于规划 StrayFog 框架中 AssetBundle 资源加载链路的重构。当前阶段先文档化设计,不直接修改代码。
重构范围只包含 `Assets/StrayFog/Editor/CopyToX/...` 下的 `ESF_` 框架源头代码。`ESF_` 后续会通过 CopyTo 流程同步到 `GameHFScripte` 目录,并替换为业务运行时使用的 `HF_` 命名。因此:
- 框架优化只修改 `ESF_` 源头。
- 不直接修改 CopyTo 后的 `HF_` 副本代码。
- 对外调用接口保持兼容,业务仍通过现有 `HF_` API 使用资源系统。
需要保持兼容的公开入口:
- `ESF_ABMgr.LoadInMemory(...)`
- `ESF_ABMgr.SetModel(...)`
- `ESF_ABMgr.SetDownloadWhilePlaying(...)`
- `ESF_ABMgr.SetDownLoadUri(...)`
- `ESF_ABMgr.RegisterDownloadMainifestFun(...)`
- `ESF_AssetBundlePathAttribute` 现有构造与使用方式
计划新增的公开入口:
```csharp
public bool TryUnloadPrefabInMemory(string _prefabName);
public bool TryUnloadAssetInMemory(string _assetName);
public long GetLoadedAssetBundleMemorySize();
public void SetCdnSaveRootPath(string _saveRootPath);
```
说明:
- `TryUnloadPrefabInMemory(string)` 供业务按预制体名称主动请求卸载,例如 `TryUnloadPrefabInMemory("GameLogicFightWindow")`
- `TryUnloadAssetInMemory(string)` 供业务按资源名称主动请求卸载,适用于非 prefab 资源。
- 业务接口不暴露 `assetBundleKey``ESF_AssetBundlePathAttribute``_unloadAllLoadedObjects`,避免上层误用内部概念。
- 内部卸载统一使用 `AssetBundle.Unload(false)`,只释放 Bundle 文件内存,不强制销毁已经实例化出去的对象。
- 框架必须先判断资源已经没有框架侧活跃引用;如果仍在加载中、正在被窗口/任务使用、处于 ACTIVE 生命周期,或名称匹配不唯一,则拒绝卸载并返回 `false`
- `GetLoadedAssetBundleMemorySize()` 返回当前 ESF_ABMgr 统计到的已加载 AssetBundle 内存大小,单位为 byte用于运行时监控面板或调试日志。
- `SetCdnSaveRootPath(string)` 供外部按平台传入 CDN 热更新文件落盘根目录;如果未传入或传入空值,默认使用 PersistentData 资源根路径。
## 二、当前问题
### 2.1 职责封装需要加强
`ESF_ABMgr` 当前通过 partial 分布类承载 Manifest 读取、资源路径修正、Loader 管理、依赖查询、加载模式控制、GC 回收等职责。这种设计保留了统一入口,方便后续 CopyTo 和上层调用,是框架既有设计方向。
后续优化不应拆散 `ESF_ABMgr` 的统一入口,而是需要继续在 `ESF_ABMgr` 内部强化职责封装:把 Manifest 查询、加载来源选择、下载落盘、GC 回收、主动卸载等流程封装成清晰的内部方法或内部服务,减少方法之间直接互相修改状态。`ESF_AssetLoader` 内部也需要把依赖加载、主包加载、下载等待、进度计算和回调分发拆成更明确的状态步骤。
### 2.2 本地加载内存峰值偏高
PersistentData 等普通本地文件路径当前存在 `File.ReadAllBytes` + `AssetBundle.LoadFromMemoryAsync` 的加载方式。结合 Unity 官方说明和当前框架实现,这个判断成立,但需要精确理解:
- `File.ReadAllBytes(path)` 会先把整个 AssetBundle 文件读入托管 `byte[]`,这一步本身就会产生一份完整文件大小的内存占用。
- `AssetBundle.LoadFromMemoryAsync(byte[])` 是从内存中的 byte[] 创建 AssetBundle适合已经下载、解密或只能拿到 byte[] 的场景。
- `AssetBundle.LoadFromFileAsync(path)` 是从磁盘文件异步加载 AssetBundleUnity 官方说明它支持各种压缩类型,并且未压缩和 Chunk 压缩包可以直接从磁盘读取。
- 当前框架构建 AssetBundle 时使用了 `BuildAssetBundleOptions.ChunkBasedCompression`,因此 PersistentData 下的普通本地文件优先改为 `LoadFromFileAsync` 是合理方向。
需要注意:如果资源包需要先经过 `LoadFileByDecryptFunc` 解密,或者来源是 StreamingAssets URL、CDN 下载数据、小游戏文件系统 byte[],仍然需要保留 `LoadFromMemoryAsync` 路径。
### 2.3 StreamingAssets 存在忙等阻塞
`ESF_AssetBundleHelper.OnReadStreamingAssets` 使用 `UnityWebRequest.SendWebRequest()``while (!async.isDone)` 空循环等待,会阻塞主线程。该问题在移动端和 WebGL/小游戏类环境中风险更高。
### 2.4 下载队列缺少统一并发上限
`ESF_DownLoadGroup``m_MaxTask` 由任务文件数量传入,可能导致一次任务内所有文件同时下载。资源数量较多时容易造成网络拥塞、内存峰值和下载回调竞争。
### 2.5 进度与回收存在边界风险
`ESF_ABMgr.progress` 在没有 active loader 时存在除零风险。GC 回收时销毁 loader GameObject 后,字典与内存统计需要同步清理,否则可能残留失效 loader 或重复统计。
## 三、目标架构
重构后仍保留 `ESF_ABMgr` 作为统一对外入口,并继续允许使用 partial 分布类组织代码;优化重点是把内部职责封装成更明确的方法组或内部服务。
| 模块 | 职责 |
|-----|-----|
| Manifest 管理封装 | 读取 Manifest、维护带 hash 包名映射、处理语言回退、提供依赖查询 |
| Loader 管理封装 | 管理 assetBundleKey 到 loader 的映射、创建/复用/重载 loader |
| Bundle 读取策略封装 | 根据加载模式选择 `LoadFromFileAsync``LoadFromMemoryAsync` 或下载数据加载 |
| 下载队列封装 | 控制下载并发、失败重试、进度回调、下载完成数据回传 |
| GC 缓存策略封装 | 维护 ACTIVE/Normal/LAZY 生命周期,回收时同步移除 loader 映射和内存统计 |
| CDN 落盘路径策略封装 | 管理 CDN 下载后的保存根目录,支持外部传入平台专用路径,默认回退 PersistentData |
### 3.1 资源加载主流程
```
LoadInMemory
Manifest 管理:确认 StreamingAssets / PersistentData / CDN 来源
Manifest 管理:按当前语言和各来源 Manifest 修正 ESF_AssetBundlePathAttribute
Loader 管理:创建或复用 ESF_AssetLoader
ESF_AssetLoader加载依赖包
Bundle 读取策略:加载主 AssetBundle
ESF_AssetResponse按原有回调返回资源
```
### 3.2 本地加载策略
Unity 常用资源路径语义:
| 路径 | Unity 语义 | 常见平台表现 | 框架用途 |
|-----|-----------|-------------|---------|
| `Application.streamingAssetsPath` | 随包只读资源目录,构建时原样拷贝 `Assets/StreamingAssets` 内容 | Editor/Windows/Linux 多数是普通目录macOS、iOS 有各自包内路径Android 为 APK/JAR 内 URLWebGL/Web 平台也是 URL 类路径 | 放母包内置 AssetBundle作为首次启动和无热更文件时的基础资源 |
| `Application.persistentDataPath` | 运行期可写、跨启动保留的数据目录 | iOS/Android 更新 App 后通常保留Windows/macOS/Linux 为用户目录WebGL/Web 为浏览器持久化文件系统tvOS 不支持 | 放热更下载后的 AssetBundle作为 StreamingAssets 缺失后的本地补充资源 |
| `Application.dataPath` | 当前项目或 Player 数据目录 | 本框架只允许在 Editor 宏包裹的代码中使用Editor 下指向项目 `Assets` | 仅用于 Editor 下路径推导和资源分析,不参与 Player 运行期加载策略 |
框架目标读取优先级:
```text
非 CDN 模式:
StreamingAssets Manifest
├─ Manifest 包含目标资源:从 StreamingAssets 加载随包资源
└─ Manifest 不包含目标资源
PersistentData Manifest
├─ Manifest 包含目标资源:从 PersistentData 加载热更资源
└─ Manifest 不包含目标资源
如果配置 CDN 路径,则从 CDN 下载
CDN 模式:
CDN / 小游戏缓存 / 小游戏平台文件系统
└─ 不走 StreamingAssets / PersistentData fallback
```
具体策略:
- 非 CDN 模式不直接按文件是否存在做第一判断,而是先读取 StreamingAssets 下的 Manifest。
- 如果 StreamingAssets Manifest 包含目标资源,则从 `SFGlobalData.streamingAssetsRoot` 加载对应 AssetBundle。
- 如果 StreamingAssets Manifest 不包含目标资源,再读取 PersistentData 下的 Manifest。
- 如果 PersistentData Manifest 包含目标资源,则从 `SFGlobalData.assetBundleRoot` 加载对应 AssetBundle。
- 如果两个本地 Manifest 都不包含目标资源,且配置了 CDN 路径,则从 CDN 下载目标资源;如果没有配置 CDN 路径,则返回明确失败。
- StreamingAssets 是随包母包资源PersistentData 是热更落盘资源;二者通过各自 Manifest 判断是否拥有目标资源。
- PersistentData 命中且是普通文件系统路径时,优先使用 `AssetBundle.LoadFromFileAsync(path)`,避免 `File.ReadAllBytes + LoadFromMemoryAsync` 带来的额外 byte[] 内存峰值。
- Android、WebGL/Web 等 StreamingAssets URL 类路径不能用普通 `File.Exists` / `File.ReadAllBytes` 判断和读取,需要通过 `UnityWebRequest` 异步读取。
- StreamingAssets 读取到 byte[] 后仍使用 `AssetBundle.LoadFromMemoryAsync(data)`
- `Application.dataPath` 只允许出现在 `UNITY_EDITOR` 宏包裹的编辑器逻辑中用于路径推导、AssetDatabase 资源定位或构建分析;运行期加载不能依赖该路径。
- 显式 CDN 模式仍视为独立资源来源:优先走 CDN/小游戏缓存/小游戏文件系统逻辑,不参与 StreamingAssets 和 PersistentData 的本地 fallback。
- 非 CDN 模式下的 CDN 下载只作为本地两个 Manifest 都不包含资源后的最后补充来源,前提是已经配置 CDN 地址。
- CDN 下载返回的数据、小程序缓存读取返回的数据、需要 `LoadFileByDecryptFunc` 解密的数据,仍保持 byte[] 加载路径。
- 后续如果发现部分平台对 `LoadFromFileAsync` 支持不一致,再增加内部策略开关;第一版不暴露为公开 API。
### 3.2.1 CDN 热更新落盘路径
CDN 下载属于热更新链路。下载成功后,需要把资源保存到当前平台可读写的位置,供后续启动或二次加载复用。
落盘根目录优先级:
```text
外部传入 CDN 保存根目录
├─ 有效:使用外部路径
└─ 无效或未设置:使用 SFGlobalData.assetBundleRoot / PersistentData
```
计划新增接口:
```csharp
public void SetCdnSaveRootPath(string _saveRootPath);
```
行为要求:
- 外部可在初始化资源系统时传入平台专用保存根目录。
- 如果 `_saveRootPath``null`、空字符串或非法路径,则不覆盖默认值。
- 未设置外部路径时CDN 下载文件统一保存到 `SFGlobalData.assetBundleRoot`,也就是框架的 PersistentData 资源根路径。
- 保存路径只影响 CDN 下载后的本地落盘位置,不改变 CDN 下载 URL。
- 保存时仍保留 AssetBundle 的相对路径结构,避免不同 bundle 重名覆盖。
- 小游戏平台如果有专用文件系统路径,应通过平台宏或外部初始化代码传入,不在通用逻辑里硬编码平台路径。
- 后续读取 CDN 已下载文件时,优先从该保存根目录查找;未命中再走 CDN 下载。
平台隔离原则:
- 通用 ESF_ 逻辑只维护“外部传入路径优先,否则 PersistentData”的规则。
- 平台差异通过宏定义包裹在平台适配层例如小游戏、WebGL、原生移动端等各自传入保存路径。
- 不在业务层暴露 `assetBundleKey` 或内部 Manifest 路径;业务只配置 CDN 地址和可选保存根目录。
### 3.3 StreamingAssets 异步读取
`OnReadStreamingAssets` 不再提供阻塞式读取给加载主链路使用。主链路应改为异步读取:
- 使用 `UnityWebRequest.Get`
- 通过协程或 loader 状态机等待完成。
- 下载成功后使用 `AssetBundle.LoadFromMemoryAsync(data)`
- 下载失败时进入统一失败处理或 CDN fallback。
### 3.4 下载队列
下载管理器增加内部并发上限,建议默认值为 3。
要求:
- 单个 `ESF_DownLoadGroup` 不再以文件数量作为最大并发数。
- 高优先级任务优先执行,但不能绕过全局并发上限。
- 下载失败保持现有回调语义。
- `WaitDownload` 回调保持兼容。
### 3.5 GC 与 loader 映射
GC 回收和主动卸载共用同一套内部释放逻辑,避免自动回收与业务手动卸载产生两套状态。
GC 自动回收时需要同时处理:
- `AssetBundle.Unload(false)`,默认不销毁已经实例化出去的对象。
- Destroy loader GameObject。
-`mESF_AssetLoaderMaping` 移除对应 key。
- 从生命周期池和内存统计中移除对应 key。
回收后再次加载同一资源时,必须能够重新创建 loader 并正常触发回调。
业务主动卸载接口:
```csharp
public bool TryUnloadPrefabInMemory(string _prefabName);
public bool TryUnloadAssetInMemory(string _assetName);
```
行为要求:
- 根据 `_prefabName``_assetName` 在当前已加载 loader 中查找匹配资源,匹配字段以 `abPathAttr.assetName` 为准。
- 找到唯一 loader且 loader 已完成加载、没有排队请求、没有 active 任务、生命周期不为 ACTIVE 时,执行内部释放流程并返回 `true`
- 未找到 loader、名称匹配多个 loader、loader 未完成加载、资源正在使用、或资源已被释放时,返回 `false`,不抛异常。
- 如果资源正在加载中,第一版不强制中断异步加载,避免破坏现有回调语义。
- 卸载成功后,`mESF_AssetLoaderMaping`、生命周期池、内存统计必须同步移除对应 key。
- 后续再次请求同一资源时,必须走正常创建 loader 和加载流程。
- `AssetBundle.Unload(false)` 不会销毁已经实例化出去的 GameObject但为了避免业务继续依赖原始 asset 对象,业务接口仍必须通过框架引用判断后才允许释放。
内部卸载能力:
```csharp
bool OnUnloadInMemory(int _assetBundleKey);
bool OnCanUnloadInMemory(ESF_AssetLoader _loader);
```
说明:
- 按 key 卸载只作为框架内部实现和调试面板基础能力,不作为普通业务接口。
- 所有卸载入口都必须经过 `OnCanUnloadInMemory` 安全判断。
- 后续如果需要更精确的“完全没有引用”,应引入框架级引用计数或资源句柄释放机制;第一版以 loader 生命周期、请求队列和窗口/任务活跃状态作为安全判断依据。
内存查询接口:
```csharp
public long GetLoadedAssetBundleMemorySize();
```
行为要求:
- 返回当前 AssetBundle 加载器统计到的内存大小,单位为 byte。
- 统计值以 loader 的 `Size` 为准;`LoadFromFileAsync` 无 byte[] 时,使用文件长度作为估算值。
- 每次主动卸载或 GC 回收后同步扣减。
- 只作为运行时查看和调试依据,不作为精确 Profiler 替代。
## 四、分批实施顺序
### 第一批:低风险修复
- 修正 `ESF_ABMgr.progress` 无 loader 时除零问题。
- 新增 `TryUnloadPrefabInMemory(...)``TryUnloadAssetInMemory(...)` 业务主动卸载接口。
- 新增 `GetLoadedAssetBundleMemorySize()` 内存统计查询接口。
- 下载队列增加内部并发上限。
- GC 回收时同步移除失效 loader。
- 保持现有公开接口兼容。
### 第二批:加载策略重构
-`ESF_AssetLoader` 引入更清晰的内部状态。
- 非 CDN 模式读取优先级改为 `StreamingAssets Manifest -> PersistentData Manifest -> CDN fallback`
- Manifest 管理支持同时维护 StreamingAssets 与 PersistentData 两套本地 Manifest 查询结果。
- CDN 下载支持外部传入保存根目录,未设置时默认保存到 PersistentData。
- PersistentData 普通文件命中时优先使用 `AssetBundle.LoadFromFileAsync`
- 保留 StreamingAssets、CDN、小游戏缓存的 byte[] 加载路径。
- 保持 `ESF_AssetResponse.GetAsset<T>()` 行为不变。
### 第三批StreamingAssets 异步化
- 移除主链路中的忙等读取。
- 将 StreamingAssets 读取接入 loader 状态机。
- 统一成功、失败、fallback、进度回调。
### 第四批:内部封装整理
- 保留 `ESF_ABMgr` 统一入口和 partial 分布类组织方式。
- 在不改变公开接口的前提下,将 Manifest、Loader 管理、Bundle 读取策略、下载队列、GC 策略封装成更清晰的内部方法组或内部服务。
- 清理重复路径拼接和重复错误处理。
## 五、验证方案
验证以 Editor 菜单测试为主,建议新增或复用一个内部测试入口,用于覆盖以下场景:
| 场景 | 验证点 |
|-----|-------|
| Editor 模式加载 prefab | `LoadInMemory` 成功回调,`GetAsset<GameObject>()` 正常 |
| PersistentData 本地 AB | 普通文件路径优先走 `LoadFromFileAsync` |
| StreamingAssets 加载 | 不出现主线程忙等卡死 |
| Manifest 缺失 | 返回清晰失败日志,不静默卡住 |
| 资源缺失 | 触发 failed 回调,错误信息包含资源路径 |
| 语言回退 | 当前语言缺资源时回退默认语言 |
| CDN/边玩边下 | 触发 `WaitDownload`,下载完成后继续加载 |
| SpriteAtlas 请求 | `SpriteAtlasManager.atlasRequested` 能正确加载图集 |
| 多资源并发下载 | 并发数不超过内部上限 |
| 非 CDN 读取优先级 | StreamingAssets Manifest 包含目标资源时不读取 PersistentData 资源 |
| PersistentData fallback | StreamingAssets Manifest 不包含目标资源时,再查 PersistentData Manifest |
| CDN fallback | 两个本地 Manifest 都不包含目标资源且配置 CDN 时,触发 CDN 下载 |
| 无 CDN fallback | 两个本地 Manifest 都不包含目标资源且未配置 CDN 时,返回明确失败 |
| CDN 模式路径隔离 | CDN 模式不走 StreamingAssets / PersistentData fallback |
| CDN 默认落盘路径 | 未设置外部保存路径时,下载文件保存到 PersistentData 资源根目录 |
| CDN 外部落盘路径 | 设置外部保存路径时,下载文件保存到外部路径并保留相对目录结构 |
| 平台路径隔离 | 平台专用保存路径通过宏或初始化代码传入,通用逻辑不硬编码平台路径 |
| GC 后重新加载 | 回收后能重新创建 loader 并成功加载 |
| 主动卸载资源 | `TryUnloadPrefabInMemory` 返回成功,内存统计下降,再次加载正常 |
| 正在使用资源卸载 | 资源处于 ACTIVE 或仍有框架侧引用时返回 `false`,游戏对象不受影响 |
| 重名资源卸载 | 多个已加载资源同名时返回 `false`,要求后续使用更明确名称或内部诊断工具处理 |
| 查询加载内存 | `GetLoadedAssetBundleMemorySize` 返回 byte 数,加载/卸载后变化合理 |
## 六、风险点
- `LoadFileByDecryptFunc` 依赖 byte[],如果某些本地包需要解密,不能直接走 `LoadFromFileAsync`
- StreamingAssets 在不同平台路径格式不同,异步读取必须保留原有平台兼容性。
- 小游戏环境使用 `MiniGame` 抽象,不能直接套用普通文件系统逻辑。
- GC 逻辑改动会影响资源复用,需要重点验证窗口、图集、材质、文本配置等常驻资源。
- CopyTo 后 `ESF_` 会替换为 `HF_`,新增内部类命名必须遵守替换规则,避免生成后引用不一致。
## 七、Unity 路径参考
- Unity Manual - Streaming Assets: `https://docs.unity3d.com/2022.3/Documentation/Manual/StreamingAssets.html`
- Unity API - `Application.streamingAssetsPath`: `https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Application-streamingAssetsPath.html`
- Unity API - `Application.persistentDataPath`: `https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Application-persistentDataPath.html`
- Unity API - `Application.dataPath`: `https://docs.unity3d.com/2022.3/Documentation/ScriptReference/Application-dataPath.html`
- Unity API - `AssetBundle.LoadFromMemoryAsync`: `https://docs.unity3d.com/2022.3/Documentation/ScriptReference/AssetBundle.LoadFromMemoryAsync.html`
- Unity API - `AssetBundle.LoadFromFileAsync`: `https://docs.unity3d.com/2022.3/Documentation/ScriptReference/AssetBundle.LoadFromFileAsync.html`
- Unity API - `BuildAssetBundleOptions.ChunkBasedCompression`: `https://docs.unity3d.com/2022.3/Documentation/ScriptReference/BuildAssetBundleOptions.ChunkBasedCompression.html`
## 八、默认决策
- 第一阶段只文档化方案,不改 ESF_ 代码。
- 后续代码实现只改 `ESF_` 源头,不直接改 `HF_` 副本。
- 公开接口保持兼容。
- 允许新增 `TryUnloadPrefabInMemory(...)``TryUnloadAssetInMemory(...)``GetLoadedAssetBundleMemorySize()` 作为可选公开能力。
- 允许新增 `SetCdnSaveRootPath(...)` 作为 CDN 热更新落盘根目录配置入口。
- 允许新增内部 `ESF_` 类和内部辅助方法。
- 非 CDN 模式默认按 Manifest 查找顺序读取StreamingAssets Manifest、PersistentData Manifest、CDN fallback。
- PersistentData 普通文件默认优先 `LoadFromFileAsync`
- StreamingAssets、CDN、小游戏路径保持内存加载。
- 下载证书校验暂不作为第一批主目标,后续单独处理。
- CDN 下载保存根目录允许外部传入;未传入时默认使用 PersistentData 资源根目录。
---
## 文档关联
> 当本文档发生变更时,应同步检查以下关联文档,避免信息不一致。
| 文档 | 关联原因 |
|---|---|
| [资源管理入口](README.md) | 本文件是资源管理模块的子文档 |
| [资源管理使用指南](使用指南.md) | 当前资源加载使用指南 |
| [modules/模块索引.md](../../模块索引.md) | 模块索引是项目知识总入口 |
| [rules/10-架构说明.md](../../../rules/10-架构说明.md) | 架构说明定义总体分层和依赖方向 |
| [rules/30-模块登记规则.md](../../../rules/30-模块登记规则.md) | 模块登记规则定义分类和状态口径 |

View File

@@ -31,7 +31,7 @@
| [模拟行为系统](frameworks/通用框架/README.md) | `Assets/StrayFog/Core/SimulateBehaviour/` | 解耦 MonoBehaviour模拟完整生命周期 | 已完成 |
| [对象池](frameworks/内存对象管理/README.md) | `Assets/Game/GameHFScripte/CopyToHF/MemoryManager/` | GameObject 对象池、脚本对象池、类实例池 | 已完成 |
| [事件系统](frameworks/事件系统/README.md) | `Assets/Game/GameHFScripte/CopyToHF/Event/` | CTC/STC 事件参数和事件派发 | 已完成 |
| [资源管理](frameworks/资源管理/README.md) | `Assets/Game/GameHFScripte/CopyToHF/AssetBundle/` | AssetBundle、语言资源、图集、加载规则 | 已完成 |
| [资源管理](frameworks/资源管理/README.md) | `Assets/Game/GameHFScripte/CopyToHF/AssetBundle/` | AssetBundle、语言资源、图集、加载规则;含 [资源加载重构方案](frameworks/资源管理/资源加载重构方案.md) | 已完成 |
| [相机系统](frameworks/相机系统/README.md) | `Assets/Game/GameHFScripte/CopyToHF/` | 多相机管理、相机堆栈、渲染顺序 | 初步梳理 |
| [实体系统](frameworks/实体系统/README.md) | `Assets/Game/GameHFScripte/GameFunction/GameEntity/` | 实体基类、模型/系统、实体池 | 已完成 |
| [UI 窗口系统](frameworks/UI窗口系统/README.md) | `Assets/Game/GameHFScripte/CopyToHF/UIWindowMgr/` | UI 窗口管理、窗口基类、生命周期 | 已完成 |
@@ -49,6 +49,8 @@
|---|---|---|---|
| [SF Editor](frameworks/编辑器工具/README.md#sf-editor) | `Assets/StrayFog/Editor/` | StrayFog 编辑器扩展 | 待验证 |
| [项目 Editor Assets](frameworks/编辑器工具/README.md#project-editor-assets) | `Assets/Editor/` | 项目编辑器资源与脚本 | 待验证 |
| [UI 拼接工具](frameworks/编辑器工具/UI拼接工具.md) | `Assets/Editor/Command/` | UI 预制体 JSON 双向转换 | 待验证 |
| [MCP 工具](frameworks/编辑器工具/MCP工具/README.md) | `Tools/CodexUnityMcp/` | Codex ↔ Unity Editor MCP | 待验证 |
---