一个游戏 70% 都是 UI,剩下 30% 才是 GamePlay。TEngine UI 模块是一套轻量级、高可拓展性的商业化 UI 框架,配合事件模块实现 MVE(Model - View - Event)事件驱动架构,能够提高至少一倍的开发效率。
-
🚀 轻量级设计
- UI 脚本为纯 C# 实现,完全脱离 Mono 的生命周期
- 由 UIModule 的帧更新驱动并管理 UI 的生命周期
- 零依赖,不强制绑定任何第三方 UI 框架
- 极小的内存占用和性能开销
-
🔧 极高的可拓展性
- 模块化架构:UIBase → UIWindow/UIWidget 分层设计
- 代码自动生成:一键生成 UI 绑定代码,提升开发效率
- 灵活的生命周期:可自定义 UI 的创建、刷新、更新、销毁流程
- 事件驱动:与事件模块深度集成,支持 MVE 架构
-
💡 商业级开发流程
- 完整的 UI 开发工作流
- 支持同步/异步打开面板
- 自动管理 UI 资源引用(与资源模块集成)
- 支持 UI 层级管理和全屏面板
-
🎯 智能资源管理
- 与资源模块深度集成
- UI 销毁时自动释放资源引用
- 支持 UIWidget 动态创建和销毁
UIBase (UI基类)
├── UIWindow (UI窗口基类)
└── UIWidget (UI组件基类)
- UIBase:所有 UI 的基类,提供基础功能和生命周期管理
- UIWindow:窗口级 UI,用于全屏或半屏面板
- UIWidget:组件级 UI,用于可复用的 UI 组件
- 纯 C# 实现:不依赖 MonoBehaviour,完全由框架管理生命周期
- 事件驱动:通过事件系统解耦 UI 逻辑和业务逻辑
- 资源自动化:UI 销毁时自动清理资源引用和事件监听
在 Scene 窗口下右键 ScriptGenerator 菜单下的 About 目录可以查看默认 UI 命名前缀规范:
默认命名规范(完整前缀表):
| 前缀 | 生成类型 | 示例节点名 |
|---|---|---|
m_go_ |
GameObject |
m_go_Effect |
m_item_ |
UIWidget(子类) |
m_item_Slot |
m_tf_ |
Transform |
m_tf_Container |
m_rect_ |
RectTransform |
m_rect_Panel |
m_text_ |
Text |
m_text_Title |
m_richText_ |
RichTextItem |
m_richText_Desc |
m_btn_ |
Button |
m_btn_Start |
m_img_ |
Image |
m_img_Icon |
m_rimg_ |
RawImage |
m_rimg_Avatar |
m_scroll_ |
ScrollRect |
m_scroll_List |
m_scrollBar_ |
Scrollbar |
m_scrollBar_Vert |
m_input_ |
InputField |
m_input_Name |
m_grid_ |
GridLayoutGroup |
m_grid_Items |
m_hlay_ |
HorizontalLayoutGroup |
m_hlay_Tabs |
m_vlay_ |
VerticalLayoutGroup |
m_vlay_List |
m_slider_ |
Slider |
m_slider_Volume |
m_toggle_ |
Toggle |
m_toggle_Sound |
m_group_ |
ToggleGroup |
m_group_Tab |
m_curve_ |
AnimationCurve |
m_curve_Anim |
m_canvasGroup_ |
CanvasGroup |
m_canvasGroup_Fade |
m_tmp_ |
TextMeshProUGUI |
m_tmp_Name |
m_tmpInput_ |
TMP_InputField |
m_tmpInput_Search |
m_tmpDropdown_ |
TMP_Dropdown |
m_tmpDropdown_Lang |
m_canvas_ |
Canvas |
m_canvas_Overlay |
m_dropdown_ |
Dropdown |
m_dropdown_Select |
注意:前缀匹配按
ScriptGeneratorSetting.asset中的顺序进行,长前缀优先匹配(如m_scrollBar_在m_scroll_之前,m_tmpInput_在m_tmp_之前)。
重要提示:
m_item节点为特殊节点,表示是 UI 下的 UIWidget 组件,不会继续往下遍历生成 UI 代码。若需要这个 UIWidget 组件的代码,则在m_item节点右键生成该组件的 UI 脚本。
有自定义需求可以在 TEngineSetting 下进行自定义:
遵守前期默认配置或者自定义配置进行 UI 编排:
在 UI 的根节点右键 ScriptGenerator 生成 UI 代码到剪贴板上:
提示:使用 UniTask 的生成代码可以做异步事件流驱动的 UI。
自行创建 UI 脚本到需要的目录下并复制生成的 UI 代码。
// 同步打开面板
GameModule.UI.ShowUI<GameMainUI>(userData);
// 异步打开面板(推荐)
await GameModule.UI.ShowUIAsync<GameMainUI>(userData);namespace GameLogic
{
/// <summary>
/// BattleMainUI 面板
/// <remarks>
/// UIWindow 需要标记 Window 特性:
/// - UILayer:UI 层级(可以自行定义)
/// - fullScreen:是否为全屏面板(全屏面板会停止和隐藏这个面板堆栈后面的面板)
/// </remarks>
/// </summary>
[Window(UILayer.Bottom, fullScreen: true)]
class BattleMainUI : UIWindow
{
private TouchMove m_touchView;
#region 脚本工具生成的代码
private RectTransform m_rectContainer;
private GameObject m_itemTouch;
private Button m_btnLeaveBattle;
private GameObject m_goTopInfo;
private Button m_btnPause;
public override void ScriptGenerator()
{
m_rectContainer = FindChildComponent<RectTransform>("m_rectContainer");
m_itemTouch = FindChild("m_rectContainer/m_itemTouch").gameObject;
m_btnLeaveBattle = FindChildComponent<Button>("m_btnLeaveBattle");
m_goTopInfo = FindChild("m_goTopInfo").gameObject;
m_btnPause = FindChildComponent<Button>("m_goTopInfo/m_btnPause");
m_btnLeaveBattle.onClick.AddListener(OnClickLeaveBattleBtn);
m_btnPause.onClick.AddListener(OnClickPauseBtn);
}
#endregion
#region 事件处理
private void OnClickPauseBtn()
{
BattleSys.Instance.Pause = !BattleSys.Instance.Pause;
}
private void OnClickLeaveBattleBtn()
{
BattleSys.Instance.StopBattle(isBattleEnd: false, isWin: false);
}
#endregion
/// <summary>
/// 注册事件
/// 通过 AddUIEvent 注册的事件会自动绑定 UI 生命周期,面板销毁时自动移除监听
/// </summary>
public override void RegisterEvent()
{
AddUIEvent(ActorLogicEventDefined.OnMainPlayerBagDataChange, RefreshUI);
}
/// <summary>
/// 绑定 UI 成员属性
/// 特殊的 m_item 节点需要创建对应的 UIWidget 脚本
/// </summary>
public override void BindMemberProperty()
{
// 方式1:通过 GameObject 创建 Widget
m_touchView = CreateWidget<TouchMove>(m_itemTouch);
// 方式2:通过路径创建 Widget
// m_touchView = CreateWidget<TouchMove>("m_rectContainer/m_itemTouch");
// 方式3:通过资源路径异步创建 Widget
// m_touchView = await CreateWidgetByPathAsync<TouchMove>(m_rectContainer, "UI/TouchMove");
}
/// <summary>
/// UI 刷新
/// </summary>
public override void OnRefresh()
{
// 刷新 UI 数据
}
}
}namespace GameLogic
{
/// <summary>
/// 移动操作 UIWidget
/// </summary>
class TouchMove : UIWidget, IUICtrlMove
{
public override void BindMemberProperty()
{
// 绑定 Widget 成员
}
/// <summary>
/// UI 更新
/// 注意:只有在重写了此方法才会驱动这个 Widget 或面板的 Update
/// </summary>
public override void OnUpdate()
{
TProfiler.BeginSample("CheckMoveTouchFinger");
CheckMoveTouchFinger();
TProfiler.EndSample();
TProfiler.BeginSample("UpdateTouchMovePos");
UpdateTouchMovePos();
TProfiler.EndSample();
TProfiler.BeginSample("UpdateKeyMove");
UpdateKeyMove();
TProfiler.EndSample();
}
}
}UIWidget 是 TEngine UI 框架中的核心组件设计,采用组合优于继承的设计理念,通过 Widget 的组合来构建复杂的 UI 界面。
-
🔧 高度可复用
- Widget 可以在多个 Window 中复用
- 独立的生命周期管理
- 可独立测试和维护
-
📦 模块化设计
- 每个 Widget 职责单一
- 通过组合构建复杂界面
- 易于扩展和维护
-
🎯 灵活的生命周期
- 支持动态创建和销毁
- 支持嵌套 Widget
- 自动管理资源引用
TEngine UI 框架支持多层次的 Widget 嵌套结构:
UIWindow (窗口)
├── UIPage (页面 Widget)
│ ├── TabButtonWidget (页签按钮 Widget)
│ ├── ContentWidget (内容 Widget)
│ │ └── ItemWidget (列表项 Widget)
│ └── ...
└── ...
下面通过一个完整的背包界面示例,展示如何使用 UIWidget 构建复杂的 UI 界面。
BagWindow (UIWindow - 背包窗口)
│
├── m_goTopBar (顶部栏)
│ ├── m_btnClose (关闭按钮)
│ └── m_textTitle (标题文本)
│
├── m_goTabBar (页签栏)
│ ├── m_itemTabWeapon (TabButtonWidget - 武器页签)
│ ├── m_itemTabArmor (TabButtonWidget - 防具页签)
│ ├── m_itemTabConsumable (TabButtonWidget - 消耗品页签)
│ └── m_itemTabMaterial (TabButtonWidget - 材料页签)
│
└── m_goContent (内容区域)
└── m_itemPageContainer (页面容器)
├── WeaponPage (UIPage Widget - 武器页面)
│ └── m_itemScrollView (滚动视图)
│ └── m_itemList (列表容器)
│ ├── BagItemWidget (Item Widget - 背包物品)
│ ├── BagItemWidget
│ └── ...
├── ArmorPage (UIPage Widget - 防具页面)
├── ConsumablePage (UIPage Widget - 消耗品页面)
└── MaterialPage (UIPage Widget - 材料页面)
namespace GameLogic
{
/// <summary>
/// 背包窗口
/// </summary>
[Window(UILayer.UI)]
class BagWindow : UIWindow
{
// 页签按钮 Widget 列表
private List<TabButtonWidget> _tabButtons = new List<TabButtonWidget>();
// 页面 Widget 列表
private List<UIPage> _pages = new List<UIPage>();
// 当前选中的页签索引
private int _currentTabIndex = 0;
#region 脚本工具生成的代码
private GameObject m_goTopBar;
private Button m_btnClose;
private Text m_textTitle;
private GameObject m_goTabBar;
private GameObject m_itemTabWeapon;
private GameObject m_itemTabArmor;
private GameObject m_itemTabConsumable;
private GameObject m_itemTabMaterial;
private GameObject m_goContent;
private Transform m_itemPageContainer;
public override void ScriptGenerator()
{
m_goTopBar = FindChild("m_goTopBar").gameObject;
m_btnClose = FindChildComponent<Button>("m_goTopBar/m_btnClose");
m_textTitle = FindChildComponent<Text>("m_goTopBar/m_textTitle");
m_goTabBar = FindChild("m_goTabBar").gameObject;
m_itemTabWeapon = FindChild("m_goTabBar/m_itemTabWeapon").gameObject;
m_itemTabArmor = FindChild("m_goTabBar/m_itemTabArmor").gameObject;
m_itemTabConsumable = FindChild("m_goTabBar/m_itemTabConsumable").gameObject;
m_itemTabMaterial = FindChild("m_goTabBar/m_itemTabMaterial").gameObject;
m_goContent = FindChild("m_goContent").gameObject;
m_itemPageContainer = FindChild("m_goContent/m_itemPageContainer");
m_btnClose.onClick.AddListener(OnClickCloseBtn);
}
#endregion
public override void BindMemberProperty()
{
// 创建页签按钮 Widget
_tabButtons.Add(CreateWidget<TabButtonWidget>(m_itemTabWeapon));
_tabButtons.Add(CreateWidget<TabButtonWidget>(m_itemTabArmor));
_tabButtons.Add(CreateWidget<TabButtonWidget>(m_itemTabConsumable));
_tabButtons.Add(CreateWidget<TabButtonWidget>(m_itemTabMaterial));
// 设置页签按钮数据
_tabButtons[0].SetData("武器", 0, OnTabClick);
_tabButtons[1].SetData("防具", 1, OnTabClick);
_tabButtons[2].SetData("消耗品", 2, OnTabClick);
_tabButtons[3].SetData("材料", 3, OnTabClick);
// 创建页面 Widget
_pages.Add(CreateWidgetByType<WeaponPage>(m_itemPageContainer));
_pages.Add(CreateWidgetByType<ArmorPage>(m_itemPageContainer));
_pages.Add(CreateWidgetByType<ConsumablePage>(m_itemPageContainer));
_pages.Add(CreateWidgetByType<MaterialPage>(m_itemPageContainer));
// 默认显示第一个页面
SwitchPage(0);
}
public override void RegisterEvent()
{
// 监听背包数据变化
AddUIEvent(BagEventDefined.OnBagDataChange, OnBagDataChange);
}
private void OnClickCloseBtn()
{
GameModule.UI.CloseUI<BagWindow>();
}
private void OnTabClick(int tabIndex)
{
if (_currentTabIndex == tabIndex) return;
// 更新页签状态
_tabButtons[_currentTabIndex].SetSelected(false);
_tabButtons[tabIndex].SetSelected(true);
// 切换页面
SwitchPage(tabIndex);
_currentTabIndex = tabIndex;
}
private void SwitchPage(int pageIndex)
{
// 隐藏所有页面
for (int i = 0; i < _pages.Count; i++)
{
_pages[i].Visible = (i == pageIndex);
}
}
private void OnBagDataChange()
{
// 刷新当前页面
if (_currentTabIndex < _pages.Count)
{
_pages[_currentTabIndex].Refresh();
}
}
}
}namespace GameLogic
{
/// <summary>
/// 页签按钮 Widget
/// 可复用的页签按钮组件
/// </summary>
class TabButtonWidget : UIWidget
{
private int _tabIndex;
private Action<int> _onClick;
#region 脚本工具生成的代码
private Button m_btnTab;
private Text m_textTabName;
private GameObject m_goSelected;
public override void ScriptGenerator()
{
m_btnTab = FindChildComponent<Button>("m_btnTab");
m_textTabName = FindChildComponent<Text>("m_btnTab/m_textTabName");
m_goSelected = FindChild("m_btnTab/m_goSelected").gameObject;
m_btnTab.onClick.AddListener(OnClickTab);
}
#endregion
/// <summary>
/// 设置页签数据
/// </summary>
public void SetData(string tabName, int tabIndex, Action<int> onClick)
{
m_textTabName.text = tabName;
_tabIndex = tabIndex;
_onClick = onClick;
}
/// <summary>
/// 设置选中状态
/// </summary>
public void SetSelected(bool selected)
{
m_goSelected.SetActive(selected);
// 可以添加更多选中状态的视觉效果
}
private void OnClickTab()
{
_onClick?.Invoke(_tabIndex);
}
}
}namespace GameLogic
{
/// <summary>
/// 页面基类 Widget
/// 所有页面的基类,提供通用的页面功能
/// </summary>
abstract class UIPage : UIWidget
{
/// <summary>
/// 刷新页面数据
/// </summary>
public virtual void Refresh()
{
// 子类实现具体的刷新逻辑
}
/// <summary>
/// 页面显示时调用
/// </summary>
public override void OnSetVisible(bool visible)
{
base.OnSetVisible(visible);
if (visible)
{
Refresh();
}
}
}
}namespace GameLogic
{
/// <summary>
/// 武器页面 Widget
/// 展示武器类型的背包物品
/// </summary>
class WeaponPage : UIPage
{
// 物品 Widget 列表
private List<BagItemWidget> _itemWidgets = new List<BagItemWidget>();
#region 脚本工具生成的代码
private ScrollRect m_itemScrollView;
private Transform m_itemList;
public override void ScriptGenerator()
{
m_itemScrollView = FindChildComponent<ScrollRect>("m_itemScrollView");
m_itemList = FindChild("m_itemScrollView/Viewport/Content/m_itemList");
}
#endregion
public override void Refresh()
{
// 获取武器类型的物品数据
var weaponItems = BagMgr.Instance.GetItemsByType(ItemType.Weapon);
// 调整物品 Widget 数量
AdjustIconNum(_itemWidgets, weaponItems.Count, m_itemList);
// 更新每个物品 Widget 的数据
for (int i = 0; i < weaponItems.Count; i++)
{
_itemWidgets[i].SetData(weaponItems[i]);
}
}
}
}namespace GameLogic
{
/// <summary>
/// 背包物品 Widget
/// 可复用的物品展示组件
/// </summary>
class BagItemWidget : UIWidget
{
private ItemData _itemData;
#region 脚本工具生成的代码
private Image m_imgIcon;
private Text m_textName;
private Text m_textCount;
private GameObject m_goQuality;
private Button m_btnItem;
public override void ScriptGenerator()
{
m_imgIcon = FindChildComponent<Image>("m_imgIcon");
m_textName = FindChildComponent<Text>("m_textName");
m_textCount = FindChildComponent<Text>("m_textCount");
m_goQuality = FindChild("m_goQuality").gameObject;
m_btnItem = FindChildComponent<Button>("m_btnItem");
m_btnItem.onClick.AddListener(OnClickItem);
}
#endregion
/// <summary>
/// 设置物品数据
/// </summary>
public void SetData(ItemData itemData)
{
_itemData = itemData;
if (itemData == null)
{
Visible = false;
return;
}
Visible = true;
// 设置物品图标(使用 SetSprite 自动管理资源)
m_imgIcon.SetSprite($"UI/Icon/Item/{itemData.IconName}");
// 设置物品名称
m_textName.text = itemData.Name;
// 设置数量
m_textCount.text = itemData.Count > 1 ? itemData.Count.ToString() : "";
// 设置品质显示
SetQuality(itemData.Quality);
}
/// <summary>
/// 设置品质显示
/// </summary>
private void SetQuality(int quality)
{
// 根据品质显示不同的边框颜色等
// m_goQuality.GetComponent<Image>().color = GetQualityColor(quality);
}
private void OnClickItem()
{
if (_itemData != null)
{
// 发送物品点击事件
GameEvent.Send(BagEventDefined.OnItemClick, _itemData);
}
}
}
}列表项是 UIWidget 最常见的应用场景:
// 聊天消息列表项
class ChatMessageWidget : UIWidget
{
public void SetData(ChatMessageData data) { }
}
// 排行榜列表项
class RankItemWidget : UIWidget
{
public void SetData(RankData data) { }
}
// 商店商品列表项
class ShopItemWidget : UIWidget
{
public void SetData(ShopItemData data) { }
}将复杂功能封装成 Widget:
// 角色属性面板 Widget
class CharacterAttributeWidget : UIWidget
{
public void RefreshAttribute(CharacterData data) { }
}
// 技能树 Widget
class SkillTreeWidget : UIWidget
{
public void RefreshSkillTree(SkillTreeData data) { }
}支持动态创建和销毁的 Widget:
// 动态创建提示 Widget
class TipWidget : UIWidget
{
public static void ShowTip(string message, Vector2 position)
{
var tip = CreateWidgetByType<TipWidget>(UIModule.UIRoot);
tip.SetMessage(message);
tip.SetPosition(position);
// 3秒后自动销毁
UniTask.Delay(3000).ContinueWith(() => {
tip.OnDestroy();
Object.Destroy(tip.gameObject);
}).Forget();
}
}// Widget 通过 SetData 方法接收数据,自动更新 UI
class ItemWidget : UIWidget
{
public void SetData(ItemData data)
{
// 根据数据自动更新 UI
UpdateUI(data);
}
}// Widget 通过事件系统与外部通信
class ItemWidget : UIWidget
{
public override void RegisterEvent()
{
// 监听物品数据变化
AddUIEvent(ItemEventDefined.OnItemUpdate, OnItemUpdate);
}
private void OnItemUpdate(ItemData data)
{
if (data.ItemId == _itemData.ItemId)
{
SetData(data);
}
}
}// 通过组合多个 Widget 构建复杂界面
class ComplexPage : UIPage
{
private HeaderWidget _header;
private ContentWidget _content;
private FooterWidget _footer;
public override void BindMemberProperty()
{
_header = CreateWidget<HeaderWidget>("m_itemHeader");
_content = CreateWidget<ContentWidget>("m_itemContent");
_footer = CreateWidget<FooterWidget>("m_itemFooter");
}
}每个 Widget 应该只负责一个明确的功能:
// ✅ 推荐:职责单一
class ItemIconWidget : UIWidget
{
// 只负责显示物品图标
}
// ❌ 不推荐:职责过多
class ItemWidget : UIWidget
{
// 同时负责显示图标、名称、数量、品质、操作按钮等
}Widget 只负责视图展示,数据由外部传入:
// ✅ 推荐:数据与视图分离
class ItemWidget : UIWidget
{
public void SetData(ItemData data)
{
// 只负责根据数据更新视图
}
}
// ❌ 不推荐:Widget 直接访问数据管理器
class ItemWidget : UIWidget
{
public void Refresh()
{
var data = ItemMgr.Instance.GetItem(_itemId); // 不推荐
}
}统一使用 SetData 方法更新 Widget:
// ✅ 推荐:统一的 SetData 接口
class ItemWidget : UIWidget
{
public void SetData(ItemData data) { }
}
// ❌ 不推荐:多个更新方法
class ItemWidget : UIWidget
{
public void SetIcon(string icon) { }
public void SetName(string name) { }
public void SetCount(int count) { }
}根据需求重写生命周期方法:
class ItemWidget : UIWidget
{
// 只在需要时重写 OnUpdate
public override void OnUpdate()
{
// 实时更新倒计时等
UpdateCountdown();
}
// 在 OnSetVisible 中处理显示/隐藏逻辑
public override void OnSetVisible(bool visible)
{
base.OnSetVisible(visible);
if (visible)
{
PlayShowAnimation();
}
}
}/// <summary>
/// UI 创建时调用(资源加载完成后)
/// </summary>
protected virtual void OnCreate() { }
/// <summary>
/// UI 刷新时调用(每次打开时)
/// </summary>
protected virtual void OnRefresh() { }
/// <summary>
/// UI 更新时调用(每帧,需要重写才会调用)
/// </summary>
protected virtual void OnUpdate() { }
/// <summary>
/// UI 销毁时调用
/// </summary>
protected virtual void OnDestroy() { }// 方式1:通过 GameObject 创建(推荐,资源已存在)
T CreateWidget<T>(GameObject goRoot, bool visible = true) where T : UIWidget, new();
// 方式2:通过路径创建(同步)
T CreateWidgetByPath<T>(Transform parentTrans, string assetLocation, bool visible = true) where T : UIWidget, new();
// 方式3:通过路径创建(异步,推荐)
UniTask<T> CreateWidgetByPathAsync<T>(Transform parentTrans, string assetLocation, bool visible = true) where T : UIWidget, new();
// 方式4:通过类型创建(根据类型名加载资源)
T CreateWidgetByType<T>(Transform parentTrans, bool visible = true) where T : UIWidget, new();// 注册事件(自动绑定 UI 生命周期)
AddUIEvent(int eventType, Action handler);
AddUIEvent<T>(int eventType, Action<T> handler);
AddUIEvent<T, U>(int eventType, Action<T, U> handler);
// 移除所有事件(通常在 OnDestroy 中自动调用)
RemoveAllUIEvent();// 查找子节点
Transform FindChild(string path);
Transform FindChild(Transform trans, string path);
// 查找组件
T FindChildComponent<T>(string path) where T : Component;
T FindChildComponent<T>(Transform trans, string path) where T : Component;UI 面板需要标记 Window 特性,以标识层级和是否全屏:
[Window(UILayer.Bottom, fullScreen: true)]
class BattleMainUI : UIWindow
{
// 全屏面板会把下层面板的 Visible 设置为 false
}通过 AddUIEvent 注册的事件会自动绑定 UI 生命周期:
- ✅ UI 创建时自动注册事件
- ✅ UI 销毁时自动移除事件监听
- ✅ 无需手动管理事件生命周期
public override void RegisterEvent()
{
// 面板销毁时自动移除监听,无需手动 RemoveEventListener
AddUIEvent(ActorLogicEventDefined.OnMainPlayerBagDataChange, RefreshUI);
}UI 模块与资源模块深度集成:
- ✅ UI 加载时自动管理资源引用
- ✅ UI 销毁时自动释放资源
- ✅ 支持 UIWidget 动态创建和销毁
UI 模块支持依赖注入:
// 设置依赖注入器
UIBase.Injector = (uiBase) => {
// 注入依赖
if (uiBase is BattleMainUI battleUI)
{
battleUI.SetDependency(someService);
}
};UI 的 Update 方法只有在子类显式 override 时才会被框架调用,避免不必要的性能开销。这是通过基类的 _hasOverrideUpdate 字段实现的:
- 基类
UIBase默认将_hasOverrideUpdate设为false,框架跳过未重写OnUpdate的面板 - 子类一旦 override
OnUpdate,框架自动识别并每帧驱动该面板的 Update
// ✅ 正确:重写 OnUpdate,框架会每帧调用
public override void OnUpdate()
{
// 更新逻辑
}
// ✅ 正确:不需要每帧更新时,不重写 OnUpdate,节省性能开销
// (不要写空的 override OnUpdate,否则框架仍会每帧调用)// ✅ 推荐:异步加载,不阻塞主线程
await GameModule.UI.ShowUIAsync<GameMainUI>(userData);
// ❌ 不推荐:同步加载,可能造成卡顿
GameModule.UI.ShowUI<GameMainUI>(userData);// ✅ 推荐:自动管理事件生命周期
public override void RegisterEvent()
{
AddUIEvent(EventDefined.SomeEvent, OnSomeEvent);
}
// ❌ 不推荐:需要手动管理
void OnCreate()
{
GameEvent.AddEventListener(EventDefined.SomeEvent, OnSomeEvent);
}
void OnDestroy()
{
GameEvent.RemoveEventListener(EventDefined.SomeEvent, OnSomeEvent);
}对于可复用的 UI 组件,使用 UIWidget:
// 创建多个相同的 Widget
List<ItemWidget> items = new List<ItemWidget>();
for (int i = 0; i < 10; i++)
{
var item = CreateWidgetByType<ItemWidget>(m_itemContainer);
items.Add(item);
}只有需要每帧更新的 UI 才重写 OnUpdate:
// ✅ 需要实时更新的 UI
public override void OnUpdate()
{
UpdateTimer();
}
// ❌ 不需要每帧更新的 UI,不重写 OnUpdate



