Cocos Creator MVVM实现UI框架(一)

您所在的位置:网站首页 mvvm开源框架 Cocos Creator MVVM实现UI框架(一)

Cocos Creator MVVM实现UI框架(一)

2023-12-19 05:29| 来源: 网络整理| 查看: 265

为什么要做UI框架?

框架其实就是一种模块的分离,一种写代码的规则,目的都是便于代码的管理修改,更有利于编码思维。

通常制作UI的过程,我们一般的做法大多如此:

在脚本内定义所需要的预制体组件; 将组件拖拽到脚本定义的位置内实现UI的事件绑定。

但这样的方式存在一些弊端:

每次重新编写脚本,都需要重新拖拽组件。对于小项目来说这倒不是大问题,偶尔忘记也能很快复原,但是有一百个组件的时候,你再试试? 在项目的制作过程中,很容易遇到UI结构和排版需要调整的情况。如果把结构写死了,每次调整都意味着之前的操作要重新做一遍。 思维框架 & UI框架

既然要写框架,目的就是便于后期我们用代码管理修改。无论如何调整结构,都不需要我们过多修改代码。这也是UI框架的核心需求。我们通过这些需求点,去一层一层地剖析结构。

想要什么,该怎么做

框架的体现是方便,那么体现方便的具体表现是什么?对于程序最重要的就是代码的简洁还有合理的数据管理。但是饼画大了也有点吃不消,不如先以玩家的视角来考虑。

试着想象一个完整的游戏呈现在屏幕上(如果没少玩游戏的话应该不难想象到,或者干脆打开一个游戏界面看看),能想象到一个大致的内容,比如总有一个占满全屏幕的游戏画面吧?左上角应该有个玩家头像和账户信息?右上角一般有个设置按钮和邮箱按钮?噢对,点开邮箱还会弹出一个窗口显示邮件内容……OK!到这里就足够了,简单区分一下他们吧。

UI的层级

UI的种类可以分为很多种,但总离不开基本的三种:

页面(Page):一般占满全屏,像游戏场景、背景等,在所有UI的最底层。 挂件(Widgit):一般占据一小部分的位置,停靠在屏幕的某个位置,处于页面层上方。 弹窗(Window):一般处于屏幕的正中央,在所有UI层级之上,属于动态出现的层级。

UI在这个分类下,就可以在层级方面进行分类了,要体现层级完全不麻烦,在编辑器内可以此为基础进行设计:

让所有的UI页面都通过预制体的方式存储管理:如prefabs/LoginPage

创建一个空节点uiRoot,再分支不同的空节点区分优先级,最下面的节点就会显示在最前面

在编辑器Canvas节点内添加如下排布:

uiRoot page- 页面层 widget - 挂件层 window - 弹窗层

养成好习惯,将文件结构规范一下

assets

prefabs - 预制体文件夹 UI - UI预制体 LoginPage ... scripts - 脚本文件夹 UI - UI预制体对应的脚本 LoginPage.ts ... UIFramework - UI框架 ... - UI框架内容 创建配置表

配置表的目的在于建立编辑器面板与代码之间的连接,在数据上统一。

