第三章:UI 系统架构拆解与动态界面管理实录

还记得我们第二章刚跑通主场景,那时候是不是觉得"终于见到界面了"?但请等等,你看到的只是冰山一角,下面藏着的是 UIManager 的地狱之门。

本章我们将深入探讨:

  • UI 界面如何加载(Prefab 动态加载机制)

  • UIManager 的职责划分与扩展方式

  • 多层级弹窗的实现与交互逻辑

  • UI 缓存机制与复用策略

  • 动态绑定、异步初始化、点击穿透等实际开发坑

我们以 HallScene 为例,逐步分析主界面 UI 加载的全过程,并提供完整源码结构与实现方式。


一、界面加载的核心流程

在项目主场景中,大部分主界面是由 UIManager 控制加载的:

复制代码
UIManager.showUI("UIMainHall", () => {
    console.log("大厅主界面加载完成");
});

UIManager 的职责就是维护一套 UI 栈,并且支持弹窗控制、界面缓存与卸载逻辑。

核心结构如下:

复制代码
const UIManager = {
    uiStack: [],             // 当前打开的UI界面栈
    uiCache: {},             // 缓存的 prefab 实例
    uiRoot: null,            // 根节点容器

    init(rootNode) {
        this.uiRoot = rootNode;
    },

    showUI(name, callback) {
        if (this.uiCache[name]) {
            this._activateUI(name);
            callback && callback();
            return;
        }

        cc.loader.loadRes("ui/" + name, cc.Prefab, (err, prefab) => {
            if (err) {
                console.error("加载UI失败", name, err);
                return;
            }
            let uiNode = cc.instantiate(prefab);
            this.uiRoot.addChild(uiNode);
            this.uiCache[name] = uiNode;
            this.uiStack.push(name);
            callback && callback();
        });
    },

    closeUI(name) {
        let uiNode = this.uiCache[name];
        if (uiNode) {
            uiNode.removeFromParent();
            delete this.uiCache[name];
            this.uiStack = this.uiStack.filter(n => n !== name);
        }
    },

    _activateUI(name) {
        let uiNode = this.uiCache[name];
        if (uiNode) uiNode.active = true;
    }
};

二、UI分层机制(防穿透、防混乱)

在复杂场景中,一定要把 UI 分层,典型分为:

  • Scene 层(常驻 UI)

  • Window 层(普通弹窗)

  • Modal 层(模态遮罩)

  • Tips 层(Toast/消息)

初始化时结构如下:

复制代码
this.uiRoot = new cc.Node("UIRoot");
this.uiRoot.addChild(new cc.Node("SceneLayer"));
this.uiRoot.addChild(new cc.Node("WindowLayer"));
this.uiRoot.addChild(new cc.Node("ModalLayer"));
this.uiRoot.addChild(new cc.Node("TipsLayer"));

每次加载界面都要指定其层级:

复制代码
let targetLayer = this.uiRoot.getChildByName("WindowLayer");
targetLayer.addChild(uiNode);

三、界面复用与缓存优化

为什么缓存?

  • 动态加载太慢,影响体验

  • 热更更新资源时 prefab 不变

  • 弹窗频繁使用(如提示框、设置面板)

如何判断是否可复用?

  • 无状态类 UI(如提示类、头像框)建议复用

  • 强状态类 UI(如创建房间、匹配中)建议销毁后重建

示例:

复制代码
if (!this.uiCache["UITip"]) {
    let prefab = await cc.resources.load("ui/UITip", cc.Prefab);
    let node = cc.instantiate(prefab);
    this.uiRoot.addChild(node);
    this.uiCache["UITip"] = node;
}

四、常见 UI 问题与调试技巧

问题 1:按钮无效点击

检查:

  • Button 是否启用 interactable?

  • 是否被透明遮罩挡住?

  • 节点是否 active=false?

    buttonNode.getComponent(cc.Button).interactable = true;

问题 2:穿透点击

解决方式:添加透明节点吸收事件:

复制代码
let blocker = new cc.Node("Blocker");
let comp = blocker.addComponent(cc.BlockInputEvents);

问题 3:切换场景后 UI 丢失

  • 确保 UIRoot 为常驻节点:

    cc.game.addPersistRootNode(this.uiRoot);


五、UI 动画与协程处理

所有动画建议统一管理,防止资源释放冲突。

复制代码
async showPopup(node) {
    node.opacity = 0;
    node.scale = 0.5;
    cc.tween(node)
        .to(0.2, { opacity: 255, scale: 1.0 }, { easing: 'backOut' })
        .start();
}

延时关闭的协程动画:

复制代码
async hideWithDelay(node, delay) {
    await this.wait(delay);
    cc.tween(node)
        .to(0.2, { scale: 0.3, opacity: 0 })
        .call(() => node.removeFromParent())
        .start();
}

小结

这一章我们重点拆解了 UI 系统:

  • 界面加载流程

  • 管理器封装方式

  • 弹窗管理、分层、点击处理

  • 动画、异步控制与缓存机制

相关推荐
I'm Jie12 小时前
Swagger UI 本地化部署,解决 FastAPI Swagger UI 依赖外部 CDN 加载失败问题
python·ui·fastapi·swagger·swagger ui
爱学习的程序媛13 小时前
【Web前端】优化Core Web Vitals提升用户体验
前端·ui·web·ux·用户体验
爱学习的程序媛13 小时前
【Web前端】前端用户体验优化全攻略
前端·ui·交互·web·ux·用户体验
紫丁香13 小时前
Selenium自动化测试详解1
python·selenium·测试工具·ui
GISer_Jing14 小时前
前端组件库——shadcn/ui:轻量、自由、可拥有,解锁前端组件库的AI时代未来
前端·人工智能·ui
rjc_lihui17 小时前
IntelliSense: 无法打开 源 文件 “ui_mainwindow.h“ demo\qtdemosrc\mainwindow
ui
老星*1 天前
Lucide Icons:开源、轻量、设计师友好的现代图标库
ui·开源·github
Swift社区1 天前
AI 驱动 UI:鸿蒙 ArkUI 的新可能
人工智能·ui·harmonyos
Feng-licong2 天前
告别手写 UI:当 Google Stitch 遇上 Flutter,2026 年的“Vibe Coding”开发流
flutter·ui
一字白首2 天前
微信小程序进阶实战:从 UI 组件库到全局状态管理全解DAY05
ui·微信小程序·小程序