This commit is contained in:
huangjun
2026-08-30 22:25:23 +08:00
parent 92bf3e9097
commit 93a10a89c5
180 changed files with 15257 additions and 11604 deletions
+64
View File
@@ -0,0 +1,64 @@
# UI 维护要点
> 汇总 SBE 项目中 UI 的代码组织、嵌套 Form 协作与常见注意事项。
> 相关代码:`Assets/GameMain/Scripts/UI/`(生成 + 手写 partial)、`Assets/GameMain/Scripts/Base/Event/UIForm/`(UI 事件)。
## 一、Form / View 的组织方式
- **自动生成**:`UIPrefabBuilder`(编辑器菜单 `Utility/UI/Build UI Prefabs`)构建 prefab;`UIAssetsTools` 根据 `UISerializationRoot`/`UISerializationItem` 上的引用生成 `XxxForm.cs` 与 `XxxView.cs`(文件头带 `// <auto-generated />`)。
- **手写逻辑**:业务代码写在同名的 `.Logic.cs` partial 里(如 `LobbyForm.Logic.cs`),与生成文件合并;**不要**修改自动生成文件。
- **View 字段**:prefab 中需要代码访问的组件在序列化项上登记变量名(`variableName`),生成到 View 的 public 字段。View 只暴露组件引用,不做业务。
- **View 引用的是子 Form 组件而非子 View**:如 `LobbyView.warehouseForm`(WarehouseForm 组件)、`WarehouseView.inventoryPanelForm` / `itemDetailsPanelForm`。通过 `子Form.View` 再取具体组件。
## 二、嵌套 Form 的能力模型
- 一个 Form 可以嵌套子 Form(prefab 实例层级:`LobbyForm → WarehouseForm → InventoryForm / ItemDetailsForm`)。
- **子 Form 只提供能力**(public 方法),**不直接读取全局数据源、不主动触发刷新**;数据由调用方传入。例:`InventoryForm.RefreshList(stacks)`、`ItemDetailsForm.Refresh(itemId)`、`WarehouseForm.Refresh(stacks)`。
- **只有顶级 Form(通过 `GameEntry.UI.OpenUIForm` 打开的)才有编排权**,负责读数据并向下分发。例:`LobbyForm.OnOpen → SwitchPage → warehouseForm.Refresh(save.mainWarehouse)`。
- 子 Form 的能力入口保持幂等、可反复调用;父 Form 切换页面时重新调用即可。
## 三、嵌套 Form 与 UGF 生命周期
- **只有被 UIComponent 打开的顶级 Form 才会被框架调用 `OnInit / OnOpen / OnClose / OnUpdate` 等生命周期方法。嵌套 Form 的这些方法永远不会执行**——不要在嵌套 Form 里依赖它们(最常见错误:把事件绑定写在 `OnInit`,结果永远不生效)。
- 嵌套 Form 的**事件绑定/订阅改为幂等式**:`bool` 标志 + `EnsureXxx()`,在能力入口(如 `Refresh`)调用,保证先于任何交互执行。
```csharp
private bool _listenersBound = false;
private void EnsureListenersBound()
{
if (_listenersBound) return;
_listenersBound = true;
View.allToggle.onValueChanged.AddListener(OnAllToggleValueChanged);
}
```
- UGF 表单实例会被**对象池复用**,幂等标志同时防止重复订阅。
- 订阅了 `GameEntry.Event` 的嵌套 Form 实例被销毁后,事件中心可能残留引用:handler 开头加 `if (this == null) return;` 防御。
## 四、子 Form 向上传递数据:事件
- 子组件(如 `WarehouseSlotItem`)**禁止直接持有并调用父 Form**(避免反向引用),需要向上层传数据时发事件:
```csharp
GameEntry.Event.Fire(this, WarehouseSlotItemClickEventArgs.Create(_slotId));
```
- 事件参数类放在 `Assets/GameMain/Scripts/Base/Event/UIForm/`,命名 `XxxEventArgs`,继承 `GameEventArgs`,使用 `ReferencePool`(`Create` 静态工厂 + `Clear` 回收):
```csharp
public class XxxEventArgs : GameEventArgs
{
public static int EventId => typeof(XxxEventArgs).GetHashCode();
public override int Id => EventId;
public static XxxEventArgs Create(...) { var a = ReferencePool.Acquire<XxxEventArgs>(); ...; return a; }
public override void Clear() { ... }
}
```
- 事件参数要携带**唯一标识**(如格子索引 `SlotId`),不要用可能重复的语义值(如 itemId 在多堆同道具时无法唯一定位)。
- 订阅方是拥有上下文的父 Form(`WarehouseForm` 监听格子点击 → 分发选中高亮与详情刷新),在能力入口幂等订阅。
- 事件从 `GameEntry.Event.Subscribe(XxxEventArgs.EventId, handler)` / `GameEntry.Event.Fire(this, args)` 走 UGF 事件总线,无需手动管理对象生命周期。
## 五、其它约定
- **固定网格 + 模板格子**:列表用 `warehouseSlotRoot`(GridLayout 容器)+ `warehouseSlotTemplate`(一个模板格)。刷新时隐藏模板、销毁其他子物体、按模板实例化;格子数等数值来自配表(`GlobalConfig.WarehouseSlotCount`),**不写死**。
- **选中态**:点击选中由事件驱动,`SetSelectedSlot(slotId)` 按唯一索引高亮;筛选后重建网格时选中自动清除。
- **格式化文本**:界面文案用 `FormatTextUI`(Inspector 填 key,文本从 `FormatText_格式化文本.xlsx` 读取,`Set(params object[])` 传参),不要硬编码在代码里。
- **导航切换**:导航用 `Toggle + ToggleGroup` 管理选中态,代码只订阅 `onValueChanged`(仅在 `isOn == true` 时处理),不再手动维护按钮状态。
- **动画**:DOTween 移动 UI 元素时,`TransformPoint` 的入参是目标 rect **pivot 局部空间**坐标;元素锚点偏移需要换算:`(anchor - pivot) * rect.size + anchoredPosition`。
- **打开流程**:Form 由 `UIFormType` 枚举 + `UIFormConfig` 表(`tbuiformconfig`)数据驱动,`GameEntry.UI.OpenUIForm(UIFormType.Xxx)`;Procedure 的 `OnLeave` 记得 `GameEntry.UI.CloseAllLoadedUIForms()`。
- **布局组件注意**:生成 prefab 的 `Content` 上挂 `GridLayoutGroup` + `ContentSizeFitter`,运行时实例化子物体即可自动排布,不要手动改尺寸。