//********************* // create by 流云 // time: 2020.12.18 // desc: UI管理器 //********************* import { Scene, Node, UITransform, Camera, director, Widget, Layout, Label, Overflow, HorizontalTextAlignment, Layers, Director, Asset, AssetManager, Prefab, assetManager, instantiate, Tween, tween, v3, RichText, game, view, EventTouch, isValid, ResolutionPolicy, screen, Mask } from "cc"; import { OpenParam, UIBase } from "./UIBase"; import type { IPopAnimConfig } from './popAnim'; import { PopAnimPlayer } from './popAnim/PopAnimPlayer'; import type { IPopAnimService } from './popAnim/IPopAnimService'; import { BundleLifecycle } from '../res/BundleLifecycle'; import { LoadRecoveryConfig } from '../res/LoadRecovery'; import { LoadScheduler } from '../res/LoadScheduler'; import { createMembershipSet, membershipAdd, membershipDelete, mapKeysToArray, type MembershipSet, } from '../../tools/MembershipSet'; /** 局内 pauseBattle 面板开关钩子(由业务层注册,避免 mx→script 静态依赖) */ export type BattlePausePanelHook = (uiId: number, open: boolean) => void; /** Toast 出现基准高度(相对可见区域,约在角色名下方) */ const TOAST_BASE_Y_RATIO = 0.075; const TOAST_STACK_GAP = 50; /**UI信息 */ export class UIInfo { /**UIID */ id: number = 0; /**UI节点 */ uiNode: Node = null; /**UI组件 */ uiBase: UIBase = null; /**预制体资源 */ prefabRes: Prefab = null; /**UI配置 */ uiconf: IUIConfig = null; } /** * UI层级(新增层级在此添加枚举即可,层级按枚举顺序显示) */ export enum UILayer { /**2D游戏UI */ Game2D = `Game2D`, /**主UI层 */ MainUI = `MainUI`, /**菜单层 */ Menu = `Menu`, /**全屏面板层 */ Page = `Page`, /**弹窗页面 */ Pop = `Pop`, /**提示页面(模态对话框) */ Dalog = `Dalog`, /**动画层 */ Anim = `Anim`, /**引导页面 */ Guide = `Guide`, /**加载页面 */ Loading = `Loading`, /**调试层 */ DeBug = `DeBug`, /**广告页面 */ Ad = `Ad`, } /** 面板成功打开后钩子(含已打开再置顶),由业务层注册,避免 mx→script 静态依赖 */ export type PanelOpenedHook = (uiId: number, layer: UILayer) => void; /** * UI配置 */ export interface IUIConfig { /**UI描述 */ desc: string; /**UI层级 */ layer: UILayer; /**资源包名 */ bundle: string; /**资源路径 */ path: string; /**资源名称 */ name: string; /** * 局内打开时暂停战斗(引用计数:关完所有 pauseBattle 面板才恢复)。 * 新增需暂停的弹窗:在 ConfigRes.UIConfigs 对应项加 pauseBattle: true。 */ pauseBattle?: boolean; /** 弹窗入场/退场预设,配好后无需在 onInit 手写 showOpen* */ popAnim?: IPopAnimConfig; } /** * UI管理器 */ export class UIManager { private static _ins: UIManager = null; static get Ins() { if (!this._ins) this._ins = new UIManager(); return this._ins; } private readonly _popAnim: IPopAnimService = new PopAnimPlayer(); /** 弹窗入场/退场动效 */ get popAnim(): IPopAnimService { return this._popAnim; } /**宽度 */ get width() { return this._root ? this._root.getComponent(UITransform).width : view.getVisibleSize().width; } /**高度 */ get height() { return this._root ? this._root.getComponent(UITransform).height : view.getVisibleSize().height; } /**场景唯一相机 */ UICamera: Camera = null; /**是否动态适配屏幕(默认开启) */ isDynamicAdaptScreen: boolean = true; private _curent_scene: Scene = null; private _canvas: Node = null; private _root: Node = null; private _log: Node = null; private _timer_flag = null; private _layer_map: Map = new Map(); private _ui_map: Map = new Map(); /** 同 id 的 openPanel 异步加载未完成时,后续点击复用该 Promise,避免 uiBase 仍为 null 时写 prema */ private _panelOpenInflight: Map> = new Map(); /** 与 _panelOpenInflight 配对,失败恢复时必须 resolve,否则后续 openPanel 会永久等待 */ private _panelOpenResolve: Record void) | undefined> = {}; /** 打开代数:abort 后作废仍在 await load 的过期回调 */ private _openEpoch: Record = {}; /** 同 id 的 closePanel 进行中时复用 Promise,避免连点双 destroy / 双 decRef */ private _panelCloseInflight: Map> = new Map(); private _wait_popIds: { id: number; param?: OpenParam }[] = []; private _curent_pop: { id: number; param?: OpenParam } | null = null; /** 同 id 连开时只保留最后一次参数 */ private _latestOpenParam: Record = {}; /** 弹窗队列单工人 */ private _popRunning = false; /** 本管理器已 acquire 的 pauseBattle 面板 */ private _pauseHeld: MembershipSet = createMembershipSet(); private uiConfigs: { [key: number]: IUIConfig } = []; private _battlePauseHook: BattlePausePanelHook | null = null; private _panelOpenedHook: PanelOpenedHook | null = null; /** * 注册局内 pauseBattle 钩子(业务层在启动时调用一次)。 * open=true 打开面板;open=false 关闭面板。 */ setBattlePauseHook(hook: BattlePausePanelHook | null): void { this._battlePauseHook = hook; } /** * 注册面板打开钩子(业务层在战斗 UI 存活期间注册)。 * 任意 openPanel 成功展示后回调(含已打开再置顶)。 */ setPanelOpenedHook(hook: PanelOpenedHook | null): void { this._panelOpenedHook = hook; } private _notifyBattlePause(id: number, open: boolean): void { const conf = this.uiConfigs[id]; if (!conf?.pauseBattle) return; this._battlePauseHook?.(id, open); } private _notifyPanelOpened(id: number): void { const conf = this.uiConfigs[id]; if (!conf) return; this._panelOpenedHook?.(id, conf.layer); } /**初始化(切换场景的时候调用) */ init(config: { [key: number]: IUIConfig }, camera?: Camera, isDynamicAdaptScreen: boolean = true) { this.uiConfigs = config; this.UICamera = camera; this.isDynamicAdaptScreen = isDynamicAdaptScreen; this._curent_scene = director.getScene(); this._canvas = this._curent_scene.getChildByName(`Canvas`); if (this.isDynamicAdaptScreen) { let winsize = screen.windowSize; let ratio = winsize.width / winsize.height; let drs = view.getDesignResolutionSize(); let drsRatio = drs.width / drs.height; if (ratio > drsRatio) { view.setResolutionPolicy(ResolutionPolicy.FIXED_HEIGHT); } else { view.setResolutionPolicy(ResolutionPolicy.FIXED_WIDTH); } } //初始化UI根节点 this._root = this._canvas.getChildByName(`root`); if (!this._root) this._root = this.addUILayer(this._canvas, `root`); //初始化层级 for (let k in UILayer) { let v = UILayer[k]; this._layer_map.set(v, this.addUILayer(this._root, v)); } //初始化log层 this._log = this._root.getChildByName(`log`); if (!this._log) { this._log = new Node(`log`); this._root.addChild(this._log); let w = this._log.addComponent(Widget); w.isAlignLeft = w.isAlignTop = w.isAlignRight = w.isAlignBottom = true; w.left = w.top = w.right = w.bottom = 0; w.updateAlignment(); let ly = this._log.addComponent(Layout); ly.type = Layout.Type.VERTICAL; let dc = new Node("dc"); dc.layer = Layers.Enum.UI_2D; let lb = dc.addComponent(Label); lb.string = ""; lb.fontSize = 50; this._log.addChild(dc); lb.isBold = true; let l = new Node("log"); l.layer = Layers.Enum.UI_2D; l.addComponent(UITransform).width = 700; lb = l.addComponent(Label); lb.horizontalAlign = HorizontalTextAlignment.LEFT; lb.overflow = Overflow.RESIZE_HEIGHT; lb.string = ""; lb.fontSize = 30; this._log.addChild(l); } this._kickPopQueue(); // this._log.on(Node.EventType.TOUCH_START, (e: EventTouch) => { // GEvent.Ins.emit(GEvent.GlobalTouchStart, e); // }) // this._log.on(Node.EventType.TOUCH_MOVE, (e: EventTouch) => { // GEvent.Ins.emit(GEvent.GlobalTouchMove, e); // e.preventSwallow = true; // e.propagationStopped = true; // }) // this._log.on(Node.EventType.TOUCH_END, (e: EventTouch) => { // GEvent.Ins.emit(GEvent.GlobalTouchEnd, e); // e.preventSwallow = true; // e.propagationStopped = true; // }) // this._log.on(Node.EventType.TOUCH_CANCEL, (e: EventTouch) => { // GEvent.Ins.emit(GEvent.GlobalTouchCancel, e); // e.preventSwallow = true; // e.propagationStopped = true; // }) } /** * 设置底部距离 * @param value 游戏整个UI底边沿距离屏幕底边沿的设计分辨率像素距离 */ setBottomWidget(value) { let w = this._root.getComponent(Widget); w.bottom = value; } /** * 设置顶部距离 * @param value 游戏整个UI顶部边沿距离屏幕顶部边沿的设计分辨率像素距离 */ setTopWidget(value) { let w = this._root.getComponent(Widget); w.top = value; } /** * 显示一个节点到指定层级 * @param ui ui节点 * @param layer 层级 */ showNode(ui, layer: UILayer, index = -2) { this._layer_map.get(layer).addChild(ui); //index设置为0,确保在最底层 //index设置为-1,确保在最上层 //index设置为-2,不设置z-index if (index != -2) ui.setSiblingIndex(index); } /** * 打开单例页面 * @param id 页面ID * @param param 参数 */ async openPanel(id: number, param?: OpenParam): Promise { this._latestOpenParam[id] = param; const closeWait = this._panelCloseInflight.get(id); if (closeWait) { await closeWait; } const pending = this._panelOpenInflight.get(id); if (pending) { await pending; const latest = this._latestOpenParam[id]; if (this._ui_map.get(id)?.uiBase) { return this._applyToOpenPanel(id, latest); } // 前次打开失败:inflight 已清,必须重试,否则这次点击直接 false、体感「再也打不开」 console.warn('[UIManager] openPanel 前次打开未成功,重试', id); if (this._ui_map.has(id)) { this._ui_map.delete(id); } return this._openPanelOnce(id, latest); } if (this._ui_map.get(id)?.uiBase) { return this._applyToOpenPanel(id, param); } if (this._ui_map.has(id)) { this._ui_map.delete(id); } return this._openPanelOnce(id, param); } /** * 已打开面板套用最新参数并再走 onInit。 * @param id 面板 id * @param param 打开参数 */ private async _applyToOpenPanel(id: number, param?: OpenParam): Promise { const uiInfo = this._ui_map.get(id); if (!uiInfo?.uiBase) { console.error('[UIManager] applyToOpenPanel 面板未打开', id); return false; } uiInfo.uiBase.applyOpenParam(param); uiInfo.uiBase.beforePanelInit(); await Promise.resolve(uiInfo.uiBase.onInit()); if (!uiInfo.uiBase?.isValid) { console.error('[UIManager] applyToOpenPanel 期间面板已销毁', id); return false; } uiInfo.uiNode.setSiblingIndex(-1); this._holdPause(id); this._notifyPanelOpened(id); return true; } /** * 首次实例化打开。 * @param id 面板 id * @param param 打开参数 */ private async _openPanelOnce(id: number, param?: OpenParam): Promise { const conf = this.uiConfigs[id]; if (!conf) { console.error('[UIManager] openPanel missing config', id); return false; } const uiInfo = new UIInfo(); uiInfo.id = id; uiInfo.uiconf = conf; this._ui_map.set(id, uiInfo); const epoch = (this._openEpoch[id] ?? 0) + 1; this._openEpoch[id] = epoch; let resolveInflight: () => void; const inflightPromise = new Promise((resolve) => { resolveInflight = resolve; }); this._panelOpenInflight.set(id, inflightPromise); this._panelOpenResolve[id] = resolveInflight; let ok = false; let didShowLoading = false; LoadScheduler.getInstance().beginUserLoad(); this._holdPause(id); try { const path = conf.path + conf.name; const layer = this._layer_map.get(conf.layer); if (!layer) { console.error('[UIManager] openPanel missing layer', id, conf.layer); return false; } if (param && param.showLoading && param.showLoading > 0) { gg.game.showLoading(param.showLoading); didShowLoading = true; } else { const bundle = assetManager.getBundle(conf.bundle); const cached = bundle?.get(path, Prefab); if (!cached?.isValid) { gg.game.showLoading(2); didShowLoading = true; } } const p = await this.loadPrefab(conf.bundle, path, this._bundleOwner(id)) as Prefab; if (this._openEpoch[id] !== epoch) { return false; } if (!p) { console.error('[UIManager] openPanel prefab load failed', id, conf.bundle, path); return false; } const ui = instantiate(p); uiInfo.prefabRes = p; uiInfo.uiNode = ui; uiInfo.uiBase = ui.getComponent(UIBase); if (!uiInfo.uiBase) { console.error('[UIManager] openPanel missing UIBase', id); return false; } uiInfo.uiBase.uiInfo = uiInfo; uiInfo.uiBase.preparePopOpenPose(); layer.addChild(ui); // onAdded 可能 await 资源包/onInit;挂起会导致 inflight 永不结束 // 超时 timer 必须在成功后清除,否则 reject 会变成未处理 rejection await new Promise((resolve, reject) => { let settled = false; const timer = setTimeout(() => { if (settled) return; settled = true; reject(new Error('[UIManager] onAdded timeout')); }, LoadRecoveryConfig.defaultTimeoutMs); void Promise.resolve(uiInfo.uiBase.onAdded(param)).then( () => { if (settled) return; settled = true; clearTimeout(timer); resolve(); }, (e) => { if (settled) return; settled = true; clearTimeout(timer); reject(e); }, ); }); if (this._openEpoch[id] !== epoch) { return false; } if (!uiInfo.uiBase.isValid) { console.error('[UIManager] openPanel onAdded 后节点已销毁', id); return false; } this._notifyPanelOpened(id); this._ui_map.set(id, uiInfo); ok = true; return true; } catch (err) { console.error('[UIManager] openPanel failed', id, String(err)); return false; } finally { const stale = this._openEpoch[id] !== epoch; if (!ok && !stale) { this._cleanupFailedOpen(id, uiInfo); this._releasePause(id); } if (didShowLoading) { const pkg = ok && !stale ? uiInfo.uiBase?.getResPkg() : null; const extraCount = pkg ? pkg.ress.length + (pkg.dirs?.length ?? 0) : 0; if (extraCount <= 0) { gg.game.hideLoading(); } } if (!stale) { this._finishPanelOpenWait(id); } LoadScheduler.getInstance().endUserLoad(); } } /** * 结束一次 openPanel 等待;恢复失败时必须调用,否则同 id 再也打不开。 * @param id 面板 id */ private _finishPanelOpenWait(id: number): void { this._panelOpenInflight.delete(id); const fin = this._panelOpenResolve[id]; if (fin) { this._panelOpenResolve[id] = undefined; fin(); } } /** * 作废进行中的 openPanel(loading 失败恢复)。过期回调不再上屏。 * @param keepIds 保留的面板 id(如匹配页) */ abortInflightOpens(keepIds?: readonly number[]): void { const ids = mapKeysToArray(this._panelOpenInflight); for (let i = 0; i < ids.length; i++) { const id = ids[i]; if (keepIds && keepIds.indexOf(id) !== -1) continue; this._abortOpen(id); } } /** * 作废指定 id 的打开中面板。 * @param id 面板 id */ private _abortOpen(id: number): void { this._openEpoch[id] = (this._openEpoch[id] ?? 0) + 1; const uiInfo = this._ui_map.get(id); if (uiInfo) { this._cleanupFailedOpen(id, uiInfo); } this._releasePause(id); this._finishPanelOpenWait(id); } /** 打开失败或 abort:销毁节点、归还预制引用与 bundle owner */ private _cleanupFailedOpen(id: number, uiInfo: UIInfo | null): void { if (uiInfo?.uiNode?.isValid) { uiInfo.uiNode.destroy(); } if (uiInfo?.prefabRes) { uiInfo.prefabRes.decRef(); uiInfo.prefabRes = null; } if (uiInfo) { uiInfo.uiNode = null; uiInfo.uiBase = null; this._releasePanelBundle(id, uiInfo.uiconf?.bundle); } this._ui_map.delete(id); if (this._curent_pop?.id === id) { this._curent_pop = null; } } /** * 关闭单例页面 * @param id 页面ID * @param tweenType 1: 直接关闭 2: 弹窗缩回,同时背景渐变成透明 3: 不缩放,直接渐变到透明 */ async closePanel(id: number, tweenType = 1) { if (this._panelOpenInflight.has(id)) { this._abortOpen(id); this._kickPopQueue(); return; } const inflight = this._panelCloseInflight.get(id); if (inflight) { await inflight; return; } if (!this._ui_map.has(id)) return; const run = this._closePanelOnce(id, tweenType); this._panelCloseInflight.set(id, run); try { await run; } finally { this._panelCloseInflight.delete(id); this._kickPopQueue(); } } /** * 关闭单例页面(已排除重入)。 * @param id 页面ID * @param tweenType 动效类型 */ private async _closePanelOnce(id: number, tweenType: number): Promise { if (!this._ui_map.has(id)) return; const uiInfo = this._ui_map.get(id); const bundleName = uiInfo.uiconf?.bundle; if (uiInfo.uiNode && uiInfo.uiNode.isValid) { await uiInfo.uiNode.getComponent(UIBase)?.destroyPanel(tweenType); } if (uiInfo.prefabRes) { uiInfo.prefabRes.decRef(); uiInfo.prefabRes = null; } uiInfo.uiNode = null; uiInfo.uiBase = null; this._ui_map.delete(id); if (this._curent_pop?.id === id) { this._curent_pop = null; } this._releasePanelBundle(id, bundleName); this._releasePause(id); } /** * 先打开新页面,成功后再关闭旧页面。 * @param openId 新页面 * @param closeIds 打开成功后关闭的页面 * @param param 打开参数 * @param closeTween closePanel 动效类型 */ async openThenClose(openId: number, closeIds: number[], param?: OpenParam, closeTween = 1): Promise { const ok = await this.openPanel(openId, param); if (!ok) return false; for (let i = 0; i < closeIds.length; i++) { const cid = closeIds[i]; if (cid === openId) continue; await this.closePanel(cid, closeTween); } return true; } /** * 关闭所有页面 * @param ids 排除的页面ID数组(传入的id页面不会关闭) */ async closeAllPanel(ids: number[] = []) { for (const entry of this._wait_popIds) { if (ids.indexOf(entry.id) !== -1) continue; this._releasePause(entry.id); } this._wait_popIds = this._wait_popIds.filter(entry => ids.indexOf(entry.id) !== -1); if (this._curent_pop && ids.indexOf(this._curent_pop.id) === -1) { this._curent_pop = null; } this.abortInflightOpens(ids); const keys = mapKeysToArray(this._ui_map); const layerRank = (layer: UILayer | undefined): number => { switch (layer) { case UILayer.Pop: case UILayer.Dalog: case UILayer.Guide: case UILayer.Anim: return 0; case UILayer.Page: return 1; case UILayer.Menu: case UILayer.MainUI: return 2; default: return 3; } }; keys.sort((a, b) => { const la = layerRank(this._ui_map.get(a)?.uiconf?.layer); const lb = layerRank(this._ui_map.get(b)?.uiconf?.layer); return la - lb; }); for (const id of keys) { if (ids.indexOf(id) !== -1) continue; await this.closePanel(id); } } /** * 清理 Anim 层上的飞金币/飞道具节点(挂有 FlyCurrency / CoinFly 等)。 * 进战斗/回大厅会卸载资源时,若飞行动画仍在跑,Sprite 可能已失效,需在切场景前销毁。 */ clearAnimFlyNodes() { const anim = this._layer_map?.get(UILayer.Anim); if (!anim?.isValid) { return; } const list = anim.children.slice(); for (const n of list) { if (!n?.isValid) continue; // 飞桃/飞道具节点一律清掉,避免持已释放贴图继续合批 Tween.stopAllByTarget(n); n.destroy(); } } /** * 是否存在页面 * @param id 页面ID * @returns */ hasPanel(id: number) { return !!this._ui_map.get(id)?.uiBase; } getPanelBase(id: number) { return this._ui_map.get(id)?.uiBase ?? null; } /** 弹窗是否正在显示或在 pop 队列中等待 */ hasPopPanelActiveOrQueued(id: number): boolean { if (this.hasPanel(id)) return true; if (this._curent_pop?.id === id) return true; return this._wait_popIds.some(entry => entry.id === id); } /** 从 pop 等待队列移除指定弹窗(复活成功等场景清掉重复入队) */ removeQueuedPopPanels(id: number): void { if (this._wait_popIds.length === 0) return; const wasQueued = this._wait_popIds.some(entry => entry.id === id); this._wait_popIds = this._wait_popIds.filter(entry => entry.id !== id); if (wasQueued && !this.hasPanel(id) && !this._panelOpenInflight.has(id)) { this._releasePause(id); } } /** * 判断是否有其他页面 * @param id 排除的页面id * @returns */ hasOtherPanel(id: number, sameLayer = true) { let conf = this.uiConfigs[id]; if (!conf) { console.error('[UIManager] hasOtherPanel missing config', id); return false; } let keys = mapKeysToArray(this._ui_map); for (let key of keys) { let info = this._ui_map.get(key); if (!info?.uiBase || !info.uiconf) continue; if (sameLayer && info.uiconf.layer != conf.layer) { continue; } if (info.id != id) { return true; } } return false; } /** * 页面是否显示中 * @param id 页面ID * @returns */ panelIsShow(id: number) { if (!this._ui_map.has(id)) return false; let uiInfo = this._ui_map.get(id); if (!uiInfo.uiNode || !uiInfo.uiBase || !uiInfo.uiNode.isValid || !uiInfo.uiNode.active) return false; return true; } /** * 打开弹窗(队列式弹窗,连续调用不会一起弹出来,关闭上一个后才会自动弹下一个) * @param id 弹窗id * @param param 参数 */ popPanel(id: number, param?: OpenParam) { this._wait_popIds.push({ id: id, param: param }); this._holdPause(id); this._kickPopQueue(); } /** 推进弹窗队列 */ flushPopQueue(): void { this._kickPopQueue(); } getHaveWaitPopLayer() { return this._wait_popIds.length > 0; } /** * 显示加载提示 */ async showWaitIcon() { if (!this._waitIcon) { let conf = null; for (let k in this.uiConfigs) { let v = this.uiConfigs[k]; if (v.name == "waitIcon") { conf = v; break; } } if (conf == null) return; let p = await this.loadPrefab(conf.bundle, conf.path + conf.name, 'toast') as Prefab; if (!p) { console.error('[UIManager] waitIcon 预制加载失败'); return; } this._waitIcon = instantiate(p); } this.showNode(this._waitIcon, UILayer.Loading); } /** * 关闭加载提示 */ closeWaitIcon() { if (this._waitIcon) { this._waitIcon.removeFromParent(); } } private _waitIcon: Node = null; //显示消息提示,不带自动翻译的 /** * 显示消息提示,带自动翻译的 * @param msg 消息 * @param time 显示时间(秒) */ async showToast(msg: string, isAutoTranslate: boolean = true, time: number = 1) { //翻译消息 if (isAutoTranslate) { msg = gg.lang.get(msg); } if (!msg || msg == "") { return; } let n: Node = null; if (this._msgPool.length > 0) { n = this._msgPool.pop(); } else { let conf = null; for (let k in this.uiConfigs) { let v = this.uiConfigs[k]; if (v.name == "msg") { conf = v; break; } } if (conf == null) return; let p = await this.loadPrefab(conf.bundle, conf.path + conf.name, 'toast') as Prefab; if (!p) { console.error('[UIManager] toast 预制加载失败'); return; } n = instantiate(p); } const layNode = n.getChildByName("lay"); const richTextNode = layNode.getChildByName("RichText"); richTextNode.getComponent(RichText)!.string = `${msg}`; richTextNode.getComponent(Label)!.string = `${msg}`; layNode.getComponent(Layout)?.updateLayout(); this.showNode(n, UILayer.Ad); const toastBaseY = view.getVisibleSize().height * TOAST_BASE_Y_RATIO; n.setPosition(0, toastBaseY, 0); tween(n) .set({ opacity: 255 }) .delay(time) .by(0.5, { position: v3(0, 100, 0) }) .call(() => { tween(n).to(0.5, { opacity: 0 }).start(); }) .by(0.25, { position: v3(0, 50, 0) }) .call(() => { this._msgPool.push(n); Tween.stopAllByTarget(n); n.removeFromParent(); let ind = this._msgShowPool.indexOf(n); if (ind >= 0) this._msgShowPool.splice(ind, 1); }).start(); this._msgShowPool.forEach(x => x.y += TOAST_STACK_GAP); this._msgShowPool.push(n); } private _msgPool: Node[] = []; private _msgShowPool: Node[] = []; /** * 显示动画(显示到UI管理的指定层级) * @param n 动画节点 */ showEff(n: Node) { this.showNode(n, UILayer.Anim); } showLog(str = "", clear = false) { if (this._log) { this._log.active = true; let log = this._log.getChildByName("log").getComponent(Label); log.string = clear ? str : log.string + "\n" + str; if (this._timer_flag) clearInterval(this._timer_flag); this._timer_flag = setInterval(this.updateDC.bind(this), 200); } } closeLog() { if (this._log) { this._log.active = false; } } updateDC() { if (this._log && this._log.active) { let dc = this._log.getChildByName("dc").getComponent(Label); dc.string = "当前dc:" + director.root.device.numDrawCalls; } } /** * 启动弹窗队列工人(已在跑则忽略)。 */ private _kickPopQueue(): void { if (this._popRunning) return; this._popRunning = true; void this._runPopQueue().finally(() => { this._popRunning = false; }); } /** * 串行弹出:当前窗还在展示则退出,等关闭再踢。 */ private async _runPopQueue(): Promise { if (this._curent_pop != null) { if (this._panelOpenInflight.has(this._curent_pop.id)) { return; } const cur = this._ui_map.get(this._curent_pop.id); if (cur?.uiBase) { return; } if (cur) { this._cleanupFailedOpen(this._curent_pop.id, cur); } this._releasePause(this._curent_pop.id); this._curent_pop = null; } while (this._wait_popIds.length > 0) { this._curent_pop = this._wait_popIds.shift(); const id = this._curent_pop.id; const param = this._curent_pop.param; const ok = await this.openPanel(id, param); if (!ok) { this._releasePause(id); this._curent_pop = null; continue; } return; } } private addUILayer(parent: Node, name: string, layer: UILayer = null) { let layerNode = new Node(name); parent.addChild(layerNode); //layerNode.addComponent(Mask); let w = layerNode.addComponent(Widget); w.isAlignLeft = w.isAlignTop = w.isAlignRight = w.isAlignBottom = true; w.left = w.top = w.right = w.bottom = 0; w.updateAlignment(); if (layer) { this._layer_map.set(layer, layerNode); } return layerNode; } /** * 面板持有的 bundle owner,按 id 独立,关闭时成对释放。 * @param panelId 面板 id */ private _bundleOwner(panelId: number): string { return `ui:${panelId}`; } /** * 归还本面板对分包的占用。 * @param panelId 面板 id * @param bundleName 分包 */ private _releasePanelBundle(panelId: number, bundleName: string | undefined): void { if (!bundleName) return; BundleLifecycle.getInstance().releaseBundleOwner(bundleName, this._bundleOwner(panelId)); } /** * 持有 pauseBattle(同 id 只 acquire 一次)。 * @param id 面板 id */ private _holdPause(id: number): void { if (!this.uiConfigs[id]?.pauseBattle) return; if (this._pauseHeld.has(id)) return; membershipAdd(this._pauseHeld, id); this._notifyBattlePause(id, true); } /** * 释放 pauseBattle(未持有则忽略)。 * @param id 面板 id */ private _releasePause(id: number): void { if (!membershipDelete(this._pauseHeld, id)) return; this._notifyBattlePause(id, false); } private async loadPrefab(bundleName: string, assetPath: string, owner: string) { let bundle = assetManager.getBundle(bundleName); if (!bundle) bundle = await this.loadBundle(bundleName, owner); else if (owner) BundleLifecycle.getInstance().acquire(bundleName, owner); if (!bundle) { console.error('[UIManager] loadPrefab bundle 失败', bundleName, assetPath); return null; } return this.loadRes(bundle, assetPath); } private async loadBundle(bundleName: string, owner: string) { const bundle = await BundleLifecycle.getInstance().ensureLoaded(bundleName); if (bundle && owner) { BundleLifecycle.getInstance().acquire(bundleName, owner); } if (!bundle) { console.error('[UIManager] loadBundle 失败', bundleName); } return bundle; } /** * 加载预制。缓存命中须 addRef;原生 load 由引擎持有本次引用。 * 经 ResourcesManager 同路径合并,避免与静默 preload/load 双飞后回调丢失。 */ private loadRes(bundle: AssetManager.Bundle, assetPath: string): Promise { return new Promise((resolve) => { gg.res.loadResPublic(bundle, assetPath, (asset: Asset | null, fromCache?: boolean) => { if (!asset?.isValid) { console.error('[UIManager] loadRes 预制无效', bundle.name, assetPath); resolve(null); return; } if (fromCache) { asset.addRef(); } resolve(asset); }, Prefab); }); } }