Files
rougelike-demo/client/assets/scripts/Framework/UI/README.md
2025-12-14 23:35:54 +08:00

11 KiB

UI 系统 (Framework/UI)

📋 模块概述

统一的 UI 管理系统,管理 UI 的加载、显示、隐藏和生命周期,提供 UI 基类和管理器,支持缓存和自动资源管理。

🎯 核心特性

  • UI 基类定义
  • UI 生命周期管理
  • UI 缓存机制
  • 自动资源管理
  • UI 更新机制
  • 类型安全

🗂️ 文件结构

Framework/UI/
├── UIBase.ts         # UI 基类
├── UIMgr.ts          # UI 管理器
└── UIMgrExample.ts   # 使用示例

📘 核心类详解

UIBase - UI 基类

职责: 定义 UI 的基础接口和生命周期

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 的加载、卸载和生命周期

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

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 资源路径
     * 
     * 编写规则:
     * 1. 普通资源路径: 'prefabs/ui/UILogin'
     * 2. Bundle资源路径: '[bundle名]://资源路径'
     *    示例: 'ui-bundle://prefabs/UILogin'
     */
    onGetUrl(): string {
        return 'prefabs/ui/UILogin';
        // 或使用 bundle 方式:
        // return 'ui-bundle://prefabs/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

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

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 状态切换示例

// 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 必须明确定义资源路径
    • 普通资源路径格式: 'prefabs/ui/UILogin'
    • Bundle资源路径格式: '[bundle名]://资源路径' (例如: 'ui-bundle://prefabs/UILogin')
  2. UI 根节点设置: 在加载任何 UI 之前,必须先设置 UI 根节点
  3. 缓存机制: 已加载的 UI 会被缓存,再次加载时直接使用缓存
  4. 资源自动管理: UIMgr 会自动管理资源加载和释放
  5. 异步加载: load 方法是异步的,需要使用 await
  6. 类型安全: 使用泛型确保返回正确的 UI 类型

🔍 调试技巧

日志输出

// UIMgr 内部包含详细日志:
// [UIMgr] 设置UI根节点
// [UIMgr] 开始加载UI: UILogin
// [UIMgr] 从缓存加载UI: UILogin
// [UIMgr] UI加载完成: UILogin
// [UIMgr] 卸载UI: UILogin

检查 UI 状态

const ui = UIMgr.getInstance().get(UILogin);
if (ui) {
    console.log(`UI已加载: ${ui.isLoaded()}`);
    console.log(`UI显示中: ${ui.isShowing()}`);
}

常见问题

问题1: UI 根节点未设置

// 解决: 在加载 UI 前设置根节点
const canvas = find('Canvas');
if (canvas) {
    UIMgr.getInstance().setUIRoot(canvas);
}

问题2: UI 未正确显示

// 检查: 资源路径是否正确
onGetUrl(): string {
    // 普通资源路径
    return 'prefabs/ui/UILogin';
    
    // 或使用 Bundle 资源路径
    // return 'ui-bundle://prefabs/UILogin';
}

问题3: UI 重复加载

// 利用缓存机制:
if (!UIMgr.getInstance().has(UILogin)) {
    await UIMgr.getInstance().load(UILogin);
}

💡 最佳实践

  1. 单一职责: 每个 UI 类只负责一个界面
  2. 资源路径统一: 建议在配置文件中统一管理 UI 资源路径
    • 对于需要分包加载的 UI,使用 Bundle 路径格式: '[bundle名]://资源路径'
    • 对于通用 UI,使用普通路径格式: '资源路径'
  3. 事件解绑: 在 onEnd() 中解绑所有事件,避免内存泄漏
  4. 参数传递: 使用 params 参数在加载时传递初始数据
  5. 缓存利用: 对频繁切换的 UI,利用缓存避免重复加载
  6. 及时卸载: 不再使用的 UI 及时卸载,释放资源

🎯 应用场景

  • 登录界面
  • 主界面
  • 战斗界面
  • 设置界面
  • 背包界面
  • 商店界面
  • 对话框
  • 提示框

📚 扩展功能

UI 层级管理

// 可以扩展 UIMgr 支持 UI 层级
enum UILayer {
    Background = 0,    // 背景层
    Normal = 100,      // 普通层
    Popup = 200,       // 弹窗层
    Top = 300          // 顶层
}

// 在加载 UI 时指定层级
ui.getNode()?.setSiblingIndex(UILayer.Popup);

UI 动画

// 在 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);
}