# 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; // 可选重载: 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; // UI 缓存 private _uiRoot: Node | null; // UI 根节点 private _updateList: UIBase[]; // 需要更新的 UI 列表 // 获取单例 static getInstance(): UIMgr; // 设置 UI 根节点(Canvas) setUIRoot(root: Node): void; // 加载并显示 UI async load( uiClass: new () => T, params?: any ): Promise; // 卸载 UI unload(uiClass: (new () => UIBase) | string): void; // 获取已加载的 UI 实例 get(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 { 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 { // 隐藏其他 UI UIMgr.getInstance().unload(UIMain); // 显示登录 UI await UIMgr.getInstance().load(UILogin); } async showMain(): Promise { // 隐藏登录 UI UIMgr.getInstance().unload(UILogin); // 显示主界面 await UIMgr.getInstance().load(UIMain); } async showBattle(): Promise { // 主界面保持显示,加载战斗 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 { const node = this.getNode(); if (node) { // 缩放动画 node.scale = Vec3.ZERO; tween(node) .to(0.3, { scale: Vec3.ONE }, { easing: 'backOut' }) .start(); } } // 在关闭时添加动画 async close(): Promise { 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); } ```