/** @format */ /** * UIManager界面管理类 * * 1.打开界面,根据配置自动加载界面、调用初始化、播放打开动画、隐藏其他界面、屏蔽下方界面点击 * 2.关闭界面,根据配置自动关闭界面、播放关闭动画、恢复其他界面 * 3.切换界面,与打开界面类似,但是是将当前栈顶的界面切换成新的界面(先关闭再打开) * 4.提供界面缓存功能 * */ import {BaseUI, UIShowTypes} from '../ui/BaseUI' import {IUIConfig, UICfg} from '../config/UICfg' import {ccUtils} from '../utils/ccUtils' import {UI} from '../enums/UI' import {Data, Mgr} from '../GameControl' import {Log} from '../utils/LogUtils' import {IOperateNeed} from '../interface/GlobalInterface' import {IMessage, IReward, IRewardNty, ItemUICfg} from '../interface/UIInterface' import {SOUND} from '../enums/Sound' /** UI栈结构体 */ export interface UIInfo { uiID: number baseUI: BaseUI uiArgs: any uiCfg: IUIConfig preventNode?: cc.Node aniNode?: cc.Node zOrder?: number isClose?: boolean hideTime?: number prefab?: cc.Prefab } export interface GroupUIInfo { uiID: number uiArgs?: any } export class UIManager { /** 是否正在关闭UI */ private isClosing = false /** 是否正在打开UI */ private isOpening = false /** UI界面缓存(key为UIId,value为BaseUI节点)*/ private UICache: {[UIId: number]: UIInfo} = {} /** UI界面栈({UIID + BaseUI + UIArgs}数组)*/ private UIStack: UIInfo[] = [] /** UI待打开列表 */ private UIOpenQueue: UIInfo[] = [] /** UI待关闭列表 */ private UICloseQueue: number[] = [] /** UI配置 */ private releaseTimeID: any /** 打开一组UI */ private groupUIs: GroupUIInfo[] = [] private curGroupUI: GroupUIInfo = null public prefabBundle: cc.AssetManager.Bundle = null /** UI打开前回调 */ public uiOpenBeforeDelegate: (uiID: number, preUIId: number) => void = null /** UI打开回调 */ public uiOpenDelegate: (uiID: number, preUIId: number) => void = null /** UI关闭回调 */ public uiCloseDelegate: (uiID: number) => void = null public async init() { if (this.releaseTimeID) clearInterval(this.releaseTimeID) this.releaseTimeID = setInterval(this.cleanUI.bind(this), 15000) Data.main.texBundle = await ccUtils.getBundleAsync('texture') this.prefabBundle = await ccUtils.getBundleAsync('prefab') //加载全局通用预制体 this.prefabBundle.loadDir('items', cc.Prefab, (error, assets: cc.Prefab[]) => { if (error) { Log.error(error) } else { for (let j = 0; j < assets.length; j++) { Data.main.itemsPrefabMap.set(assets[j].name, assets[j]) let pool = Data.main.itemsPoolMap.get(assets[j].name) if (!pool) { pool = new cc.NodePool() Data.main.itemsPoolMap.set(assets[j].name, pool) } } } }) return this.prefabBundle != null } /****************** 私有方法,UIManager内部的功能和基础规则 *******************/ /** * 添加防触摸层 * @param zOrder 屏蔽层的层级 */ private preventTouch(zOrder: number) { let node = new cc.Node() node.name = 'preventTouch' node.setContentSize(cc.winSize) node.on( cc.Node.EventType.TOUCH_START, (event: cc.Event.EventCustom) => { event.stopPropagation() }, node, ) let child = cc.director.getScene().getChildByName('Canvas') child.addChild(node, zOrder) return node } /** 自动执行下一个待关闭或待打开的界面 */ private autoExecNextUI() { // 逻辑上是先关后开 if (this.UICloseQueue.length > 0) { let closeUI = this.UICloseQueue[0] this.UICloseQueue.splice(0, 1) this.hide(closeUI) } else if (this.UIOpenQueue.length > 0) { let uiQueueInfo = this.UIOpenQueue[0] this.UIOpenQueue.splice(0, 1) this.show(uiQueueInfo.uiID, uiQueueInfo.uiArgs) } } /** * 自动检测动画组件以及特定动画,如存在则播放动画,无论动画是否播放,都执行回调 * @param aniName 动画名 * @param aniOverCallback 动画播放完成回调 */ private autoExecAnimation(baseUI: BaseUI, aniName: string, aniOverCallback: () => void) { // 暂时先省略动画播放的逻辑 let node = baseUI.node switch (aniName) { case 'uiOpen': if ( baseUI.showType >= UIShowTypes.UIAddition && ![UI.CurrencyUI, UI.LoadingUI, UI.TipUI, UI.PowerUpUI, UI.GameLoadingUI].includes(baseUI.uiID) ) { Mgr.audio.playSFX(SOUND.uiPop) node.setScale(0) node.opacity = 0 cc.tween(node) .to(0.3, {scale: 1.1}, {easing: 'backOut'}) // 缩放动作,0.3秒内从 0 缩放到 1,使用 backOut 缓动效果 .to(0.3, {opacity: 255}) // 渐显动作,0.1秒内从透明度 0 到 255 .union() .to(0.1, {scale: 1}) // 缩小一点,形成回弹效果 .call(aniOverCallback) .start() } else { aniOverCallback() } break case 'uiClose': aniOverCallback() break } } /** * 异步加载一个UI的prefab,成功加载了一个prefab之后 * @param uiID 界面id * @param processCallback 加载进度回调 * @param completeCallback 加载完成回调 * @param uiArgs 初始化参数 */ private getOrCreateUI( uiID: number, processCallback: any, completeCallback: (baseUI: BaseUI, prefab?: cc.Prefab) => void, uiArgs: any, ): void { // 如果找到缓存对象,则直接返回 let baseUI: BaseUI let cacheUIInfo = this.UICache[uiID] if (cacheUIInfo) { completeCallback(cacheUIInfo.baseUI) return } // 找到UI配置 let uiPath = UICfg.get(uiID).url if (null == uiPath) { cc.log(`getOrCreateUI ${uiID} faile, prefab conf not found!`) completeCallback(null) return } this.prefabBundle.load(uiPath, cc.Prefab, processCallback, (err: Error, prefab: cc.Prefab) => { // 检查加载资源错误 if (err) { cc.error(`getOrCreateUI loadRes ${uiID} faile, path: ${uiPath} error: ${err}`) completeCallback(null) return } // 检查实例化错误 let uiNode: cc.Node = cc.instantiate(prefab) if (null == uiNode) { cc.error(`getOrCreateUI instantiate ${uiID} faile, path: ${uiPath}`) completeCallback(null) cc.assetManager.releaseAsset(prefab) return } // 检查组件获取错误 baseUI = uiNode.getComponent(BaseUI) if (null == baseUI) { cc.error(`getOrCreateUI getComponent ${uiID} faile, path: ${uiPath}`) uiNode.destroy() completeCallback(null) cc.assetManager.releaseAsset(prefab) return } baseUI.uiID = uiID baseUI.init(uiArgs) completeCallback(baseUI, prefab) }) } /** * UI被打开时回调,对UI进行初始化设置,刷新其他界面的显示,并根据 * @param uiID 哪个界面被打开了 * @param baseUI 界面对象 * @param uiInfo 界面栈对应的信息结构 * @param uiArgs 界面初始化参数 */ private onUIOpen(uiID: number, baseUI: BaseUI, uiInfo: UIInfo, uiArgs: any) { if (null == baseUI) { return } // 激活界面 uiInfo.baseUI = baseUI baseUI.node.active = true let realZIndex = uiInfo.zOrder + baseUI.showType baseUI.node.zIndex = realZIndex if (baseUI.prevent) { let blockInputEvents = baseUI.node.getChildByName('blockInputEvents') if (!blockInputEvents) { blockInputEvents = new cc.Node() blockInputEvents.name = 'blockInputEvents' blockInputEvents.setContentSize(cc.winSize) blockInputEvents.addComponent(cc.BlockInputEvents) baseUI.node.addChild(blockInputEvents, -9999) } // 这里直接给node添加BlockInputEvents导致子节点setSwallowTouches无法吞噬,改成上面的实现方式 // baseUI.node.setContentSize(cc.winSize) // if (!baseUI.node.getComponent(cc.BlockInputEvents)) baseUI.node.addComponent(cc.BlockInputEvents) } // 快速关闭界面的设置,绑定界面中的background,实现快速关闭 if (baseUI.quickClose) { let backGround = baseUI.node.getChildByName('background') if (!backGround) { backGround = new cc.Node() backGround.name = 'background' backGround.setContentSize(cc.winSize) baseUI.node.addChild(backGround, -1) } backGround.targetOff(cc.Node.EventType.TOUCH_START) backGround.on( cc.Node.EventType.TOUCH_START, (event: cc.Event.EventCustom) => { event.stopPropagation() this.hideBaseUI(baseUI) }, backGround, ) } // 添加到场景中 let child = cc.director.getScene().getChildByName('Canvas') child.addChild(baseUI.node) // 从那个界面打开的 let fromUIID = 0 if (this.UIStack.length > 1) { fromUIID = this.UIStack[this.UIStack.length - 2].uiID } // 打开界面之前回调 if (this.uiOpenBeforeDelegate) { this.uiOpenBeforeDelegate(uiID, fromUIID) } // 执行onOpen回调 下一帧执行保证onLoad onEnable先执行 baseUI.scheduleOnce(() => { if (!uiInfo.isClose && baseUI.node.activeInHierarchy) baseUI.onShow(uiArgs, fromUIID) }) this.autoExecAnimation(baseUI, 'uiOpen', () => { baseUI.onOpenAniOver() if (this.uiOpenDelegate) { this.uiOpenDelegate(uiID, fromUIID) } }) } /** 打开界面并添加到界面栈中 */ public show(uiID: number, uiArgs: any = null, progressCallback: Function = null): void { let uiInfo: UIInfo = { uiCfg: UICfg.get(uiID), uiID: uiID, uiArgs: uiArgs, baseUI: null, } if (this.isOpening || this.isClosing) { // 插入待打开队列 let openIndex = this.UIOpenQueue.findIndex(item => item.uiID == uiID) if (openIndex >= 0) this.UIOpenQueue.splice(openIndex, 1) this.UIOpenQueue.push(uiInfo) return } let uiInfoTmp = this.getUIInfo(uiID) if (uiInfoTmp) { // 重复打开了同一个界面 this.hide(uiID) this.show(uiID, uiArgs) return } // 设置UI的zOrder uiInfo.zOrder = this.UIStack.length this.UIStack.push(uiInfo) // 先屏蔽点击 uiInfo.preventNode = this.preventTouch(10000) this.isOpening = true // 预加载资源,并在资源加载完成后自动打开界面 this.getOrCreateUI( uiID, progressCallback, (baseUI: BaseUI, prefab: cc.Prefab): void => { if (uiInfo.preventNode) { uiInfo.preventNode.destroy() uiInfo.preventNode = null } // 如果界面已经被关闭或创建失败 if (uiInfo.isClose || null == baseUI) { cc.log(`getOrCreateUI ${uiID} faile! close state : ${uiInfo.isClose} , baseUI : ${baseUI}`) this.isOpening = false return } if (prefab) uiInfo.prefab = prefab this.onUIOpen(uiID, baseUI, uiInfo, uiArgs) this.isOpening = false this.autoExecNextUI() }, uiArgs, ) } /** 显示一组UI */ showUIs(UIs: GroupUIInfo[]) { if (!UIs) return UIs = UIs.filter(value => value) if (UIs.length <= 0) return this.groupUIs = UIs this.curGroupUI = this.groupUIs.splice(0, 1)[0] this.show(this.curGroupUI.uiID, this.curGroupUI.uiArgs) } /** 替换栈顶界面 */ public replace(uiID: number, uiArgs: any = null) { this.hideBaseUI(this.UIStack[this.UIStack.length - 1].baseUI) this.show(uiID, uiArgs) } /** * 关闭当前界面 * @param closeUI 要关闭的界面 */ public hideBaseUI(closeUI?: BaseUI) { let uiCount = this.UIStack.length if (uiCount < 1 || this.isClosing || this.isOpening) { if (closeUI) { // 插入待关闭队列 let closeIndex = this.UICloseQueue.findIndex(v => v == closeUI.uiID) if (closeIndex >= 0) this.UICloseQueue.splice(closeIndex, 1) this.UICloseQueue.push(closeUI.uiID) } return } let uiInfo: UIInfo if (closeUI) { let isSplice = false for (let index = this.UIStack.length - 1; index >= 0; index--) { let ui = this.UIStack[index] if (ui.baseUI === closeUI) { uiInfo = ui isSplice = true this.UIStack.splice(index, 1) break } } if (isSplice) this.resetUIStackZIndex() // 找不到这个UI if (uiInfo === undefined) { let index = this.UIOpenQueue.findIndex(value => value.uiID == closeUI.uiID) if (index >= 0) { this.UIOpenQueue.splice(index, 1) } this.autoExecNextUI() return } } else { uiInfo = this.UIStack.pop() } // 关闭当前界面 let uiID = uiInfo.uiID let baseUI = uiInfo.baseUI uiInfo.isClose = true // 回收遮罩层 if (uiInfo.preventNode) { uiInfo.preventNode.destroy() uiInfo.preventNode = null } if (null == baseUI) { return } this.isClosing = true let preUIInfo = null for (let i = uiCount - 2; i >= 0; i--) { if (this.UIStack[i] && this.UIStack[i].baseUI.showType == baseUI.showType) { preUIInfo = this.UIStack[i] break } } let close = () => { this.isClosing = false baseUI.onHide() // 显示之前的界面 if (preUIInfo && preUIInfo.baseUI && preUIInfo.baseUI.node.activeInHierarchy) { // 回调onTop onTop只是当前层级生效 preUIInfo.baseUI.onTop(uiID) } if (this.uiCloseDelegate) { this.uiCloseDelegate(uiID) } this.releaseUI(uiInfo) this.autoExecNextUI() if (this.groupUIs.length > 0) { this.curGroupUI = this.groupUIs.splice(0, 1)[0] this.show(this.curGroupUI.uiID, this.curGroupUI.uiArgs) } } // 执行关闭动画 this.autoExecAnimation(baseUI, 'uiClose', close) } /** * 关闭当前界面 * @param uiID 要关闭的界面 */ public hide(uiID: UI) { let uiCount = this.UIStack.length if (uiCount < 1 || this.isClosing || this.isOpening) { if (uiID) { // 插入待关闭队列 const openIndex = this.UIOpenQueue.findIndex(v => v.uiID == uiID) if (openIndex != -1) { this.UIOpenQueue.splice(openIndex, 1) } else { this.UICloseQueue.push(uiID) } } return } if (this.getUI(uiID)) this.hideBaseUI(this.getUI(uiID)) } /** 关闭所有界面 */ public closeAll(remainUIs: UI[] = []) { // 不播放动画,也不清理缓存 let remainUIInfo = [] for (const uiInfo of this.UIStack) { if (remainUIs.includes(uiInfo.uiID)) { remainUIInfo.push(uiInfo) continue } uiInfo.isClose = true if (uiInfo.preventNode) { uiInfo.preventNode.destroy() uiInfo.preventNode = null } if (uiInfo.baseUI) { uiInfo.baseUI.onHide() //uiInfo.baseUI.decAllRef() this.releaseUI(uiInfo) } } this.UIOpenQueue = [] this.UICloseQueue = [] this.UIStack = remainUIInfo this.resetUIStackZIndex() this.isOpening = false this.isClosing = false this.curGroupUI = null this.groupUIs = [] } releaseUI(uiInfo: UIInfo) { let baseUI = uiInfo.baseUI baseUI.node.parent = null this.UICache[uiInfo.uiID] = uiInfo if (!baseUI.cache) { uiInfo.hideTime = Date.now() } } cleanUI() { let now = Date.now() for (let key in this.UICache) { let uiInfo = this.UICache[key] if (uiInfo) { let node = uiInfo.baseUI.node let hideTime = uiInfo.hideTime let prefab = uiInfo.prefab if (!node.parent && hideTime > 0 && now - hideTime > 15000 && !this.isClosing && !this.isOpening) { console.log('UIMgr releaseUI:', node.name) uiInfo.baseUI.decAllRef() node.destroy() cc.assetManager.releaseAsset(prefab) delete this.UICache[key] } } } } resetUIStackZIndex() { for (let i = 0; i < this.UIStack.length; i++) { let uiInfo = this.UIStack[i] uiInfo.zOrder = i uiInfo.baseUI.node.zIndex = i + uiInfo.baseUI.showType } } /******************** UI的便捷接口 *******************/ //可能存在当时UI正在加载node,没有baseUI,顶层不是那么精确 public isTopUI(uiID, isCurLayer: boolean = true): boolean { let curUIs = this.UIStack let uiInfo = curUIs.find(v => v.uiID == uiID) if (!uiInfo) return false if (uiInfo && isCurLayer) { curUIs = this.UIStack.filter(v => v.baseUI && uiInfo.baseUI && v.baseUI.showType == uiInfo.baseUI.showType) } return curUIs[curUIs.length - 1]?.uiID == uiID } public getUI(uiID: number): BaseUI { for (let index = 0; index < this.UIStack.length; index++) { const element = this.UIStack[index] if (uiID == element.uiID) { return element.baseUI } } return null } //返回uiID,可能存在当时UI正在加载node,没有baseUI public getTopUI(): number { if (this.UIStack.length > 0) { return this.UIStack[this.UIStack.length - 1].uiID } return null } public getUIInfo(uiID: number): UIInfo { for (let index = 0; index < this.UIStack.length; index++) { let element = this.UIStack[index] if (uiID == element.uiID) { return element } } return null } public getGroupUIInfo(uiID: number, args?: any): GroupUIInfo { return {uiID, uiArgs: args} } public getRewardGroupUIInfo(rewardNty: IRewardNty): GroupUIInfo | null { let hasReward = false for (let key in rewardNty) { hasReward ||= Array.isArray(rewardNty[key]) && rewardNty[key].length > 0 } if (hasReward) { let rewardArgs: IReward = {idNumArr: Mgr.goods.getGoodsListByRewardNty(rewardNty)} return {uiID: UI.RewardUI, uiArgs: rewardArgs} } else { return null } } public callOnShow(uiID: UI, args: any) { let uiInfo = this.getUIInfo(uiID) if (uiInfo && uiInfo.baseUI) { uiInfo.baseUI.onShow(args, null) } } /******************** 通用UI的操作接口 *******************/ public tip(text: string = '') { if (text == '') { return } this.hide(UI.TipUI) this.show(UI.TipUI, text) } /** * 显示网络请求等待响应屏蔽触摸的界面 * @param str 显示的文本 * @param isDelay 是否延时显示可见的屏蔽层,延时中为透明的屏蔽层 * @param cutNet 超时后是否重启游戏 */ public showLoading(str: string = '', isDelay: boolean = true, cutNet: boolean = false, timeOut: boolean = false) { this.show(UI.LoadingUI, {str, isDelay, cutNet, timeOut}) } /** * 隐藏网络请求等待响应屏蔽触摸的界面 */ public hideLoading() { this.hide(UI.LoadingUI) } public message(text: string = '', sureFunc: Function, isHideCancel: boolean = false, sureLb: string = '') { if (text == '') { return } let args: IMessage = { sureFuc: sureFunc, tip: text, isHideCancel, sureLb, } this.show(UI.MessageUI, args) } public fadeShow(uiID: number, uiArgs: any = null, inOrOut = true) { this.show(UI.FadeInOutUI, { inOrOut, aniFinishFuc: () => {}, }) if (uiID > 0) this.show(uiID, uiArgs) } public showReward(rewardNty: IRewardNty, itemUICfgArr?: ItemUICfg[]) { if (!rewardNty) return let hasReward = false for (let key in rewardNty) { hasReward ||= Array.isArray(rewardNty[key]) && rewardNty[key].length > 0 } if (hasReward) { let rewardArgs: IReward = {idNumArr: Mgr.goods.getGoodsListByRewardNty(rewardNty), itemUICfgArr} this.show(UI.RewardUI, rewardArgs) } } //传入needNum表示根据needGoods道具ID判断是否显示UI。不传值默认显示UI public showObtain(needGoods: number | IOperateNeed, needNum: number = 0): boolean { let operateNeed: IOperateNeed let showObtain = true if (typeof needGoods == 'number') { operateNeed = { canUp: false, isMax: false, need: [ { id: needGoods, num: Data.user.goods.get(needGoods) ? Data.user.goods.get(needGoods) : 0, need: needNum, }, ], } } else { operateNeed = needGoods } if (needNum > 0) { let hasNum = Data.user.goods.get(operateNeed.need[0]?.id) showObtain = hasNum < needNum } if (showObtain) this.show(UI.ObtainChannelUI, operateNeed) return showObtain } }