在cocos2d-js實現自動綁定cocostudioUI控制項與事件,cocos2djs事件
一.起因
在用戶端遊戲開發中最讓人噁心的工作就是UI相關的東西,雖然有了像cocostudio這樣的視覺化檢視,但介面中有大量需要由代碼訪問的控制項的時候,需要寫太多重複的代碼例如:
//載入UI設定檔 var root = ccs.uiReader.widgetFromJsonFile("res/cocosui/UIEditorTest/UIButton_Editor/UIButton_Editor_1.json"); this._mainNode.addChild(root); //查詢back控制項,並添事件響應 var back_label = ccui.helper.seekWidgetByName(root, "back"); back_label.addTouchEventListener(this.backEvent,this); //查詢Button_123控制項,並添事件響應 var button = root.getChildByName(root, "Button_123"); button.addTouchEventListener(this.touchEvent,this);</span> 上面是最為直接存取控制項的方法。問題在於如果一個UI介面中有十個、二十個甚至更多的UI控制項需要操作的是候,我們做UI的介面、邏輯開發的同學們是否還有時間愉快的玩耍?有沒有什麼方法不需要去手寫這些代碼,就能愉快的訪問UI組件與接收UI事件響應呢?
二. 思考
對於手機遊戲,特別是卡牌類的遊戲來說70%~80%的用戶端工作量是在UI布局與邏輯上。 上面的seekWidgetByName、getChildByName、addTouchEventListener函數將大量充斥在用戶端代碼中,濫竽充數著我們的程式碼數。
自己曾經有過Qt的開發經驗。Qt中也有自己的UI設計工具,產生xml的ui設定檔. 對於這個xml有兩種使用方式: 第一種方式: 使用Qt自己的編譯工具,將xml翻譯產生一個c++代碼檔案,代碼內容就是根據xml中的資訊建立各種控制項,設定座標\屬性\事件等。 第二種方式: 在程式中,將xml檔案使用UILoader工具類載入進來,成為一個節點。然後調用Qt的函數實現訊號/槽的自動關聯。實現訊號/槽(事件)自動綁定的原理,是要求寫一個事件處理函數,格式為:
void on_控制項名_訊號名(參數);
具體如何使用就不細說了,有興趣的朋友可以自己去看看。根據Qt的這個功能提示,我們何嘗不可以在cocos2d-js自動綁定coccostudio輸出的ui檔案呢?
三. 名命約定
1. 代碼名命約定
根據cocos2d-js代碼風格,我們約定: (1)類成員變數以底線 "_"開頭後面接以駝峰名命格式的英文單詞。例如:_loginButton 、 _closeButton 、 _nameLabel (2)類中的私人函數也使用同樣的方式。 例如:_onLoginButtonTouchBegan: function() { ...}
2. UI命名規範
在cocostudioUI編輯器中,我們遵循上述代碼中成員變數的命名規範。將需要由代碼訪問的控制項取名為"_"開頭,後面接以駝峰名命格式的英文單詞。 請看:
3. ccui控制項事件命名
ccui.Widget事件註冊有兩種:1). 常規的touch事件有:ccui.Widget.TOUCH_BEGAN 觸摸開始 (按下)ccui.Widget.TOUCH_MOVED 觸摸移動 (移動)ccui.Widget.TOUCH_ENDED 觸摸結束 (抬起)
ccui.Widget.TOUCH_CANCELED 觸摸取消 (一般沒用)我們使用widget.addTouchEventListener(selector, target)給控制項註冊觸摸事件,設定回呼函數.
2). 控制項特殊事件:比如CheckBox的:ccui.CheckBox.EVENT_SELECTEDccui.CheckBox.EVENT_UNSELECTED又如:TextField的:ccui.TextField.EVENT_ATTACH_WITH_IMEccui.TextField.EVENT_DETACH_WITH_IMEccui.TextField.EVENT_INSERT_TEXTccui.TextField.EVENT_DELETE_BACKWARD這類事件需要使用widget.addEventListener(selector, target)來註冊。
這裡的selector就是我們的回呼函數,需要我們取名並實現這個函數,事件類型是通過參數來識別的:
ctor: function() {this._super();...button.addTouchEventListener(this._onButtonEvent, this);},_onButtonEvent: function(sender, type) {switch(type) {case ccui.Widget.TOUCH_BEGAN:...;return true;case ccui.Widget.TOUCH_MOVED:...;break;case ccui.Widget.TOUCH_ENDED:...;break;}}
這個"_onButtonEvent"就是我們為事件函數取的名字,如果我們按:【
首碼+控制項名(取掉底線)+事件名】給控制項事件函數取名舉例說明:控制項名字為:_button事件名則為:_onButtonTouchBegan、_onButtonTouchMoved、_onButtonTouchEnded
四. 代碼實現
有了上面的約定,我們可以開始實現UI的綁定了。1. 定義一個自動綁定的控制項列表,我們這裡列出了常用的控制項類型與事件名字。
//觸摸事件sz.UILoader.touchEvents = ["TouchBegan", "TouchMoved", "TouchEnded"];//控制項事件列表sz.UILoader.widgetEvents = [ //Button {widgetType: ccui.Button, events: sz.UILoader.touchEvents}, //ImageView {widgetType: ccui.ImageView, events: sz.UILoader.touchEvents}, //TextFiled {widgetType: ccui.TextField, events: ["AttachWithIME", "DetachWithIME", "InsertText", "DeleteBackward"]}, //CheckBox {widgetType: ccui.CheckBox, events: ["Selected", "Unselected"]}, //ListView {widgetType: ccui.ListView, events:["SelectedItem"]}, //Panel {widgetType: ccui.Layout, events: sz.UILoader.touchEvents}, //BMFont {widgetType: ccui.TextBMFont, events: sz.UILoader.touchEvents}, //last must null null];這個sz.UILoader.widgetEvents數組可以根據需要自己添加需要綁定的組件。
2.邏輯流程1). 使用loader載入ui檔案並傳入target為當前layer。所有事件和控制項變數都將綁定到target上。2). 遍曆載入後的子節點(childNode),檢查名字首碼是否以”_”開頭。並且該節點類型是否在widgetEvents數組中。
3). 將childNode綁定到target上。
4). 提取childNode事件函數名,檢查target是否有這些函數存在。
5). 為widgetNode註冊事件響應。6). 載入類接收到事件響應,轉寄事件到對應的target的事件處理函數上。
3.UI載入類的具體實現
sz.UILoader = cc.Class.extend({ _eventPrefix: null, _memberPrefix: null, /** * 載入UI檔案 * @param target將 jsonFile載入出的節點綁定到的目標 * @param jsonFile cocostudio UI編輯器產生的json檔案 */ widgetFromJsonFile: function(target, jsonFile, options) { cc.assert(target && jsonFile); if (!options) { options = {}; } this._eventPrefix = options.eventPrefix || sz.UILoader.DEFAULT_EVENT_PREFIX; this._memberPrefix = options.memberPrefix || sz.UILoader.DEFAULT_MEMBER_PREFIX; var rootNode = ccs.uiReader.widgetFromJsonFile(jsonFile); if (!rootNode) { cc.log("Load json file failed"); } target.rootNode = rootNode; target.addChild(rootNode); this._bindMenbers(rootNode, target); }, /** * 遞迴對rootWidget下的子節點進行成員綁定 * @param rootWidget * @param target * @private */ _bindMenbers: function(rootWidget, target) { var widgetName, children = rootWidget.getChildren(); var self = this; children.forEach(function(widget) { widgetName = widget.getName(); //控制項名存在,綁定到target上 var prefix = widgetName.substr(0, self._memberPrefix.length); if (prefix === self._memberPrefix) { target[widgetName] = widget; self._registerWidgetEvent(target, widget); } //綁定子控制項,可以實現: a._b._c._d 訪問子控制項 if (!rootWidget[widgetName]) { rootWidget[widgetName] = widget; } //如果還有子節點,遞迴進去 if (widget.getChildrenCount()) { self._bindMenbers(widget, target); } }); }, /** * 擷取控制項事件 * @param widget * @returns {*} */ _getWidgetEvent: function(widget) { var bindWidgetEvent = null; var events = sz.UILoader.widgetEvents; for (var i = 0; i < events.length; i++) { bindWidgetEvent = events[i]; if (widget instanceof bindWidgetEvent.widgetType) { break; } } return bindWidgetEvent; }, /** * 註冊控制項事件 * @param target * @param widget * @private */ _registerWidgetEvent: function(target, widget) { var name = widget.getName(); //截取首碼,首字母大寫 var newName = name[this._memberPrefix.length].toUpperCase() + name.slice(this._memberPrefix.length + 1); var eventName = this._eventPrefix + newName + "Event"; var isBindEvent = false; if (target[eventName]) { isBindEvent = true; } else { //取事控制項件名 var widgetEvent = this._getWidgetEvent(widget); if (!widgetEvent) { return; } //檢查事函數,建置事件名數組 var eventNameArray = []; for (var i = 0; i < widgetEvent.events.length; i++) { eventName = this._eventPrefix + newName + widgetEvent.events[i]; eventNameArray.push(eventName); if (cc.isFunction(target[eventName])) { isBindEvent = true; } } } //事件響應函數 var self = this; var eventFunc = function(sender, type) { var callBack; if (eventNameArray) { var funcName = eventNameArray[type]; callBack = target[funcName]; } else { callBack = target[eventName]; } if (self._onWidgetEvent) { self._onWidgetEvent(sender, type); } if (callBack) { return callBack.call(target, sender, type); } }; //註冊事件監聽 if (isBindEvent) { widget.setTouchEnabled(true); if (widget.addEventListener) { widget.addEventListener(eventFunc, target); } else { widget.addTouchEventListener(eventFunc, target); } } } });sz.uiloader = new sz.UILoader();
所有東西都已經準備好了, 我們看看怎麼在具體的代碼中使用:
GameLayer = cc.Layer.extend({ctor: function() {this._super();//載入UI檔案,繫結控制項和事件到thissz.uiloader.widgetFromJsonFile(this, "res/DemoLogin.ExportJson");//立即可以訪問控制項的屬性方法了cc.log(this._closeButton.getName());},/* * _closeButton的TouchBegan事件處理函數 */_onCloseButtonTouchBegan: function(sender) {cc.log("_onCloseButtonTouchBegan");},});
現在看看我的用戶端代碼是不是比之前的簡潔多了!
五.首碼的問題上面花了大量文字講解了關於命名的問題,有人可能會覺得使用這個UILoader會強姦他的代碼。因為他沒有使用“_”做為成員變數首碼,或是成員變數首碼不是“_”而是“m_”。為了不強姦別人的代碼,提供了下面的選項:
sz.uiloader.widgetFromJsonFile(this, "res/DemoLogin.ExportJson", {eventPerfix:"on", memberPrefix:"m_"} );最後一個選擇性參數options對象,有兩個屬性eventPerfix 、memberPrefix用於配置事件首碼和成員變數首碼
六. 首碼+控制項名+Event
有些時候,不想將TouchBegan、TouchMoved、TouchEnded分成三個響應函數分別來寫,而是使用原來事件參數來判斷事件類型。這時你只需要實現以【首碼+控制項名+Event】的函數名例如:控制項名為_loginButton, 定義一個函數如:
_onLoginButtonEvent: function(sender, type) {switch (type) {case 0:cc.log("_onLoginButtonEvent: began");break;case 1:cc.log("_onLoginButtonEvent: move");break;case 2:cc.log("_onLoginButtonEvent: end");break;}},這時UILoader會優先使用這個事件處理函數,如果同時也實現了一個"_onLoginButtonTouchBegan",它將不會被執行。這樣再次阻止sz.UILoader強姦事件的發生,可以相容你原來的代碼。
完整代碼可以到githut下載: https://github.com/ShawnZhang2015/UILoader