一、对UI节点(控件)进行命名规范有哪些好处


1. 根据其名字就知道其作用是什么,避免时间长了看不懂自己曾经写的代码。

2.多人之间协作或者多部门之间合作时,避免因更改节点的层级导致在代码中查找不到而引发的报错 和 脚本组件对其节点引用的丢失引发的错误。

3.多人协作一个功能是,由于命名不统一,导致团队沟通效率低下(有时大家说的是同一个事物,但名称不相同,可能会出现不必要的分歧)。

4.方便后面进行 UI 自动绑定,事件绑定,资源本地化,语言本地化自动化/半自动化处理,节省大量开发时间。


二、UIHelper的出现


节点的分析: 不管这个控件有多复杂,最终展示在屏幕上一定是某种渲染(Label, Sprite)或者功能(Button)呈现出来。所以建议UI节点命名规则入下:


下划线(_) + 主要组件简写(可有可无) + 控件功能描述 + $(必须和用户自定义数据一起使用) + 用户自定义数据(可有可无)


例如:


_labTitle,这控件其含义分解出来是, 下划线(_):表示这个在节点在代码中用到,lab: 是cc.Label组件的简写。Title: 表示是这个控件主要干什么。合起来就是这个控件是一个以Label组件为主的渲染组件,显示一个标题,并且在代码中引用到。那么用户自定义数据是用来干嘛的呢,比如说: _labTitle$1001,除了具有前面的意思,因为这是一个Label组件,主要显示一个标题,那么1001可以解释成,这个Label组件语言本地化(国际化)代码是1001。植入一个插件。实现语言本地化(这个本文不介绍)。


1. 绑定UI节点


我们只希望自动绑定部分UI节点,而不是绑定全部的UI节点。那么此时的下划线(_)这个前缀的作用就体现出来了,通过这个下划线(_)来区分这个UI节点是否参与UI节点的绑定。在此之前也有大佬写出了UI自动绑定功能(UIKiller )。此文和 UIKiller有些区别。


控件分析:


每一个控件一定会有一个Node,如果将UI自动绑定直接指定到这个Node节点,不是本文所想要的。希望通过组件的简写直接绑定到相应的功能组件上去(为什么这样做: 经过多年游戏经验积累,大多数时候每个控件都带有很强的目的性。), 如果UI节点参与了UI绑定,但是却省略了主要组件的简写,则希望自接绑定这个组件的node上,而不是这个组件。

// 前缀(组件简写)映射相应的组件, 用于自动绑定 绑定到到相应的组件
public static PREFIX_MAP: { [key: string]: string } = {
    "_lab": "cc.Label",
    "_btn": "cc.Button",
    "_sp": "cc.Sprite",
    "_rt": "cc.RichText",
    "_mask": "cc.Mask ",
    "_ms": "cc.MotionStreak",
    "_tm": "cc.TiledMap",
    "_tt": "cc.TiledTile",
    "_spine": "sp.Spine ",
    "_gh": "cc.Graphics",
    "_ani": "cc.Animation",
    "_wv": "cc.WebView ",
    "_eb": "cc.EditBox",
    "_sv": "cc.ScrollView ",
    "_vp": "cc.VideoPlayer",
    "_pb": "cc.ProgressBar",
    "_pv": "cc.PageView",
    "_sli": "cc.Slider",
    "_tg": "cc.Toggle",
};
// 绑定组件
public static bindComponent(component: cc.Component) {
    this.bindNode(component.node, component);
}
// 绑定node
public static bindNode(node: cc.Node, component: cc.Component) {
    if (component[`$collector`] === node.uuid) {
        cc.warn(`重复绑定退出.${node.name}`)
        return;
    }
    component[`$collector`] = node.uuid;
    this._bindSubNode(node, component);
}
// 绑定子节点
private static _bindSubNode(node: cc.Node, component: cc.Component) {
    // 检测前缀是否符合绑定规范
    let name = node.name;
    let names = name.split("$");
    let componentName = names[0];
    if (this.checkNodePrefix(name)) {
        // 查询这个节点是否重复绑定到同一个用户自定义组件上
        if (component[name]) {
            cc.warn(`组件中有相同节点被重复绑定了. ${name}`)
            component[name] = null;
            delete component[name];
        }
        let nodeName = this.getNodePrefix(name);
        component[`${componentName}`] = (nodeName && this.PREFIX_MAP[nodeName]) ? node.getComponent(this.PREFIX_MAP[nodeName]) : node;
        this._bindButtonEvent(node, component);
        this._bindNodeEvent(node, component);
        this._bindNodeLongEvent(node, component);
    }
    // 绑定子节点
    node.children.forEach((target: cc.Node) => {
        this._bindSubNode(target, component);
    });
}


代码中如何访问经过 UIHelper 绑定的节点


this["_labTitle"].string = "hello world"; // this["_labTitle"] 是一个Label组件
this["_btnStart"].interactable = fasle; // this["_btnStart"] 是一个Button 组件
this["_nodeBg"].active = false; // this["_nodeBg"]是这个组件的节点 node


2. 绑定按钮点击事件


本文希望按钮事件也不通过界面去配置,而是希望通过UIHelper去自动绑定Button事件,如果UIj节点上已经关联了事件,则取消绑定。需要在相应的脚本组件里添加相应的函数,函数名称规则如下:

按钮事件函数 = on + 控件的名字 + Click

