还记得我们第二章刚跑通主场景,那时候是不是觉得“终于见到界面了”?但请等等,你看到的只是冰山一角,下面藏着的是 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 系统:
界面加载流程
管理器封装方式
弹窗管理、分层、点击处理
动画、异步控制与缓存机制