5.5 KiB
5.5 KiB
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.cspartial 里(如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)调用,保证先于任何交互执行。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(避免反向引用),需要向上层传数据时发事件:GameEntry.Event.Fire(this, WarehouseSlotItemClickEventArgs.Create(_slotId)); - 事件参数类放在
Assets/GameMain/Scripts/Base/Event/UIForm/,命名XxxEventArgs,继承GameEventArgs,使用ReferencePool(Create静态工厂 +Clear回收):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,运行时实例化子物体即可自动排布,不要手动改尺寸。