// 绑定Button事件
private static _bindButtonEvent(node: cc.Node, component: cc.Component): void {
    if (!node.name.match(/^_btn/)) {
        // 不是下划线开头的节点不绑定事件
        return;
    }
    if (node.getComponent(cc.EditBox)) {
        return;
    }
    let eventName = `${node.name}Click`;
    eventName = eventName.replace(/^_btn/, "onBtn")
    let tempEvent = component[eventName];
    let buttonComponent = component[node.name]
    if (tempEvent && buttonComponent && buttonComponent.clickEvents.length === 0) {
        var clickEventHandler = new cc.Component.EventHandler();
        clickEventHandler.target = component.node;
        clickEventHandler.component = this._getComponentName(component);
        clickEventHandler.handler = eventName;
        clickEventHandler.customEventData = "";
        buttonComponent.clickEvents.push(clickEventHandler);
    }
}


代码中如何书写经过 UIHelper 绑定相应带有button节点的事件

onBtnStartClick(event) {
}
onBtnBackClick(event) {
}


3. 绑定节点的touch事件


在游戏中有时候会处理某个节点的touch事件,本文希望 UIHelper 自动绑定相应的事件,函数名称格式如下:

开始触摸事件函数 = on + 控件的名字 + TouchStart

触摸移动事件函数 = on + 控件的名字 + TouchMove

触摸结束事件函数 = on + 控件的名字 + TouchEnd

触摸取消事件函数 = on + 控件的名字 + TouchCancel

private static _getTouchEventName(name: string): any[] {
    return [
        [`${name}TouchStart`, cc.Node.EventType.TOUCH_START],
        [`${name}TouchMove`, cc.Node.EventType.TOUCH_MOVE],
        [`${name}TouchEnd`, cc.Node.EventType.TOUCH_END],
        [`${name}TouchCancel`, cc.Node.EventType.TOUCH_CANCEL],
    ];
}
// 绑定节点事件
private static _bindNodeEvent(node: cc.Node, component: cc.Component): void {
    if (node.getComponent(cc.EditBox)) {
        return;
    }
    let eventNames = this._getTouchEventName(node.name);
    eventNames.forEach((item) => {
        let eventName = item[0];
        let eventType = item[1];
        eventName = eventName.replace(/^_/, "");
        eventName = eventName.charAt(0).toUpperCase() + eventName.slice(1);
        eventName = `on${eventName}`;
        let tempEvent = component[eventName];
        if (tempEvent) {
            node.on(eventType, tempEvent, component);
        }
    })
}


代码中如何书写经过 UIHelper 绑定相应带有 节点的 触摸事件

onBtnBackTouchStart(event) {
    cc.log(`......onBtnBackTouchStart........`);
}
onBtnBackTouchMove(event) {
    cc.log(`......onBtnBackTouchMove........`);
}
onBtnBackTouchEnd(event) {
    cc.log(`......onBtnBackTouchEnd........`);
}
onBtnBackTouchCancel(event) {
    cc.log(`......onBtnBackTouchCanCel........`);
}


4. 绑定长按事件


有时候可能需要对某个节点进行长按事件处理,本文也希望进行自动绑定长按事件。函数名称格式如下:

长按事件函数 = on + 控件的名字 + TouchLong

private static _bindNodeLongEvent(node: cc.Node, component: cc.Component): void {
    let eventName = node.name;
    eventName = eventName.replace(/^_/, "");
    eventName = eventName.charAt(0).toUpperCase() + eventName.slice(1);
    eventName = `on${eventName}TouchLong`
    let tempEvent = component[eventName];
    if (tempEvent) {
        let timer = -1;
        node.on(cc.Node.EventType.TOUCH_START, () => {
            timer = setTimeout(() => {
                tempEvent.call(component);
            }, 3000);
        }, component);
        node.on(cc.Node.EventType.TOUCH_END, () => {
            if (timer >= 0) {
                clearTimeout(timer)
                timer = -1;
            }
        }, component);
        node.on(cc.Node.EventType.TOUCH_CANCEL, () => {
            if (timer >= 0) {
                clearTimeout(timer)
                timer = -1;
            }
        }, component);
    };
}


代码中如何书写经过 UIHelper 绑定相应带有 节点的 长按事件

onBtnBackTouchLong(event) {
    cc.log(`......onBtnBackTouchLong........`);
}


5. 其他辅助方法

export default class UIHelper {
/** 检测前缀是否符合绑定规范 */
public static checkNodePrefix(name: string): boolean {
    let ret = name.match(/^_/);
    return ret !== null && ret !== undefined;
}
/** 获取绑定节点的前缀 */
public static getNodePrefix(name: string): string {
    let ret = name.match(/^_[a-z]*/);
   return ret ? ret[0] : null;
}
private static _getComponentName(component: cc.Component) {
    return component.name.match(/<.*>$/)[0].slice(1, -1);
}


6. 附组件简写参考


简称组件简称组件
_labcc.Label_btncc.Button
_spcc.Sprite_rtcc.RichText
_maskcc.Mask_mscc.MotionStreak
_tmcc.TiledMap_ttcc.TiledTile
_spinesp.Spine_ghcc.Graphics
_anicc.Animation_wvcc.WebView
_ebcc.EditBox_svcc.ScrollView
_vpcc.VideoPlayer_pbcc.ProgressBar
_pvcc.PageView_slcc.Slider
_tgcc.Toggle


                                               关注【游戏讲坛】微信公众号,获取最新动态!