按照上述结构,建立一个UIDef类进行配置定义: 12345678910111213141516171819202122232425/** UI根节点*/export let UIRoot = 'Canvas/uiRoot';/** UI预制体路径*/export let UIPath = { LoginPage: 'prefabs/LoginPage'}/** UI层级*/export enum UILayer { E_PAGE, E_WIDGIT, E_WINDOW}/** 组件类型*/export enum CompType { E_NONE = -1, E_BUTTON, E_LABEL, E_EDITBOX, E_PAGEVIEW, E_PROGRESSBAR //可拓展}

这个类将作为配置表参与UI管理器的调度。

这么多UI,如何管理

要通过代码来管理UI,通常想到的做法是把UI拖进某个脚本预留的组件位置中,前面也分析过了这样做的弊端。

所有这次要做到的就是不需要拖拽,改用代码来识别UI,给需要参与管理的UI节点用特殊的命名法标记,便于代码识别,这里我选择用下划线_作为开头的UI节点将被识别为需要管理的节点。

以LoginPage为例,预制体结构大致如下:

LoginPage - 登陆界面根节点 _userName - (Label) 显示登陆的用户名 _btnLogin - (Button) 登录按钮

先跳过业务逻辑,点击_loginBtn按钮后可以直接更改_userName的文本内容

构建UI基类

为了管理不同类型的UI节点,显然需要通过继承的方式。同时代码访问UI的前提是UI需要存在内存中,否则先从动态文件读取然后存到内存里访问,用到的是面向对象的思想。

先来简单地分析一下所有UI组件的共性:

第一步得先找到所有_开头的节点,存进一个UI容器内,然后通过名字来进行查找调用某个节点 可以通过查找节点的名称来获取节点 可以显示 / 隐藏 通过某一个UI节点的交互,可能会改变一个或多个其他节点或其他组件的属性,亦或触发某个其他类中的函数。(比如点击登录按钮,改变用户名的文本,或者改变其他UI预制体的属性)

按照上面的需求来实现吧。

创建一个页面容器类UIContainer,用于存放和搜索一个UI预制体中的节点和各种组件。通过一个初始的加载函数,搜索并存储以_开头的节点对象,大致结构如下: graph LR UIContainer(UIContainer - UI节点容器类)-->dicUiType(_dicUiType) dicUiType(_dicUiType)-->Node(Node: mapNode) dicUiType(_dicUiType)-->Button(Button: mapButton) dicUiType(_dicUiType)-->Label(Label: mapLabel) dicUiType(_dicUiType)-->EditBox(EditBox: mapEditBox) UIContainer(UIContainer - UI容器类)-->Load(load函数 - 获取所有以 _ 开头的组件存入字典) UIContainer(UIContainer - UI容器类)-->getNode(getNode - 根据名字获取Node类型的UI) UIContainer(UIContainer - UI容器类)-->others(...) UIContainer.ts 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107import { CompType } from './UIDef'//页面的ui容器,用于存放和搜索一个页面中的节点和各种组件export default class UIContainer { //不同类型节点Map private _mapNode: Map = new Map(); private _mapBtn: Map = new Map(); private _mapLabel: Map = new Map(); private _mapEditBox: Map = new Map(); private _mapPageView: Map = new Map(); private _mapProgressBar: Map = new Map(); //可拓展 /**组件类型数组,对应UIDef中的类型枚举(可拓展) */ private _arrType: any[] = [cc.Button, cc.Label, cc.EditBox, cc.PageView, cc.ProgressBar]; /**组件类型对应的Map(可拓展) */ private _arrTypeMap: any[] = [this._mapBtn, this._mapLabel, this._mapEditBox, this._mapPageView, this._mapProgressBar]; /** * 载入UI结构 * @param rootN UI根节点 */ public load(rootN: cc.Node) { // 遍历所有子节点 let children = rootN.children; for (let i = 0; i < children.length; i++) { //筛选以下划线开头的子节点 let childN = children[i]; if (childN.name.startsWith('_')) { this._mapNode.set(childN.name, childN); //存入节点Map //遍历判断节点属于什么类型的组件 for (let i = 0; i < this._arrType.length; i++) { let comp = childN.getComponent(this._arrType[i]); //若类型匹配,存入对应的Map if (comp) { this._arrTypeMap[i].set(childN.name, comp); } } } this.load(childN); //递归 } } /** * 查找节点 * @param key 节点名 * @returns 查找结果 */ getNode(key: string): cc.Node { if (key.length UIContainer(UIContainer - UI内的节点容器类) Base(UIBase - UI组件基类)-->onInit(onInit - UI的初始化) Base(UIBase - UI组件基类)-->onEnter(onEnter - UI加载时触发的函数) Base(UIBase - UI组件基类)-->show(show - UI的显示方法) Base(UIBase - UI组件基类)-->hide(hide - UI的隐藏方法) Base(UIBase - UI组件基类)-->others(...) UIBase.ts(初步) 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273import UIContainer from './UIContainer'const { ccclass, property } = cc._decorator;@ccclassexport default class UIBase extends cc.Component { public strName: string = ''; //UI标识名称 protected ui: UIContainer = new UIContainer(); //UI节点容器 //生命周期函数 /** 初始化*/ _init(params: any) { this.ui.load(this.node); this.onInit(params); } /** 初次加载时调用*/ onInit(params: any) { } /** 每次显示时调用*/ onEnter(params: any) { } /** 每次隐藏时调用*/ onExit() { } /** * 显示UI * @param params 参数 */ show(params: any) { this.node.active = true; this.onEnter(params) } /** 隐藏UI*/ hide() { this.node.active = false; this.onExit(); } /** * 添加Button点击事件 * @param btnName Button节点名 * @param cb 点击回调 * @returns */ addButtonClick(btnName: string, cb: Function) { let btn = this.getNode(btnName); if (!btn) { return; } btn.on('click', cb); } //对UIContainer内的方法进行一层封装,跟随UIContainer的方法进行拓展 getNode(key: string): cc.Node { return this.ui.getNode(key); } getButton(key: string): cc.Button { return this.ui.getButton(key); } getLabel(key: string): cc.Label { return this.ui.getLabel(key); } getEditBox(key: string): cc.EditBox { return this.ui.getEditBox(key); } getProgressBar(key: string): cc.ProgressBar { return this.ui.getProgressBar(key); }}

如此一来,所有的UI节点都继承自UIBase类,再将UI的脚本挂载在对应的prefab中。

例如登陆页面:

123456import UIBase from '../UIManager/UIBase';const { ccclass, property } = cc._decorator;export default class LoginPage extends UIBase { //...}

现在这些UI都属于同一个基类了,因此可以很方便地进行存储管理,但是怎么管理?自然要一个类来充当管理器的角色。

UI管理器 初步定义一个UIManager单例,用于管理并操作所有继承了UIBase的节点(初始化,加载UI等)。 graph LR UIManager(UIManager单例 - 管理所有UIBase)-->arrLayerN(_arrLayerN - 编辑器中层级节点的存储列表) UIManager(UIManager单例 - 管理所有UIBase)-->mapUI(_mapUI - 存放所有已加载的UIBase) UIManager(UIManager单例 - 管理所有UIBase)-->init(_init - 存放所有已加载的UIBase) UIManager(UIManager单例 - 管理所有UIBase)-->openUI(openUI - 打开指定UI,若不存在则从预制体加载) UIManager(UIManager单例 - 管理所有UIBase)-->closeUI(closeUI - 关闭指定UI)

接着考虑需要的功能:

_init()函数要从UIDef配置表中获取根节点UIRootNode 从根节点下获取子节点,作为层级存入_arrLayerN[] openUI()函数接收自定义参数,以UI名作为参数,到_mapUI中搜索已有节点 若没有找到对应节点,则从预制体内加载并存入_mapUI下的指定层级(若没有指定层级则默认存入Page层级) UI加载完后如果需要,可能执行回调函数,需要一个可选的回调参数,方便调用 UIManager.ts(初步) 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102import { UIRoot, UIPath, UILayer } from './UIDef';import UIBase from './UIBase';const { ccclass, property } = cc._decorator;@ccclassexport default class UIManager extends cc.Component { static _instance: UIManager = null; /** 获取UIManager单例*/ static getInstance(): UIManager { if (!this._instance) { this._instance = new UIManager(); this._instance._init(); } return this._instance; } /** 用于存放层级节点*/ private _arrLayerN: cc.Node[] = []; /** 用于存放所有已加载的UIBase*/ private _mapUI: Map = new Map(); /** 初始化*/ _init() { let rootN = cc.find(UIRoot); //获取UI根节点 this._arrLayerN.length = 0; //清空层级 for (let layerN of rootN.children) { this._arrLayerN.push(layerN); //存储UI的层级节点 } } /** * 查找已加载的UIBase节点 * @param uiName UI节点名 * @returns 查找结果 */ getUI(uiName: string): UIBase { return this._mapUI.get(uiName); } /** * 加载UI * @param uiName UI节点名 * @param params (可选)参数 * @param layer (可选)指定层级 * @param cb (可选)回调函数 */ openUI(uiName: string, params?: any, layer?: number, cb?: Function) { //在配置表中查找节点名对应的UI路径 let path = UIPath[uiName]; if (!path) { cc.error('can not find the ui!'); return; } //通过路径查找UI是否加载过 let uiBase = this._mapUI.get(path); if (!uiBase) { //未加载则读取prefab cc.resources.load(path, (err, asset) => { if (err) { return; } let uiN = cc.instantiate(asset); //实例化 layer = layer || UILayer.E_PAGE; //若为空,则默认挂载page层级 uiN.parent = this._arrLayerN[layer]; //设置层级 //获取节点的UIBase组件,存入已加载的系节点列表 uiBase = uiN.getComponent(UIBase); this._mapUI.set(uiName, uiBase); uiBase.strName = uiName; //设置UI节点名字 //周期函数 uiBase._init(params); uiBase.show(params); if (cb) { cb(); } }); return; } //若已加载过,则直接显示 uiBase.show(params); if (cb) { cb(); } } /** 隐藏UI节点*/ closeUI(uiName: string) { let uiBase = this._mapUI.get(uiName); if (!uiBase) { return; } uiBase.hide(); }}

为了调用方便,同样在UIBase中加一层封装。

UIBase.ts(封装部分) 12345678910//对UIManager内的方法进行一层封装openUI(uiName: string, params?: any, layer?: number, cb?: Function) { UIManager.getInstance().openUI(uiName, params, layer, cb);}closeUI(uiName: string) { UIManager.getInstance().closeUI(uiName);}closeSelf() { UIManager.getInstance().closeUI(this.strName);} 初步测试

现在写的部分已经差不多可以打开一个页面了,先在编辑器内跑起来。

新建一个App.ts脚本,挂载到Canvas节点 12345678910import UIManager from './UIFramework/UIManager'const { ccclass, property } = cc._decorator;@ccclassexport default class AppStart extends cc.Component { onLoad() { UIManager.getInstance().openUI('LoginPage'); //路径配置表:LoginPage }} 之前啥都没写的LoginPage.ts可以加内容了,记得要挂载在预制体根节点 123456789101112131415161718import UIBase from '../UIFramework/UIBase';import UIManager from '../UIFramework/UIManager';const { ccclass, property } = cc._decorator;@ccclassexport default class LoginPage extends UIBase { onInit(params: any) { console.log(params); //按钮点击事件 this.addButtonClick('_btnLogin', () => { let lbUserName = this.getLabel('_userName'); lbUserName.string = 'AlertNote'; alert('登录成功!'); }) }}

关键注意几个可能踩坑的地方,配置表的键名和openUI()的UI名,都是容易遗漏修改的地方。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3