import { _decorator, Component, director, error, instantiate, Node, Prefab, UITransform, Widget, assetManager, AssetManager, tween, v3, UIOpacity, Vec3, ProgressBar, Sprite, Tween } from "cc"; import ui_base from "./ui_base"; import { ResolutionAutoFit } from "./ui_ResolutionAutoFit"; const { ccclass, property } = _decorator; @ccclass('ui_updater') class ui_updater extends Component { update(dt: number) { ui_base.updateAll(dt); } } export enum GameUILayers { GAME, JOY_STICK, HUD, POPUP, ALERT, NOTICE, LOADING, OVERLAY } type Constructor = new () => T; /**ui管理模块*/ class ui { private static _instance: ui; public static getInstance(): ui { if (!this._instance) this._instance = new ui(); return this._instance; } public delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } public delay_second(s: number): Promise { return new Promise(resolve => setTimeout(resolve, s * 1000)); } public progressBar_anim(bar: ProgressBar, progress: number, duration: number = 0.2): Promise { if (bar.progress >= progress) { bar.progress = progress; return; } else { return new Promise(resolve => tween(bar).to(duration, { progress: progress }).call(() => { resolve() }).start()); } } public fillRange_anim(sp: Sprite, progress: number, duration: number = 0.2, limt: -1 | 0 | 1 = 0): Promise { if ((limt > 0 && sp.fillRange >= progress) && (limt < 0 && sp.fillRange <= progress)) { sp.fillRange = progress; return; } else { return new Promise(resolve => tween(sp).to(duration, { fillRange: progress }).call(() => { resolve() }).start()); } } public scale_anim(node: Node, duration: number = 0.2, start: number = 0.5, end: number = 1,): Promise { node.setScale(start, start, 1); return new Promise(resolve => tween(node).to(duration, { scale: v3(end, end, 1) }).call(() => { resolve() }).start()); } public scale_elasticOut_anim(node: Node, duration: number = 0.2, start: number = 0.5, end: number = 1,): Promise { node.setScale(start, start, 1); return new Promise(resolve => tween(node).to(duration, { scale: v3(end, end, 1) }, { easing: 'elasticOut' }).call(() => { resolve() }).start()); } public fade_anim(node: Node, duration: number = 0.2, startOpacity: number = 0, targetOpacity: number = 255): Promise { const opacity = node.getComponent(UIOpacity) ?? node.addComponent(UIOpacity); opacity.opacity = startOpacity; return new Promise((resolve) => { tween(opacity).to(duration, { opacity: targetOpacity }).call(() => { resolve() }).start(); }); } public shake_anim(node: Node, duration: number = 0.2, intensity: number = 5, shakeCount: number = 7): Promise { return new Promise((resolve) => { const originalPosition = node.position.clone(); // 保存原始位置 const shakeDuration = duration / (shakeCount * 2); // 每次抖动的持续时间 const shakeTween = tween(node); for (let i = 0; i < shakeCount; i++) { const offsetX = (i % 2 === 0 ? intensity : -intensity); // 交替偏移 shakeTween.to(shakeDuration, { position: v3(originalPosition.x + offsetX, originalPosition.y, originalPosition.z) }) .to(shakeDuration, { position: originalPosition }); // 回到原位 } shakeTween.call(() => { resolve() }).start(); // 开始抖动动画 }); } public rotate_shake_anim(node: Node, duration: number = 0.3, intensity: number = 5, shakeCount: number = 5): Promise { return new Promise((resolve) => { const originalAngle = node.angle; // 保存原始角度 const shakeDuration = duration / (shakeCount * 2); // 每次晃动的持续时间 const shakeTween = tween(node); for (let i = 0; i < shakeCount; i++) { const offsetAngle = (i % 2 === 0 ? intensity : -intensity); // 交替旋转 shakeTween.to(shakeDuration, { angle: originalAngle + offsetAngle }) .to(shakeDuration, { angle: originalAngle }); // 回到原位 } shakeTween.call(() => { resolve() }).start(); // 开始角度晃动动画 }); } public scale_shake_anim(node: Node, duration: number = 0.3, intensity: number = 0.1, shakeCount: number = 10, reset: number | null = null): Promise { if (reset) { Tween.stopAllByTarget(node); node.setScale(reset, reset, reset); } return new Promise((resolve) => { const originalScale = node.scale.clone(); // 保存原始缩放 const shakeDuration = duration / (shakeCount * 2); // 每次震动的持续时间 const shakeTween = tween(node); for (let i = 0; i < shakeCount; i++) { const offsetScale = (i % 2 === 0 ? intensity : -intensity); // 交替缩放 shakeTween.to(shakeDuration, { scale: v3(originalScale.x + offsetScale, originalScale.y + offsetScale, originalScale.z) }) .to(shakeDuration, { scale: originalScale }); // 回到原位 } shakeTween.call(() => { resolve() }).start(); // 开始缩放震动动画 }); } public opacity_shake_anim(node: Node, duration: number = 0.8, intensity: number = 80, shakeCount: number = 5): Promise { const opacity = node.getComponent(UIOpacity) ?? node.addComponent(UIOpacity); const originalOpacity = opacity.opacity; const shakeDuration = duration / (shakeCount * 2); return new Promise((resolve) => { const shakeTween = tween(opacity); for (let i = 0; i < shakeCount; i++) { const offsetOpacity = (i % 2 === 0 ? intensity : -intensity); shakeTween.to(shakeDuration, { opacity: Math.min(Math.max(originalOpacity + offsetOpacity, 0), 255) }) .to(shakeDuration, { opacity: originalOpacity }); } shakeTween.call(() => { resolve() }).start(); // 开始透明度震动动画 }); } /**处理弹跳动画*/ public bounce_anim(node: Node, height: number = 100, duration: number = 0.5): Promise { return new Promise((resolve) => { tween(node) .to(duration, { position: node.position.clone().add(new Vec3(0, height, 0)) }, { easing: 'bounceOut' }) // 向上弹跳 .to(duration, { position: node.position }, { easing: 'bounceIn' }) // 回到原位 .call(() => { resolve() }) //完成动画 .start(); //开始动画 }); } /** * 奖励物飞行动画 * @param node 奖励物节点 * @param startPos 起始爆开点 * @param targetPos 最终目标点 * @param explosionDistance 爆开距离 * @param explosionDuration 爆开时间 * @param stayDuration 停留时间 * @param moveDuration 移动到目标的时间 * @returns */ public reward_fly_anim(node: Node, startPos: Vec3, targetPos: Vec3, explosionDistance: number = 100, explosionDuration: number = 0.3, stayDuration: number = 0.5, moveDuration: number = 0.3): Promise { node.setPosition(startPos); return new Promise((resolve) => { //随机方向 const randomDirection = new Vec3(Math.random() * 2 - 1, Math.random() * 2 - 1, 0).normalize(); const explosionEndPos = startPos.add(randomDirection.multiplyScalar(explosionDistance)); // 爆炸效果的位置 tween(node) .to(explosionDuration, { worldPosition: explosionEndPos }) // 爆炸动画 .delay(stayDuration) //停留时间 .to(moveDuration, { worldPosition: targetPos }, { easing: 'cubicIn' }) .call(() => { resolve() }).start(); }); } private _uiCanvas: Node; private _uiRoot: Node; private createFullScreenNode() { let canvas = this._uiCanvas.getComponent(UITransform); let node = new Node(); node.layer = this._uiCanvas.layer; let uiTransform = node.addComponent(UITransform); uiTransform.width = canvas.width; uiTransform.height = canvas.height; let widget = node.addComponent(Widget); widget.isAlignBottom = true; widget.isAlignTop = true; widget.isAlignLeft = true; widget.isAlignRight = true; widget.left = 0; widget.right = 0; widget.top = 0; widget.bottom = 0; return node; } private getLayers(): GameUILayers[] { return Object.values(GameUILayers).filter(value => typeof value === 'number') as GameUILayers[]; } /** * @en init,`don't call more than once`. * @zh 初始化UIMgr,`不要多次调用` * */ public init(uiCanvas: Node | Prefab) { if (this._uiCanvas) return; if (!uiCanvas) throw error('uiCanvas must be a Node or Prefab'); if (uiCanvas instanceof Node) { this._uiCanvas = uiCanvas; } else { this._uiCanvas = instantiate(uiCanvas); director.getScene().addChild(this._uiCanvas); } this._uiCanvas.name = '__ui_canvas__'; director.addPersistRootNode(this._uiCanvas); if (!this._uiCanvas.getComponent(ui_updater)) { this._uiCanvas.addComponent(ui_updater); } let canvas = this._uiCanvas.getComponent(UITransform); this._uiCanvas.addComponent(ResolutionAutoFit); this._uiRoot = this.createFullScreenNode(); this._uiRoot.name = 'root' canvas.node.addChild(this._uiRoot); const layers = this.getLayers(); //create layers for (let i = 0; i < layers.length; ++i) { let layerNode = this.createFullScreenNode(); layerNode.name = 'layer_' + GameUILayers[layers[i]]; this._uiRoot.addChild(layerNode); } } /**获取层级节点*/ public getLayerNode(layerIndex: number): Node { return this._uiRoot.children[layerIndex] || this._uiRoot; } /**关闭所有界面*/ public closeAll() { ui_base.closeAll(); } /**关闭和释放所有界面 */ public closeAndReleaseAll() { ui_base.closeAndReleaseAll(); } /**关闭某个界面*/ public close(uiCls: Constructor): void { this.get(uiCls)?.close(); } /**获取界面*/ public get(uiCls: Constructor): T { let all = (ui_base as any)._uis; for (let i = 0; i < all.length; ++i) { let c = all[i]; if (c instanceof uiCls) { return c; } } return null; } /**某个界面是否显示中*/ public isShowing(uiCls: Constructor): boolean { return (ui_base as any)._hasCls(uiCls); } private _clss_loading: Set = new Set(); /**是否有正在加载中的界面*/ public isLoading(uiCls: Constructor | null): boolean { if (!uiCls) return this._clss_loading.size > 0; return this._clss_loading.has(uiCls); } /*** * @en show ui by the given parameters. * @zh 显示UI * @param uiCls the class, must inherits from the class `UIController`. * @returns the instance of `uiCls` * */ public async show(uiCls: Constructor, ...data: any[]): Promise { if (this.isLoading(uiCls)) return null; if (this.isShowing(uiCls)) return null; let ui = new uiCls(); this._clss_loading.add(uiCls); let bundleName = ui.bundle; if (bundleName) { let bundle = assetManager.getBundle(bundleName); if (!bundle) { try { const loadedBundle = await this.loadBundleAsync(bundleName); return await this._create(loadedBundle, ui, uiCls, ...data); } catch (err) { console.error(err); this._clss_loading.delete(uiCls); } } else { return await this._create(bundle, ui, uiCls, ...data); } } else { this._clss_loading.delete(uiCls); console.error("ui no bundle name"); return null; } } private async _create(bundle: AssetManager.Bundle, ui: T, uiCls: Constructor, ...p: any[]): Promise { try { const data: Prefab = await this.loadPrefabAsync(bundle, ui.prefab); let node: Node = instantiate(data); let parent = this.getLayerNode(ui.layer); parent.addChild(node); (ui as any)._setup(uiCls, node, ...p); this._clss_loading.delete(uiCls); return ui; } catch (err) { console.error(err); this._clss_loading.delete(uiCls); return; } } private loadBundleAsync(bundleName: string): Promise { return new Promise((resolve, reject) => { assetManager.loadBundle(bundleName, null, (err, loadedBundle) => { if (err) { reject(err); } else { resolve(loadedBundle); } }); }); } private loadPrefabAsync(bundle: AssetManager.Bundle, prefabName: string): Promise { return new Promise((resolve, reject) => { bundle.load(prefabName, (err, data: Prefab) => { if (err) { reject(err); } else { resolve(data); } }); }); } } export const gui = ui.getInstance(); export { ui_base };