448 lines
10 KiB
Markdown
448 lines
10 KiB
Markdown
|
|
# 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);
|
||
|
|
}
|
||
|
|
```
|