2026-05-22 15:23:11 +08:00
|
|
|
|
# StrayFog 框架 UI开发指南
|
|
|
|
|
|
|
|
|
|
|
|
## 一、UI窗口开发
|
|
|
|
|
|
|
|
|
|
|
|
### 1.1 窗口创建模板
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
[HFAssetBundlePath(enHFLanguageRootFolder.ASSETS_GAME_GAMEHFRESOURCE, "UIWindows/MyWindow/MyWindow.prefab")]
|
|
|
|
|
|
[HFUIWindowLayer(enHFUICanvasLayer.UIWindow, enHFUIWindowLayer.Dynamic, isIgnoreCloseByESC = false)]
|
|
|
|
|
|
public class MyWindow : AbsHFUIWindow
|
|
|
|
|
|
{
|
|
|
|
|
|
// UI组件
|
|
|
|
|
|
private SFUI_Button BtnClose;
|
|
|
|
|
|
private SFUI_TextMeshProUGUI TxtTitle;
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnRunAwake()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnRunAwake();
|
|
|
|
|
|
// 初始化UI组件引用
|
|
|
|
|
|
BtnClose = FindUICtrlByName<SFUI_Button>("BtnClose");
|
|
|
|
|
|
TxtTitle = FindUICtrlByName<SFUI_TextMeshProUGUI>("TxtTitle");
|
|
|
|
|
|
|
|
|
|
|
|
// 绑定按钮事件(OnRunAwake只执行一次)
|
|
|
|
|
|
BtnClose.OnAddClickListener += OnBtnClose;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnOpen(Action _onOpenComplete)
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnOpen(_onOpenComplete);
|
|
|
|
|
|
// 注册全局事件监听(每次打开都要注册)
|
|
|
|
|
|
HFFunCatalogue.Event.TCEvent.AddListener<HFEvent_DataUpdate>(
|
|
|
|
|
|
enHFCTCEvent.DataUpdate, OnDataUpdate);
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化数据显示
|
|
|
|
|
|
RefreshUI();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnClose(Action _onCloseComplete)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 取消全局事件监听(每次关闭都要取消)
|
|
|
|
|
|
HFFunCatalogue.Event.TCEvent.RemoveListener<HFEvent_DataUpdate>(
|
|
|
|
|
|
enHFCTCEvent.DataUpdate, OnDataUpdate);
|
|
|
|
|
|
|
|
|
|
|
|
base.OnClose(_onCloseComplete);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnBtnClose(GameObject go) => CloseWindow();
|
|
|
|
|
|
private void OnDataUpdate(IHFEventArg<enHFCTCEvent> arg) => RefreshUI();
|
|
|
|
|
|
private void RefreshUI() { /* 更新UI显示 */ }
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 1.2 窗口生命周期
|
|
|
|
|
|
|
|
|
|
|
|
| 方法 | 调用时机 | 说明 | 推荐操作 |
|
|
|
|
|
|
|-----|---------|------|---------|
|
|
|
|
|
|
| `OnRunAwake` | 窗口创建时调用一次 | 初始化组件引用 | **绑定按钮事件、初始化对象池** |
|
|
|
|
|
|
| `OnRunStart` | OnRunAwake之后调用一次 | 初始化数据 | **配置数据加载** |
|
|
|
|
|
|
| `OnOpen` | 每次打开窗口时调用 | 注册事件、刷新数据 | **注册TC事件、初始化数据显示** |
|
|
|
|
|
|
| `OnClose` | 每次关闭窗口时调用 | 取消事件、清理资源 | **取消TC事件、回收对象池** |
|
|
|
|
|
|
| `OnDestroy` | 窗口销毁时调用 | 释放资源 | **释放非托管资源** |
|
|
|
|
|
|
|
|
|
|
|
|
### 1.3 窗口操作
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
// 打开窗口
|
|
|
|
|
|
HFFunCatalogue.UIManager.OpenWindow<MyWindow>();
|
|
|
|
|
|
|
|
|
|
|
|
// 打开窗口并获取实例
|
|
|
|
|
|
HFFunCatalogue.UIManager.OpenWindow<MyWindow>((win, args) => {
|
|
|
|
|
|
// win: 窗口实例
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭窗口
|
|
|
|
|
|
HFFunCatalogue.UIManager.CloseWindow<MyWindow>();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 二、事件绑定规范
|
|
|
|
|
|
|
|
|
|
|
|
### 2.1 事件绑定位置
|
|
|
|
|
|
|
|
|
|
|
|
| 事件类型 | 推荐位置 | 原因 | 是否需要 `-=` 防重复 |
|
|
|
|
|
|
|---------|---------|------|-------------------|
|
|
|
|
|
|
| **UI按钮事件** | `OnRunAwake` | 静态稳定,只需注册一次 | ❌ 不需要 |
|
|
|
|
|
|
| **列表拖拽事件** | `OnRunAwake` | 窗口生命周期内持续响应 | ❌ 不需要 |
|
|
|
|
|
|
| **全局TC事件** | `OnOpen`/`OnClose` | 窗口关闭后不应响应 | ✅ 需要 |
|
|
|
|
|
|
| **数据刷新逻辑** | `OnOpen` | 每次打开窗口需刷新 | - |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 三、常用UI组件
|
|
|
|
|
|
|
|
|
|
|
|
### 3.1 SFUI_Button组件
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
SFUI_Button btn = GetComponent<SFUI_Button>();
|
|
|
|
|
|
|
|
|
|
|
|
// 添加事件
|
|
|
|
|
|
btn.OnAddClickListener += OnBtnClick;
|
|
|
|
|
|
btn.OnAddEnterListener += OnBtnEnter;
|
|
|
|
|
|
btn.OnAddExitListener += OnBtnExit;
|
|
|
|
|
|
|
|
|
|
|
|
// 设置状态
|
|
|
|
|
|
btn.SetSelfGray(true); // 灰色不可用
|
|
|
|
|
|
btn.interactable = false; // 不可交互
|
|
|
|
|
|
btn.SetSelected(true); // 选中状态
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.2 SFUI_TextMeshProUGUI组件
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
SFUI_TextMeshProUGUI txt = GetComponent<SFUI_TextMeshProUGUI>();
|
|
|
|
|
|
|
|
|
|
|
|
txt.text = "Hello World";
|
|
|
|
|
|
txt.color = Color.red;
|
|
|
|
|
|
txt.fontSize = 24;
|
|
|
|
|
|
txt.outlineColor = Color.black;
|
|
|
|
|
|
txt.outlineWidth = 2;
|
|
|
|
|
|
|
|
|
|
|
|
// 动态调整高度
|
|
|
|
|
|
txt.rectTransform.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, txt.preferredHeight);
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.3 SFUI_Image组件
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
SFUI_Image img = GetComponent<SFUI_Image>();
|
|
|
|
|
|
|
|
|
|
|
|
// 加载Sprite
|
|
|
|
|
|
img.HFSetSprite(enHFSpriteAtlas.ASSETS_GAME_GAMEHFRESOURCE_SPRITEATLAS_ITEM,
|
|
|
|
|
|
"Icon_Player",
|
|
|
|
|
|
enHFLanguageRootFolder.ASSETS_GAME_GAMEHFRESOURCE);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置透明度
|
|
|
|
|
|
img.SetAlpha(0.5f);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置灰度
|
|
|
|
|
|
img.SetSelfGray(true);
|
|
|
|
|
|
|
|
|
|
|
|
// 清空Sprite
|
|
|
|
|
|
img.HFClearSprite();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.4 SFUI_ScrollRect组件(列表滚动)
|
|
|
|
|
|
|
2026-05-22 15:46:01 +08:00
|
|
|
|
SFUI_ScrollRect 专门用于处理多物品的上下滑动列表,**内置对象池机制实现循环列表**,提供高效的列表渲染和滚动管理。
|
2026-05-22 15:23:11 +08:00
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
SFUI_ScrollRect scroll = GetComponent<SFUI_ScrollRect>();
|
|
|
|
|
|
|
|
|
|
|
|
// 绑定列表数据(核心方法)
|
|
|
|
|
|
scroll.BindEvent(
|
|
|
|
|
|
() => itemList.Count, // 数据数量
|
|
|
|
|
|
(idx) => itemPrefab.gameObject, // 获取模板对象
|
|
|
|
|
|
OnUpdateItem, // 更新回调
|
|
|
|
|
|
(item) => item.gameObject.HFGetOrAddSimulateBehaviour<ItemSlot>() // 获取组件
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 刷新列表显示
|
|
|
|
|
|
scroll.ShowView();
|
|
|
|
|
|
|
|
|
|
|
|
// 滚动到指定位置
|
|
|
|
|
|
scroll.ScrollToIndex(0); // 滚动到第N项
|
|
|
|
|
|
scroll.verticalNormalizedPosition = 0; // 滚动到底部(1=顶部,0=底部)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-22 15:46:01 +08:00
|
|
|
|
**BindEvent 参数说明**:
|
|
|
|
|
|
|
|
|
|
|
|
| 参数 | 类型 | 说明 |
|
|
|
|
|
|
|-----|------|------|
|
|
|
|
|
|
| 1 | `Func<int>` | 返回数据总数 |
|
|
|
|
|
|
| 2 | `Func<int, GameObject>` | 根据索引返回模板对象 |
|
|
|
|
|
|
| 3 | `Action<SFUI_ScrollRectItem>` | 列表项更新回调 |
|
|
|
|
|
|
| 4 | `Func<SFUI_ScrollRectItem, AbsHFUIWindow>` | 获取列表项组件 |
|
|
|
|
|
|
|
|
|
|
|
|
**更新回调示例**:
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
private void OnUpdateItem(SFUI_ScrollRectItem _item)
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemSlot slot = _item.simulateBehaviour as ItemSlot;
|
|
|
|
|
|
ItemData data = itemList[_item.dataIdx];
|
|
|
|
|
|
slot.SetData(data, OnItemClick);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-22 15:23:11 +08:00
|
|
|
|
**使用流程**:
|
|
|
|
|
|
1. 在 `OnRunAwake` 中调用 `BindEvent` 绑定数据
|
|
|
|
|
|
2. 在 `OnOpen` 或数据更新时调用 `ShowView` 刷新显示
|
|
|
|
|
|
3. 通过 `ScrollToIndex` 或 `verticalNormalizedPosition` 控制滚动位置
|
|
|
|
|
|
|
2026-05-22 15:46:01 +08:00
|
|
|
|
> **注意**:SFUI_ScrollRect 已内置对象池机制,**无需额外使用 HFSimulateBehaviourPool** 实现循环列表。
|
|
|
|
|
|
|
2026-05-22 15:23:11 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 四、对象池模式
|
|
|
|
|
|
|
2026-05-22 15:46:01 +08:00
|
|
|
|
> **注意**:**循环滚动列表请使用 `SFUI_ScrollRect.BindEvent`**,HFSimulateBehaviourPool 适用于非滚动场景的对象复用。
|
|
|
|
|
|
|
2026-05-22 15:23:11 +08:00
|
|
|
|
### 4.1 对象池初始化
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
HFSimulateBehaviourPool<ItemSlot> m_ItemPool = null;
|
|
|
|
|
|
|
|
|
|
|
|
protected override void OnRunAwake()
|
|
|
|
|
|
{
|
|
|
|
|
|
base.OnRunAwake();
|
|
|
|
|
|
SFUI_Button itemPrefab = FindUICtrlByName<SFUI_Button>("ItemPrefab");
|
|
|
|
|
|
SFUI_Behaviour parentContainer = FindUICtrlByName<SFUI_Behaviour>("ParentContainer");
|
|
|
|
|
|
|
|
|
|
|
|
// 创建对象池
|
|
|
|
|
|
m_ItemPool = new HFSimulateBehaviourPool<ItemSlot>(
|
|
|
|
|
|
itemPrefab.gameObject, // 模板对象
|
|
|
|
|
|
parentContainer.gameObject // 父容器
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-22 15:46:01 +08:00
|
|
|
|
### 4.2 对象池使用(适用于固定数量场景)
|
2026-05-22 15:23:11 +08:00
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
private void RefreshItems()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 回收所有已使用对象
|
|
|
|
|
|
m_ItemPool.RecycUsingObject();
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历数据创建新对象
|
|
|
|
|
|
foreach (var data in itemDataList)
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemSlot slot = m_ItemPool.GetItem(out bool isNew);
|
|
|
|
|
|
slot.SetData(data, OnItemClick);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 15:46:01 +08:00
|
|
|
|
// 适用于固定数量的装备栏、技能栏等场景
|
|
|
|
|
|
private void ShowEquipedCards()
|
2026-05-22 15:23:11 +08:00
|
|
|
|
{
|
2026-05-22 15:46:01 +08:00
|
|
|
|
m_CardItems.RecycUsingObject();
|
|
|
|
|
|
for (int i = 0; i < 5; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var item = m_CardItems.GetItem(out bool isNew);
|
|
|
|
|
|
// 设置数据...
|
|
|
|
|
|
}
|
2026-05-22 15:23:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-05-22 15:46:01 +08:00
|
|
|
|
### 4.3 适用场景对比
|
|
|
|
|
|
|
|
|
|
|
|
| 场景 | 推荐方式 | 说明 |
|
|
|
|
|
|
|-----|---------|------|
|
|
|
|
|
|
| **滚动列表** | `SFUI_ScrollRect.BindEvent` | 自动实现循环复用 |
|
|
|
|
|
|
| **固定数量栏** | `HFSimulateBehaviourPool` | 如装备栏、技能栏(5个固定位置) |
|
|
|
|
|
|
| **动态弹窗** | `HFSimulateBehaviourPool` | 需要频繁创建销毁的弹窗 |
|
|
|
|
|
|
|
2026-05-22 15:23:11 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 五、子组件开发
|
|
|
|
|
|
|
|
|
|
|
|
子组件作为列表项使用,继承 `AbsHFUIWindow`,核心特点:
|
|
|
|
|
|
|
|
|
|
|
|
| 特点 | 说明 |
|
|
|
|
|
|
|-----|------|
|
|
|
|
|
|
| **数据传递** | 通过 `SetData` 方法接收数据 |
|
|
|
|
|
|
| **回调机制** | 通过 `Action<T>` 与父组件通信 |
|
|
|
|
|
|
| **对象池** | 配合 `HFSimulateBehaviourPool<T>` 实现复用 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 六、UI开发规范
|
|
|
|
|
|
|
|
|
|
|
|
### 6.1 文件放置结构
|
|
|
|
|
|
|
|
|
|
|
|
UI相关文件应按照以下规范组织:
|
|
|
|
|
|
|
|
|
|
|
|
**脚本文件**(.cs):
|
|
|
|
|
|
```
|
|
|
|
|
|
Assets/Game/GameHFScripte/GameFunction/UIWindows/
|
|
|
|
|
|
├── BagWindow/
|
|
|
|
|
|
│ └── BagWindow.cs # 窗口脚本
|
|
|
|
|
|
├── MainWindow/
|
|
|
|
|
|
│ └── MainWindow.cs
|
|
|
|
|
|
└── Common/
|
|
|
|
|
|
└── ItemSlot.cs # 公共子组件脚本
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**预制体文件**(.prefab):
|
|
|
|
|
|
```
|
|
|
|
|
|
Assets/Game/GameHFResource/CN/UIWindows/
|
|
|
|
|
|
├── BagWindow/
|
|
|
|
|
|
│ └── BagWindow.prefab # 窗口预制体
|
|
|
|
|
|
├── MainWindow/
|
|
|
|
|
|
│ └── MainWindow.prefab
|
|
|
|
|
|
└── Common/
|
|
|
|
|
|
└── ItemSlot.prefab # 公共子组件预制体
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**图片资源**(按UI名称分类):
|
|
|
|
|
|
```
|
|
|
|
|
|
Assets/Game/GameHFResource/CN/SpriteAtlas/
|
|
|
|
|
|
├── BagWindow/ # 背包窗口相关图片
|
|
|
|
|
|
│ ├── Icon_Bag.png
|
|
|
|
|
|
│ └── Btn_Close.png
|
|
|
|
|
|
├── MainWindow/ # 主窗口相关图片
|
|
|
|
|
|
│ └── Btn_Menu.png
|
|
|
|
|
|
└── Common/ # 公共图片
|
|
|
|
|
|
└── Btn_Confirm.png
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 6.2 窗口属性配置
|
|
|
|
|
|
|
|
|
|
|
|
每个窗口必须配置以下属性:
|
|
|
|
|
|
|
|
|
|
|
|
```csharp
|
|
|
|
|
|
[HFAssetBundlePath(enHFLanguageRootFolder.ASSETS_GAME_GAMEHFRESOURCE,
|
|
|
|
|
|
"UIWindows/BagWindow/BagWindow.prefab")]
|
|
|
|
|
|
[HFUIWindowLayer(enHFUICanvasLayer.UIWindow,
|
|
|
|
|
|
enHFUIWindowLayer.Dynamic,
|
|
|
|
|
|
isIgnoreCloseByESC = false)]
|
|
|
|
|
|
public class BagWindow : AbsHFUIWindow
|
|
|
|
|
|
{
|
|
|
|
|
|
// ...
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**路径对应关系**:
|
|
|
|
|
|
- 属性路径:`UIWindows/BagWindow/BagWindow.prefab`
|
|
|
|
|
|
- 实际路径:`Assets/Game/GameHFResource/CN/UIWindows/BagWindow/BagWindow.prefab`
|
|
|
|
|
|
|
|
|
|
|
|
**属性说明**:
|
|
|
|
|
|
|
|
|
|
|
|
| 属性 | 作用 | 必填 |
|
|
|
|
|
|
|-----|------|------|
|
|
|
|
|
|
| `HFAssetBundlePath` | 指定预制体路径,用于资源加载 | ✅ |
|
|
|
|
|
|
| `HFUIWindowLayer` | 指定窗口层级和行为 | ✅ |
|
|
|
|
|
|
| `isIgnoreCloseByESC` | 是否忽略ESC关闭 | ❌ |
|
|
|
|
|
|
|
|
|
|
|
|
### 6.3 性能优化建议
|
|
|
|
|
|
|
|
|
|
|
|
| 优化项 | 说明 | 实现方式 |
|
|
|
|
|
|
|-------|------|---------|
|
|
|
|
|
|
| **对象池复用** | 频繁创建的UI元素使用对象池 | 使用 `HFSimulateBehaviourPool<T>` |
|
|
|
|
|
|
| **图集打包** | 将小图标打包到SpriteAtlas | 在Unity中配置SpriteAtlas |
|
|
|
|
|
|
| **层级优化** | 合理设置Canvas层级 | 使用 `HFUIWindowLayer` 属性 |
|
|
|
|
|
|
| **避免过度绘制** | 减少重叠UI元素 | 合理设计UI布局 |
|
|
|
|
|
|
| **批量操作** | 使用RectTransform批量修改 | 禁用后批量修改再启用 |
|
|
|
|
|
|
| **列表优化** | 使用SFUI_ScrollRect绑定模式 | 自动复用列表项 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
**版本**: v1.0
|
|
|
|
|
|
**适用范围**: StrayFog框架项目业务开发人员
|