Files
rougelike-demo/client/assets/scripts/Framework/UI/README.md

448 lines
10 KiB
Markdown
Raw Normal View History

2025-12-14 22:40:38 +08:00
# UI 系统 (Framework/UI)
## 📋 模块概述
统一的 UI 管理系统,管理 UI 的加载、显示、隐藏和生命周期,提供 UI 基类和管理器,支持缓存和自动资源管理。
## 🎯 核心特性
- ✅ UI 基类定义
- ✅ UI 生命周期管理
- ✅ UI 缓存机制
- ✅ 自动资源管理
- ✅ UI 更新机制
- ✅ 类型安全
## 🗂️ 文件结构
```
Framework/UI/
├── UIBase.ts # UI 基类
├── UIMgr.ts # UI 管理器
└── UIMgrExample.ts # 使用示例
```
## 📘 核心类详解
### UIBase - UI 基类
**职责**: 定义 UI 的基础接口和生命周期
```typescript
abstract class UIBase {
protected _node: Node | null; // UI 根节点
protected _isLoaded: boolean; // 是否已加载
protected _isShowing: boolean; // 是否显示中
// 必须重载: 获取 UI 资源路径
abstract onGetUrl(): string;
// 可选重载: UI 初始化(加载完成后)
onStart?(params?: any): void | Promise<void>;
// 可选重载: UI 清理(卸载前)
onEnd?(): void;
// 可选重载: 每帧更新
onUpdate?(dt: number): void;
// 设置 UI 根节点
setNode(node: Node): void;
// 获取 UI 根节点
getNode(): Node | null;
// 显示 UI
show(): void;
// 隐藏 UI
hide(): void;
// 检查是否显示中
isShowing(): boolean;
// 检查是否已加载
isLoaded(): boolean;
}
```
### UIMgr - UI 管理器
**职责**: 管理 UI 的加载、卸载和生命周期
```typescript
class UIMgr {
private _uiCache: Map<string, UIBase>; // UI 缓存
private _uiRoot: Node | null; // UI 根节点
private _updateList: UIBase[]; // 需要更新的 UI 列表
// 获取单例
static getInstance(): UIMgr;
// 设置 UI 根节点(Canvas)
setUIRoot(root: Node): void;
// 加载并显示 UI
async load<T extends UIBase>(
uiClass: new () => T,
params?: any
): Promise<T>;
// 卸载 UI
unload(uiClass: (new () => UIBase) | string): void;
// 获取已加载的 UI 实例
get<T extends UIBase>(uiClass: (new () => T) | string): T | null;
// 检查 UI 是否已加载
has(uiClass: (new () => UIBase) | string): boolean;
// 更新所有需要更新的 UI
update(dt: number): void;
// 卸载所有 UI
unloadAll(): void;
// 销毁管理器
destroy(): void;
}
```
## 📝 使用指南
### 1. 创建自定义 UI
```typescript
import { _decorator } from 'cc';
import { UIBase } from './Framework/UI/UIBase';
const { ccclass } = _decorator;
@ccclass('UILogin')
export class UILogin extends UIBase {
private _btnLogin: Node | null = null;
private _inputAccount: EditBox | null = null;
/**
* 必须重载: 返回 UI 资源路径
*/
onGetUrl(): string {
return 'prefabs/ui/UILogin';
}
/**
* 可选重载: UI 初始化
*/
async onStart(params?: any): Promise<void> {
console.log('[UILogin] 初始化', params);
// 获取节点
const node = this.getNode();
if (node) {
this._btnLogin = node.getChildByName('BtnLogin');
this._inputAccount = node.getChildByName('InputAccount')
?.getComponent(EditBox) || null;
}
// 绑定事件
if (this._btnLogin) {
this._btnLogin.on(Node.EventType.TOUCH_END, this.onClickLogin, this);
}
}
/**
* 可选重载: UI 清理
*/
onEnd(): void {
console.log('[UILogin] 清理');
// 解绑事件
if (this._btnLogin) {
this._btnLogin.off(Node.EventType.TOUCH_END, this.onClickLogin, this);
}
}
/**
* 可选重载: 每帧更新
*/
onUpdate(dt: number): void {
// 更新倒计时等逻辑
}
private onClickLogin(): void {
console.log('[UILogin] 点击登录');
// 处理登录逻辑
}
}
```
### 2. 使用 UIMgr 管理 UI
```typescript
import { find } from 'cc';
import { UIMgr } from './Framework/UI/UIMgr';
import { UILogin } from './UI/UILogin';
// 1. 设置 UI 根节点(通常在启动时设置一次)
const canvas = find('Canvas');
if (canvas) {
UIMgr.getInstance().setUIRoot(canvas);
}
// 2. 加载并显示 UI
const loginUI = await UIMgr.getInstance().load(UILogin);
// 3. 传递参数给 UI
const loginUI = await UIMgr.getInstance().load(UILogin, {
username: 'test'
});
// 4. 获取已加载的 UI 实例
const ui = UIMgr.getInstance().get(UILogin);
if (ui) {
ui.hide(); // 隐藏
ui.show(); // 显示
}
// 5. 检查 UI 是否已加载
if (UIMgr.getInstance().has(UILogin)) {
console.log('UILogin 已加载');
}
// 6. 卸载 UI
UIMgr.getInstance().unload(UILogin);
// 或使用类名
UIMgr.getInstance().unload('UILogin');
// 7. 卸载所有 UI(场景切换时)
UIMgr.getInstance().unloadAll();
```
### 3. 在游戏主循环中更新 UI
```typescript
import { Component, _decorator } from 'cc';
import { UIMgr } from './Framework/UI/UIMgr';
const { ccclass } = _decorator;
@ccclass('GameManager')
export class GameManager extends Component {
update(deltaTime: number) {
// 更新所有 UI
UIMgr.getInstance().update(deltaTime);
}
}
```
### 4. UI 状态切换示例
```typescript
// UI 管理器封装
class UIController {
async showLogin(): Promise<void> {
// 隐藏其他 UI
UIMgr.getInstance().unload(UIMain);
// 显示登录 UI
await UIMgr.getInstance().load(UILogin);
}
async showMain(): Promise<void> {
// 隐藏登录 UI
UIMgr.getInstance().unload(UILogin);
// 显示主界面
await UIMgr.getInstance().load(UIMain);
}
async showBattle(): Promise<void> {
// 主界面保持显示,加载战斗 UI
await UIMgr.getInstance().load(UIBattle);
}
}
```
## 🔄 UI 加载流程
```
UIMgr.load(UIClass, params)
1. 检查缓存
├─ 如果已缓存
│ ├─ 调用 ui.show()
│ ├─ 调用 ui.onStart(params)
│ └─ 返回缓存实例
└─ 如果未缓存
2. 创建 UI 实例
3. 调用 ui.onGetUrl() 获取资源路径
4. 通过 ResMgr 加载预制体
5. 实例化预制体节点
6. 添加到 UI 根节点
7. 调用 ui.setNode(node)
8. 缓存 UI 实例
9. 如果有 onUpdate,添加到更新列表
10. 调用 ui.onStart(params)
11. 调用 ui.show()
12. 返回 UI 实例
```
## 📊 UI 生命周期
```
[创建 UI 类实例]
onGetUrl() - 获取资源路径(必须)
[加载预制体]
setNode(node) - 设置节点
onStart(params) - 初始化 UI(可选)
show() - 显示 UI
onUpdate(dt) - 每帧更新(可选)
[卸载 UI]
onEnd() - 清理资源(可选)
[销毁节点和释放资源]
```
## ⚠️ 注意事项
1. **必须重载 onGetUrl()**: 每个 UI 必须明确定义资源路径
2. **UI 根节点设置**: 在加载任何 UI 之前,必须先设置 UI 根节点
3. **缓存机制**: 已加载的 UI 会被缓存,再次加载时直接使用缓存
4. **资源自动管理**: UIMgr 会自动管理资源加载和释放
5. **异步加载**: load 方法是异步的,需要使用 await
6. **类型安全**: 使用泛型确保返回正确的 UI 类型
## 🔍 调试技巧
### 日志输出
```typescript
// UIMgr 内部包含详细日志:
// [UIMgr] 设置UI根节点
// [UIMgr] 开始加载UI: UILogin
// [UIMgr] 从缓存加载UI: UILogin
// [UIMgr] UI加载完成: UILogin
// [UIMgr] 卸载UI: UILogin
```
### 检查 UI 状态
```typescript
const ui = UIMgr.getInstance().get(UILogin);
if (ui) {
console.log(`UI已加载: ${ui.isLoaded()}`);
console.log(`UI显示中: ${ui.isShowing()}`);
}
```
### 常见问题
**问题1**: UI 根节点未设置
```typescript
// 解决: 在加载 UI 前设置根节点
const canvas = find('Canvas');
if (canvas) {
UIMgr.getInstance().setUIRoot(canvas);
}
```
**问题2**: UI 未正确显示
```typescript
// 检查: 资源路径是否正确
onGetUrl(): string {
return 'prefabs/ui/UILogin'; // 确保路径正确
}
```
**问题3**: UI 重复加载
```typescript
// 利用缓存机制:
if (!UIMgr.getInstance().has(UILogin)) {
await UIMgr.getInstance().load(UILogin);
}
```
## 💡 最佳实践
1. **单一职责**: 每个 UI 类只负责一个界面
2. **资源路径统一**: 建议在配置文件中统一管理 UI 资源路径
3. **事件解绑**: 在 onEnd() 中解绑所有事件,避免内存泄漏
4. **参数传递**: 使用 params 参数在加载时传递初始数据
5. **缓存利用**: 对频繁切换的 UI,利用缓存避免重复加载
6. **及时卸载**: 不再使用的 UI 及时卸载,释放资源
## 🎯 应用场景
- ✅ 登录界面
- ✅ 主界面
- ✅ 战斗界面
- ✅ 设置界面
- ✅ 背包界面
- ✅ 商店界面
- ✅ 对话框
- ✅ 提示框
## 📚 扩展功能
### UI 层级管理
```typescript
// 可以扩展 UIMgr 支持 UI 层级
enum UILayer {
Background = 0, // 背景层
Normal = 100, // 普通层
Popup = 200, // 弹窗层
Top = 300 // 顶层
}
// 在加载 UI 时指定层级
ui.getNode()?.setSiblingIndex(UILayer.Popup);
```
### UI 动画
```typescript
// 在 onStart 中添加打开动画
async onStart(): Promise<void> {
const node = this.getNode();
if (node) {
// 缩放动画
node.scale = Vec3.ZERO;
tween(node)
.to(0.3, { scale: Vec3.ONE }, { easing: 'backOut' })
.start();
}
}
// 在关闭时添加动画
async close(): Promise<void> {
const node = this.getNode();
if (node) {
await new Promise(resolve => {
tween(node)
.to(0.2, { scale: Vec3.ZERO })
.call(resolve)
.start();
});
}
UIMgr.getInstance().unload(this.constructor as any);
}
```