第三章: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 系统:

  • 界面加载流程

  • 管理器封装方式

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

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

相关推荐
UI设计兰亭妙微8 小时前
兰亭妙微|打破色彩对比度迷思:UI设计公司中的无障碍设计灵活之道
ui·b端界面设计·高端网站设计
轻口味9 小时前
HarmonyOS 6.1 全栈实战录 - 14 渲染树透镜:FrameNode 渲染状态感知与高性能 UI 调优实战
ui·华为·harmonyos
ZC跨境爬虫11 小时前
跟着 MDN 学CSS day_5:掌握属性选择器的存否匹配与子字符串匹配
前端·javascript·css·ui·html
ZC跨境爬虫12 小时前
模块化烹饪小程序开发日记 Day5:(后端Flask接口开发与AI智能解析菜谱的实现)
前端·人工智能·后端·python·ui·flask
薛定猫AI1 天前
【深度解析】Gemini Omni 多模态生成与 Agent 化创作工作流:从视频编辑到 UI 生成的技术演进
人工智能·ui·音视频
赏金术士1 天前
第七章:状态管理实战与架构总结
android·ui·kotlin·compose
幽络源小助理1 天前
全新UI 阅后即焚V2正式版系统源码_全开源_安全加密传输
安全·ui·开源·php源码
ZC跨境爬虫1 天前
跟着 MDN 学CSS day_2:(连接样式表与选择器的实战艺术)
java·前端·css·ui·html·媒体
ZC跨境爬虫1 天前
跟着 MDN 学CSS day_1:(CSS 基石与色彩的艺术)
前端·javascript·css·ui·html
前端若水2 天前
项目初始化:Vite + React + shadcn/ui
前端·react.js·ui