refactor-assembly #1
|
|
@ -9,6 +9,20 @@
|
||||||
- 本文重点约束 UIForm 级模块;子组件(`Item`、`Area` 等)可只实现 `Context + View`
|
- 本文重点约束 UIForm 级模块;子组件(`Item`、`Area` 等)可只实现 `Context + View`
|
||||||
- Unity GameFramework 的底层细节不在本文展开,本文只约束项目内 UI 代码组织
|
- Unity GameFramework 的底层细节不在本文展开,本文只约束项目内 UI 代码组织
|
||||||
|
|
||||||
|
补充说明(插件化落地):
|
||||||
|
|
||||||
|
- 本规范通过 `Assets/Plugins/UIModule/` 以**可选插件**方式落地
|
||||||
|
- 插件无独立程序集,通过 `.asmref` 融入原项目各程序集:
|
||||||
|
- `SepCore.Base.UIModule.asmref`:`Base/` → `SepCore.Base`
|
||||||
|
- `SepCore.Runtime.UIModule.asmref`:`Runtime/` → `SepCore.Runtime`
|
||||||
|
- `SepCore.Presentation.UIModule.asmref`:`UI/` → `SepCore.Presentation`
|
||||||
|
- `Runtime/` 内 `SepCore.UI` 命名空间放置基础抽象、UseCase、RawData;`SepCore.CustomComponent` 放置 `UIRouterComponent`
|
||||||
|
- `UI/` 内 `SepCore.UIModule` 命名空间放置 Controller / Context / View
|
||||||
|
- 不启用插件时,项目可继续使用原有基座 UI 路线
|
||||||
|
- 启用插件时,必须通过 `UIRouterComponent` 统一管理 `Controller / UseCase`
|
||||||
|
- 通过 `UIRouterComponent` 打开的 UIForm 必须配置对应 Controller,缺少绑定视为接入错误,不回退到原始 UI 打开路径
|
||||||
|
- 当前插件对外 UI 生命周期接口为 async-first:`OpenUIAsync / CloseUIAsync`
|
||||||
|
|
||||||
## 2. 核心原则
|
## 2. 核心原则
|
||||||
|
|
||||||
### 2.1 单向数据职责
|
### 2.1 单向数据职责
|
||||||
|
|
@ -35,7 +49,8 @@ View
|
||||||
|
|
||||||
### 2.2 严格分离业务数据与展示数据
|
### 2.2 严格分离业务数据与展示数据
|
||||||
|
|
||||||
- `RawData` 是纯业务数据,不承载展示模型
|
- `RawData` 默认是业务传输模型,不承载展示模型。
|
||||||
|
- 轻量 UI 可携带回调委托,但回调只允许由 Controller 注册和触发,不得进入 Context / View。
|
||||||
- `Context` 是纯展示数据,不进入 `UseCase`
|
- `Context` 是纯展示数据,不进入 `UseCase`
|
||||||
- `UseCase` 不能返回 `Context`
|
- `UseCase` 不能返回 `Context`
|
||||||
- `View` 只能消费 `Context`
|
- `View` 只能消费 `Context`
|
||||||
|
|
@ -44,8 +59,8 @@ View
|
||||||
|
|
||||||
对于 UIForm 级模块,外部流程只能通过 `Controller` 驱动 UI:
|
对于 UIForm 级模块,外部流程只能通过 `Controller` 驱动 UI:
|
||||||
|
|
||||||
- 打开 UI
|
- 异步打开 UI
|
||||||
- 关闭 UI
|
- 异步关闭 UI
|
||||||
- 绑定 `UseCase`
|
- 绑定 `UseCase`
|
||||||
- 刷新 UI
|
- 刷新 UI
|
||||||
- 响应 UI 专用事件
|
- 响应 UI 专用事件
|
||||||
|
|
@ -83,6 +98,7 @@ View
|
||||||
- 命名:`XXXFormRawData`
|
- 命名:`XXXFormRawData`
|
||||||
- 只描述业务数据,不包含 UI 展示行为
|
- 只描述业务数据,不包含 UI 展示行为
|
||||||
- 可以包含领域对象、配置对象、标识符、枚举、数值和纯数据集合
|
- 可以包含领域对象、配置对象、标识符、枚举、数值和纯数据集合
|
||||||
|
- **轻量场景下可携带回调委托**,由 Controller 在构建 Context 前完成注册
|
||||||
- 不允许依赖 `Context`、`View`、`Sprite`、`TMP_Text` 等展示相关类型
|
- 不允许依赖 `Context`、`View`、`Sprite`、`TMP_Text` 等展示相关类型
|
||||||
- 不允许直接使用 `XXXItemContext`、`XXXFormContext` 作为字段类型
|
- 不允许直接使用 `XXXItemContext`、`XXXFormContext` 作为字段类型
|
||||||
|
|
||||||
|
|
@ -90,7 +106,7 @@ View
|
||||||
|
|
||||||
- `RawData` 的目标是表达“业务上发生了什么”
|
- `RawData` 的目标是表达“业务上发生了什么”
|
||||||
- `Context` 的目标是表达“界面应该怎么显示”
|
- `Context` 的目标是表达“界面应该怎么显示”
|
||||||
- 两者不能混用
|
- `RawData` 可以携带行为(如回调),`Context` 只承载展示数据,不允许携带回调
|
||||||
|
|
||||||
### 3.3 Controller 层
|
### 3.3 Controller 层
|
||||||
|
|
||||||
|
|
@ -98,15 +114,19 @@ View
|
||||||
|
|
||||||
约束:
|
约束:
|
||||||
|
|
||||||
- UIForm 级模块默认必须有 `Controller`
|
- 对启用 `UIRouterComponent` 管理的 UIForm,必须存在可实例化的 Controller 绑定;未绑定时 Router 应直接失败并输出 Error,不允许 fallback 到 `GameEntry.UI.OpenUIForm(...)`
|
||||||
- 命名:`XXXFormController`
|
- 命名:`XXXFormController`
|
||||||
- 可基于 `UIFormControllerCommonBase<TContext, TForm>` 实现
|
- 可基于 `UIFormControllerBase<TContext, TForm>` 实现
|
||||||
- 通过 `BindUseCase(IUIUseCase)` 注入用例并做类型校验
|
- 通过 `BindUseCase(IUIUseCase)` 注入用例并做类型校验
|
||||||
- `OpenUI(object userData = null)` 负责接受外部参数、准备数据并打开 UI
|
- 当前对外入口为 `OpenUIAsync(object userData = null, float timeout = 30f)` 与 `CloseUIAsync(...)`
|
||||||
|
- `OpenUIAsync` 只允许外部传入 `RawData` 或可转换为 `RawData` 的参数,不接受外部传入 `Context`
|
||||||
|
- 当 `userData == null` 且 UI 绑定了 UseCase 时,Controller 应通过 UseCase 构造初始 RawData,再转换为 Context
|
||||||
|
- 当 `userData == null` 且 UI 没有 UseCase 时,Controller 可按 UI 类型决定构造默认 RawData / 默认 Context,或返回失败并输出 warning
|
||||||
- 负责 `RawData / Result -> Context` 的转换,常见形式为 `BuildContext`
|
- 负责 `RawData / Result -> Context` 的转换,常见形式为 `BuildContext`
|
||||||
- 负责事件订阅与解除订阅,且必须成对出现
|
- 负责事件订阅与解除订阅,且必须成对出现
|
||||||
- 负责全量刷新与局部刷新策略
|
- 负责全量刷新与局部刷新策略
|
||||||
- 负责过滤 UI 专用事件的 `sender`,确保事件只作用于当前 UI 实例
|
- 在同类 UI 可能多实例时,负责通过 `sender`、`serialId` 或其他等价标识限制事件只作用于当前 UI 实例
|
||||||
|
- **推荐子类显式组织业务时序**:参数校验、Context 构造、临时状态缓存、关闭后续动作等都直接写在具体 Controller 中;基类只提供共用开关窗机制,不隐藏子类业务时机
|
||||||
|
|
||||||
允许职责:
|
允许职责:
|
||||||
|
|
||||||
|
|
@ -129,6 +149,7 @@ View
|
||||||
- 命名:`XXXFormContext`、`XXXItemContext`、`XXXAreaContext`
|
- 命名:`XXXFormContext`、`XXXItemContext`、`XXXAreaContext`
|
||||||
- 只能由 `Controller` 构建和更新
|
- 只能由 `Controller` 构建和更新
|
||||||
- 字段以展示友好为目标,例如标题、描述、图标、颜色、状态、列表、按钮文案
|
- 字段以展示友好为目标,例如标题、描述、图标、颜色、状态、列表、按钮文案
|
||||||
|
- **不允许携带回调委托或行为**,交互行为由 Controller 注册,View 通过 UI 专用事件通知 Controller
|
||||||
- 允许组合子 `Context`
|
- 允许组合子 `Context`
|
||||||
- 不进入 `UseCase`
|
- 不进入 `UseCase`
|
||||||
|
|
||||||
|
|
@ -146,7 +167,9 @@ View
|
||||||
|
|
||||||
- Form 类继承 `UGuiForm`,子组件通常继承 `MonoBehaviour`
|
- Form 类继承 `UGuiForm`,子组件通常继承 `MonoBehaviour`
|
||||||
- 命名:`XXXForm`、`XXXItem`、`XXXArea`
|
- 命名:`XXXForm`、`XXXItem`、`XXXArea`
|
||||||
- 提供 `RefreshUI(Context)`、`OnInit(Context)`、`OnReset()` 等渲染入口
|
- 提供打开、关闭、刷新入口:
|
||||||
|
- `UGuiForm`:`OnOpen(object userData)`、`OnClose(bool isShutdown, object userData)`、`RefreshUI(Context)`
|
||||||
|
- `MonoBehaviour` 子组件:可统一设计为 `OnOpen(Context)`、`OnClose()`、`RefreshUI(Context)`
|
||||||
- 只消费 `Context`
|
- 只消费 `Context`
|
||||||
- 用户交互通过 UI 专用事件通知 `Controller`
|
- 用户交互通过 UI 专用事件通知 `Controller`
|
||||||
- 不承载业务规则、流程推进、数据筛选和领域状态修改
|
- 不承载业务规则、流程推进、数据筛选和领域状态修改
|
||||||
|
|
@ -220,6 +243,38 @@ View
|
||||||
- `View -> 全局业务事件`
|
- `View -> 全局业务事件`
|
||||||
- `View -> 领域状态修改`
|
- `View -> 领域状态修改`
|
||||||
|
|
||||||
|
### 5.1 插件化分层建议
|
||||||
|
|
||||||
|
建议分层如下:
|
||||||
|
|
||||||
|
- `SepCore.Base`:UI 专用事件(`SepCore.Event`)
|
||||||
|
- `SepCore.Runtime`:基座与业务流程层,同时融入插件 `Runtime/` 中的基础抽象、UseCase、RawData 与 `UIRouterComponent`
|
||||||
|
- `SepCore.Presentation`:Controller、Context、View(`SepCore.UIModule`)
|
||||||
|
- Editor:`UIModule.Editor`,代码命名空间当前为 `SepCore.UIModule.Editor`
|
||||||
|
|
||||||
|
插件目录与程序集映射:
|
||||||
|
|
||||||
|
- `Base/` → `SepCore.Base`
|
||||||
|
- `Runtime/` → `SepCore.Runtime`
|
||||||
|
- `UI/` → `SepCore.Presentation`
|
||||||
|
|
||||||
|
建议依赖方向:
|
||||||
|
|
||||||
|
```text
|
||||||
|
SepCore.Base(UI 专用事件)
|
||||||
|
↑
|
||||||
|
SepCore.Runtime(基础抽象 + UseCase + RawData + Router)
|
||||||
|
↑
|
||||||
|
SepCore.Presentation(Controller + Context + View)
|
||||||
|
```
|
||||||
|
|
||||||
|
约束:
|
||||||
|
|
||||||
|
- `SepCore.Base` / `SepCore.Runtime` 不反向依赖 Presentation 层
|
||||||
|
- UI 专用事件虽在 `SepCore.Event` 命名空间,但语义上仍归属对应 UI 模块,禁止被业务模块复用
|
||||||
|
- 业务流程层通过插件入口(如 Router)调用五层 UI 能力
|
||||||
|
- 插件内无独立程序集,通过 `.asmref` 文件融入原项目编译
|
||||||
|
|
||||||
## 6. 事件通信规范
|
## 6. 事件通信规范
|
||||||
|
|
||||||
### 6.1 UI 与 Controller 的通信方式
|
### 6.1 UI 与 Controller 的通信方式
|
||||||
|
|
@ -231,6 +286,7 @@ View
|
||||||
- UI 专用事件只服务于当前 UI 模块
|
- UI 专用事件只服务于当前 UI 模块
|
||||||
- 这些事件不是业务公共事件
|
- 这些事件不是业务公共事件
|
||||||
- 业务模块、流程模块、领域模块不应复用这些事件
|
- 业务模块、流程模块、领域模块不应复用这些事件
|
||||||
|
- UI 专用事件可统一放在 `SepCore.Event` 命名空间下,但事件语义仍归属对应 UI 模块,禁止被业务模块复用
|
||||||
- 如果底层使用全局事件总线实现,也只能把它当作“传输通道”,不能把事件语义扩散成全局契约
|
- 如果底层使用全局事件总线实现,也只能把它当作“传输通道”,不能把事件语义扩散成全局契约
|
||||||
|
|
||||||
### 6.2 事件边界
|
### 6.2 事件边界
|
||||||
|
|
@ -239,12 +295,13 @@ View
|
||||||
- `Controller -> View`:通过刷新 `Context` 或调用 `View` 的渲染接口
|
- `Controller -> View`:通过刷新 `Context` 或调用 `View` 的渲染接口
|
||||||
- `Controller -> UseCase`:直接方法调用
|
- `Controller -> UseCase`:直接方法调用
|
||||||
- `UseCase -> Controller`:通过返回 `RawData / Result`,不通过 UI 事件反推界面
|
- `UseCase -> Controller`:通过返回 `RawData / Result`,不通过 UI 事件反推界面
|
||||||
|
- `外部流程 -> Controller`:只传入 `RawData` 或可转换为 `RawData` 的参数,不传入 `Context`
|
||||||
|
|
||||||
### 6.3 事件安全要求
|
### 6.3 事件安全要求
|
||||||
|
|
||||||
- `Controller` 必须校验事件 `sender`
|
- 同类 UI 可同时存在时,事件必须只作用于当前窗体实例;可通过 `sender`、`serialId` 或其他等价标识实现
|
||||||
- 同类 UI 可同时存在时,事件必须只作用于当前窗体实例
|
|
||||||
- UI 专用事件命名应体现模块归属,避免语义过宽
|
- UI 专用事件命名应体现模块归属,避免语义过宽
|
||||||
|
- 同一 UI 可以按需要使用“多个精细事件”或“单一事件 + 子类型/按钮编号”的方式建模;例如当前 Dialog 使用 `DialogEventArgs + ButtonId`
|
||||||
|
|
||||||
## 7. 标准交互流程
|
## 7. 标准交互流程
|
||||||
|
|
||||||
|
|
@ -252,12 +309,12 @@ View
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Procedure / GameState
|
Procedure / GameState
|
||||||
-> 创建 UseCase
|
|
||||||
-> BindUseCase
|
-> BindUseCase
|
||||||
-> OpenUI
|
-> await OpenUIAsync(rawData or null)
|
||||||
|
|
||||||
Controller
|
Controller
|
||||||
-> UseCase.CreateInitialModel()
|
-> 如果外部传入 RawData:直接使用该 RawData
|
||||||
|
-> 如果外部未传入 RawData:UseCase.BuildRawData() / CreateInitialModel()
|
||||||
-> BuildContext(rawData)
|
-> BuildContext(rawData)
|
||||||
-> View.RefreshUI(context)
|
-> View.RefreshUI(context)
|
||||||
|
|
||||||
|
|
@ -271,14 +328,20 @@ Controller
|
||||||
-> View.RefreshUI(...)
|
-> View.RefreshUI(...)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 外部流程不传入 `Context`
|
||||||
|
- `Context` 只在 Presentation 层内由 Controller 构建
|
||||||
|
- `OpenUIAsync(null)` 对有 UseCase 的 UI 是合法入口,表示由 UseCase 构造初始 RawData
|
||||||
|
|
||||||
### 7.2 无 UseCase 的轻量流程
|
### 7.2 无 UseCase 的轻量流程
|
||||||
|
|
||||||
```text
|
```text
|
||||||
外部流程
|
外部流程
|
||||||
-> OpenUI(userData)
|
-> await OpenUIAsync(rawData)
|
||||||
|
|
||||||
Controller
|
Controller
|
||||||
-> BuildContext(userData)
|
-> BuildContext(rawData)
|
||||||
-> View.RefreshUI(context)
|
-> View.RefreshUI(context)
|
||||||
|
|
||||||
View
|
View
|
||||||
|
|
@ -289,22 +352,33 @@ Controller
|
||||||
-> 更新 Context / 打开其他 UI / 关闭当前 UI
|
-> 更新 Context / 打开其他 UI / 关闭当前 UI
|
||||||
```
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 轻量 UI 如果没有默认数据来源,`OpenUIAsync(null)` 可以失败并输出 warning
|
||||||
|
- Dialog 这类提示型 UI 属于必须由外部传入 RawData 的轻量 UI
|
||||||
|
- 即使是轻量 UI,外部也不传入 `Context`,Context 仍由 Controller 构建
|
||||||
|
|
||||||
### 7.3 关闭流程
|
### 7.3 关闭流程
|
||||||
|
|
||||||
1. 外部流程或 `Controller` 调用关闭逻辑
|
1. 外部流程或 `Controller` 调用 `CloseUIAsync(...)`
|
||||||
2. `Controller` 解除事件订阅
|
2. `Controller` 解除事件订阅
|
||||||
3. `View.OnClose` 清理本地视觉状态
|
3. `Controller` 清理本次交互缓存,例如回调、临时 `UserData` 和局部状态
|
||||||
4. 下次打开时重新按 `Context` 初始化
|
4. `View.OnClose` 清理本地视觉状态
|
||||||
|
5. 下次打开时重新按 `Context` 初始化
|
||||||
|
|
||||||
## 8. 目录与命名规范
|
## 8. 目录与命名规范
|
||||||
|
|
||||||
目录示例以 `Assets/GameMain/Scripts/UI` 为例。
|
目录示例以插件目录为例(`Assets/Plugins/UIModule`)。
|
||||||
|
|
||||||
- 目录:`UI/<SceneDomain>/UseCase|RawData|Controller|Context|View`
|
- 插件 `Base/Event/<UIName>/`:UI 专用事件(命名空间 `SepCore.Event`)
|
||||||
|
- 插件 `Runtime/Base/`:五层 UI 基础抽象(命名空间 `SepCore.UI`)
|
||||||
|
- 插件 `Runtime/<UIName>/`:UseCase、RawData(命名空间 `SepCore.UI`)
|
||||||
|
- 插件 `Runtime/` 根目录:`UIRouterComponent`(命名空间 `SepCore.CustomComponent`)
|
||||||
|
- 插件 `UI/<UIName>/`:Controller、Context、View(命名空间 `SepCore.UIModule`)
|
||||||
- 五层同名前缀保持一致,例如 `ShopForm*`、`LevelUpForm*`
|
- 五层同名前缀保持一致,例如 `ShopForm*`、`LevelUpForm*`
|
||||||
- 子组件上下文命名:`RoleItemContext`、`RewardItemContext`、`DisplayAreaContext`
|
- 子组件上下文命名:`RoleItemContext`、`RewardItemContext`、`DisplayAreaContext`
|
||||||
- 结果对象命名:`XXXResult`、`XXXActionResult`
|
- 结果对象命名:`XXXResult`、`XXXActionResult`
|
||||||
- 轻量 UI 可以省略 `UseCase` 和 `RawData` 目录,但不省略 `Controller`、`Context`、`View`
|
- 轻量 UI 可以省略 `UseCase` 和 `RawData`,但不省略 `Controller`、`Context`、`View`
|
||||||
|
|
||||||
## 9. 测试规范
|
## 9. 测试规范
|
||||||
|
|
||||||
|
|
@ -334,13 +408,17 @@ Controller
|
||||||
新增 UI 时,至少检查以下事项:
|
新增 UI 时,至少检查以下事项:
|
||||||
|
|
||||||
1. 先判断该 UI 属于标准五层还是轻量 UI
|
1. 先判断该 UI 属于标准五层还是轻量 UI
|
||||||
2. 如果存在业务规则或状态推进,必须引入 `UseCase`
|
2. 使用 `UIRouterComponent` 管理的 UIForm 必须配置可实例化的 Controller 绑定
|
||||||
3. `RawData` 中不得出现 `Context` 类型
|
3. 如果存在业务规则或状态推进,必须引入 `UseCase`
|
||||||
4. `Context` 只能在 `Controller` 中构建
|
4. 外部打开 UI 时只传入 `RawData` 或可转换为 `RawData` 的参数,不传入 `Context`
|
||||||
5. `View` 不得订阅全局业务事件
|
5. `OpenUIAsync(null)` 对有 UseCase 的 UI 应通过 UseCase 构造初始 RawData
|
||||||
6. `View` 的交互只能通过 UI 专用事件上报
|
6. `RawData` 中不得出现 `Context` 类型
|
||||||
7. `Controller` 必须成对管理事件订阅与解除订阅
|
7. `Context` 只能在 `Controller` 中构建
|
||||||
8. 有 `UseCase` 且需要补自动化测试时,测试写入 EditMode
|
8. `View` 不得订阅全局业务事件
|
||||||
|
9. `View` 的交互只能通过 UI 专用事件上报
|
||||||
|
10. `Controller` 必须成对管理事件订阅与解除订阅
|
||||||
|
11. `Controller` 必须在关闭时清理本次交互缓存
|
||||||
|
12. 有 `UseCase` 且需要补自动化测试时,测试写入 EditMode
|
||||||
|
|
||||||
## 11. 非目标说明
|
## 11. 非目标说明
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,220 @@
|
||||||
|
# UIModule 使用说明(可选插件)
|
||||||
|
|
||||||
|
## 1. 定位
|
||||||
|
|
||||||
|
`UIModule` 是基于 UGF 的可选 UI 插件,通过 `.asmref` 融入现有项目程序集:
|
||||||
|
|
||||||
|
- `Base/` → `SepCore.Base`
|
||||||
|
- `Runtime/` → `SepCore.Runtime`
|
||||||
|
- `UI/` → `SepCore.Presentation`
|
||||||
|
|
||||||
|
插件本身没有独立运行时程序集;基础抽象、事件、Controller、View 会并入宿主程序集编译。
|
||||||
|
不启用插件时,项目仍可继续使用原有 UI 路线。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 接入步骤
|
||||||
|
|
||||||
|
1. 确保以下 `.asmref` 已存在于插件目录:
|
||||||
|
- `SepCore.Base.UIModule.asmref`
|
||||||
|
- `SepCore.Runtime.UIModule.asmref`
|
||||||
|
- `SepCore.Presentation.UIModule.asmref`
|
||||||
|
2. 将 `Assets/Plugins/UIModule/Prefab/DialogForm.prefab` 复制到:
|
||||||
|
- `Assets/GameMain/UI/UIForms`
|
||||||
|
3. 处理原有 Dialog 资源(任选其一):
|
||||||
|
- 重命名旧 `DialogForm` 资源,或
|
||||||
|
- 确认无引用后删除旧 `DialogForm` 资源
|
||||||
|
4. 在 `Launcher` 场景的 `Customs` 下创建对象并挂载 `UIRouterComponent`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. UIRouter 配置
|
||||||
|
|
||||||
|
`UIRouterComponent` 通过 Inspector 配置 `UIFormType -> Controller` 映射:
|
||||||
|
|
||||||
|
- `UI Form Type`
|
||||||
|
- `Controller Type`
|
||||||
|
|
||||||
|
编辑器会扫描所有实现 `IUIFormController` 的**可实例化类型**(非抽象、非接口、非开放泛型)。
|
||||||
|
运行时 `Awake()` 会自动注册配置项。
|
||||||
|
|
||||||
|
当前 Router 对外接口:
|
||||||
|
|
||||||
|
- `BindUIUseCase(UIFormType uiFormType, IUIUseCase useCase)`
|
||||||
|
- `OpenUIAsync(UIFormType uiFormType, object userData = null, float timeout = 30f)`
|
||||||
|
- `CloseUIAsync(UIFormType uiFormType, object userData = null, float timeout = 30f)`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 缺少 Controller 绑定时会输出 `Error` 并直接失败
|
||||||
|
- 不会 fallback 到原始 `GameEntry.UI.OpenUIForm(...)`
|
||||||
|
- 重新注册同一 `UIFormType` 时,旧 Controller 会异步关闭并被新配置覆盖
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 当前运行时结构
|
||||||
|
|
||||||
|
### 4.1 基础抽象
|
||||||
|
|
||||||
|
`IUIFormController` 当前是 **async-first** 接口:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
UniTask<int?> OpenUIAsync(object userData = null, float timeout = 30f);
|
||||||
|
UniTask CloseUIAsync(object userData = null, float timeout = 30f);
|
||||||
|
void BindUseCase(IUIUseCase useCase);
|
||||||
|
```
|
||||||
|
|
||||||
|
`UIFormControllerBase<TContext, TForm>` 当前只提供**薄机制层**能力:
|
||||||
|
|
||||||
|
- `OpenFormAsync(TContext context, float timeout = 30f)`
|
||||||
|
- `CloseFormAsync(object userData = null, float timeout = 30f)`
|
||||||
|
|
||||||
|
它负责:
|
||||||
|
|
||||||
|
- 调用 `GameEntry.UI.OpenUIFormAsync(...) / CloseUIFormAsync(...)`
|
||||||
|
- 维护 `_context / _form / _formSerialId`
|
||||||
|
- 在打开后调用 `RefreshUI`
|
||||||
|
- 在打开 / 关闭时订阅与解除 `SubscribeCustomEvents()`
|
||||||
|
|
||||||
|
它**不负责**:
|
||||||
|
|
||||||
|
- 解析 `userData`
|
||||||
|
- 构造 `Context`
|
||||||
|
- 缓存业务回调
|
||||||
|
- 安排子类的业务时序
|
||||||
|
|
||||||
|
这些流程由具体 Controller 显式实现。
|
||||||
|
|
||||||
|
### 4.2 UGF 异步适配
|
||||||
|
|
||||||
|
插件的 async 行为并没有改 UGF 底层事件机制,而是通过以下适配层桥接:
|
||||||
|
|
||||||
|
- `AsyncTaskHelper`
|
||||||
|
- `UIAsyncExtension`
|
||||||
|
|
||||||
|
其中 `UIAsyncExtension` 目前已支持:
|
||||||
|
|
||||||
|
- `OpenUIFormAsync(string uiFormAssetName, string uiGroupName, ...)`
|
||||||
|
- `OpenUIFormAsync(int uiFormId, object userData = null, float timeout = 30f)`
|
||||||
|
- `CloseUIFormAsync(int serialId, object userData = null, float timeout = 30f)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Controller 实现约定
|
||||||
|
|
||||||
|
当前推荐写法是:
|
||||||
|
|
||||||
|
1. 子类自己在 `OpenUIAsync(...)` 里:
|
||||||
|
- 校验 `userData`
|
||||||
|
- 构造 `RawData / Context`
|
||||||
|
- 缓存本次交互需要的临时状态
|
||||||
|
- `await OpenFormAsync(...)`
|
||||||
|
2. 子类自己在 `CloseUIAsync(...)` 里:
|
||||||
|
- 清理本次交互缓存
|
||||||
|
- `await CloseFormAsync(...)`
|
||||||
|
3. 基类只提供共用开关窗机制,不隐藏业务时序
|
||||||
|
|
||||||
|
也就是说,实现 Controller 时,应该尽量做到:
|
||||||
|
|
||||||
|
- **看子类本身就能看懂完整流程**
|
||||||
|
- 不依赖“阅读基类钩子调用顺序”来推断业务时机
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Dialog 当前实现
|
||||||
|
|
||||||
|
### 6.1 相关类型
|
||||||
|
|
||||||
|
- `DialogRawData`(`SepCore.UI`)
|
||||||
|
- 外部输入模型
|
||||||
|
- 可携带:
|
||||||
|
- 标题 / 内容 / 按钮文本
|
||||||
|
- `PauseGame`
|
||||||
|
- `OnClickConfirm / OnClickCancel / OnClickOther`
|
||||||
|
- `UserData`
|
||||||
|
|
||||||
|
- `DialogContext`(`SepCore.UIModule`)
|
||||||
|
- 纯展示数据
|
||||||
|
- 不携带回调
|
||||||
|
|
||||||
|
- `DialogController`(`SepCore.UIModule`)
|
||||||
|
- 显式实现 `OpenUIAsync / CloseUIAsync`
|
||||||
|
- 负责缓存按钮回调与 `UserData`
|
||||||
|
- 负责把 `DialogRawData` 转成 `DialogContext`
|
||||||
|
|
||||||
|
- `DialogForm`(`SepCore.UIModule`)
|
||||||
|
- 只负责渲染和派发 UI 专用事件
|
||||||
|
|
||||||
|
- `DialogEventArgs`(`SepCore.Event`)
|
||||||
|
- 当前 Dialog 只使用**一个事件类型**
|
||||||
|
- 通过 `ButtonId` 区分按钮:
|
||||||
|
- `1`:Confirm
|
||||||
|
- `2`:Cancel
|
||||||
|
- `3`:Other
|
||||||
|
|
||||||
|
### 6.2 当前数据流
|
||||||
|
|
||||||
|
1. 外部调用:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
await uiRouter.OpenUIAsync(UIFormType.DialogForm, rawData);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. `DialogController.OpenUIAsync(...)`:
|
||||||
|
- 校验 `DialogRawData`
|
||||||
|
- 构造 `DialogContext`
|
||||||
|
- 缓存按钮回调和 `UserData`
|
||||||
|
- `await OpenFormAsync(context, timeout)`
|
||||||
|
|
||||||
|
3. `DialogForm` 在按钮点击时只发出:
|
||||||
|
|
||||||
|
- `DialogEventArgs.Create(1, null)`
|
||||||
|
- `DialogEventArgs.Create(2, null)`
|
||||||
|
- `DialogEventArgs.Create(3, null)`
|
||||||
|
|
||||||
|
4. `DialogController` 收到 `DialogEventArgs` 后根据 `ButtonId` 选出对应回调
|
||||||
|
|
||||||
|
5. `DialogController` 调用异步关闭流程,再执行回调:
|
||||||
|
- `await CloseUIAsync()`
|
||||||
|
- `callback?.Invoke(userData)`
|
||||||
|
|
||||||
|
6. `CloseUIAsync()` 会先清理缓存,再交给基类关闭 UI
|
||||||
|
|
||||||
|
### 6.3 当前限制
|
||||||
|
|
||||||
|
- `DialogController.OpenUIAsync(null)` 会失败并输出 warning
|
||||||
|
- Dialog 当前**不依赖 UseCase**
|
||||||
|
- `DialogForm` 当前没有本地化默认文本回退:
|
||||||
|
- 标题、正文、按钮文本都以 `DialogRawData` 传入值为准
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 注意点
|
||||||
|
|
||||||
|
1. **先注册再打开**
|
||||||
|
使用前需确保 `UIRouterComponent` 已完成 `Awake` 自动注册。
|
||||||
|
|
||||||
|
2. **Controller 必须绑定且可实例化**
|
||||||
|
缺少绑定时 Router 直接失败,不会回退到原生打开路径。
|
||||||
|
|
||||||
|
3. **统一走 async 接口**
|
||||||
|
启用 UIModule 管理的 UI,应统一通过:
|
||||||
|
- `OpenUIAsync(...)`
|
||||||
|
- `CloseUIAsync(...)`
|
||||||
|
|
||||||
|
4. **Controller 负责显式业务流程**
|
||||||
|
基类只提供开关窗机制;`userData` 校验、`Context` 构造、回调缓存与关闭后的后续动作都应在具体 Controller 中明确写出。
|
||||||
|
|
||||||
|
5. **保持依赖方向**
|
||||||
|
- `SepCore.Base`:UI 专用事件
|
||||||
|
- `SepCore.Runtime`:UI 基础抽象、`RawData`、`UseCase`、`UIRouterComponent`
|
||||||
|
- `SepCore.Presentation`:Controller、Context、View
|
||||||
|
|
||||||
|
6. **Prefab 脚本一致性**
|
||||||
|
Dialog prefab 上必须挂载插件内 `SepCore.UIModule.DialogForm`。
|
||||||
|
|
||||||
|
7. **Dialog 事件语义**
|
||||||
|
当前 Dialog 使用单一 `DialogEventArgs` + `ButtonId` 区分按钮,不再使用多个按钮事件类型。
|
||||||
|
|
||||||
|
8. **Dialog 当前按单实例思路使用**
|
||||||
|
`DialogController` 当前没有额外用 `sender` / `serialId` 做多实例过滤,默认按单实例 Dialog 使用。
|
||||||
Loading…
Reference in New Issue