Files
Seek-Battle-Evacuate/Docs/Tech/03_UIMaintenance.md
T
2026-08-30 22:25:23 +08:00

64 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`,运行时实例化子物体即可自动排布,不要手动改尺寸。