簡介
遊戲開發中一個很重要的功能就是交互,如果沒有與用戶的交互,那麼遊戲將變成動畫,而處理用戶交互就需要使用事件監聽器了。
總概:
- 事件監聽器(cc.EventListener) 封裝用戶的事件處理邏輯
- 事件管理器(cc.eventManager) 管理用戶註冊的事件監聽器,根據觸發的事件類型分發給相應的事件監聽器
- 事件對象(cc.Event) 包含事件相關信息的對象
如何使用呢? 首先需要創建一個事件監聽器,事件監聽器包含以下幾種類型:
- 觸摸事件監聽器 (cc.EventListenerTouch)
- 鍵盤事件監聽器 (cc.EventListenerKeyboard)
- 加速計事件監聽器 (cc.EventListenerAcceleration)
- 鼠標事件監聽器 (cc.EventListenerMouse)
- 自定義事件監聽器 (cc.EventListenerCustom)
在監聽器中實現各種事件的處理邏輯,然後將監聽器加入到事件管理器中, 當事件觸發時,事件管理器會根據事件類型分發給相應的事件監聽器。下面以一個簡單的示例來演示使用的方法。
使用方法
現在會在一個場景中添加三個按鈕(cc.Sprite),三個按鈕將會互相遮擋,並且都需要能夠監聽和處理觸摸事件,以下是具體實現
首先創建三個精靈,作爲三個按鈕的顯示圖片
var sprite1 = new cc.Sprite("Images/CyanSquare.png");
sprite1.x = size.width/2 - 80;
sprite1.y = size.height/2 + 80;
this.addChild(sprite1, 10);
var sprite2 = new cc.Sprite("Images/MagentaSquare.png");
sprite2.x = size.width/2;
sprite2.y = size.height/2;
this.addChild(sprite2, 20);
var sprite3 = new cc.Sprite("Images/YellowSquare.png");
sprite3.x = 0;
sprite3.y = 0;
sprite2.addChild(sprite3, 1);
創建一個單點觸摸事件監聽器(事件類型:TOUCH_ONE_BY_ONE),並完成邏輯處理內容
// 創建一個事件監聽器 OneByOne 爲單點觸摸
var listener1 = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
swallowTouches: true, // 設置是否吞沒事件,在 onTouchBegan 方法返回 true 時吞掉事件,不再向下傳遞。
onTouchBegan: function (touch, event) { //實現 onTouchBegan 事件處理回調函數
var target = event.getCurrentTarget(); // 獲取事件所綁定的 target, 通常是cc.Node及其子類
// 獲取當前觸摸點相對於按鈕所在的座標
var locationInNode = target.convertToNodeSpace(touch.getLocation());
var s = target.getContentSize();
var rect = cc.rect(0, 0, s.width, s.height);
if (cc.rectContainsPoint(rect, locationInNode)) { // 判斷觸摸點是否在按鈕範圍內
cc.log("sprite began... x = " + locationInNode.x + ", y = " + locationInNode.y);
target.opacity = 180;
return true;
}
return false;
},
onTouchMoved: function (touch, event) { //實現onTouchMoved事件處理回調函數, 觸摸移動時觸發
// 移動當前按鈕精靈的座標位置
var target = event.getCurrentTarget();
var delta = touch.getDelta(); //獲取事件數據: delta
target.x += delta.x;
target.y += delta.y;
},
onTouchEnded: function (touch, event) { // 實現onTouchEnded事件處理回調函數
var target = event.getCurrentTarget();
cc.log("sprite onTouchesEnded.. ");
target.setOpacity(255);
if (target == sprite2) {
sprite1.setLocalZOrder(100); // 重新設置 ZOrder,顯示的前後順序將會改變
} else if (target == sprite1) {
sprite1.setLocalZOrder(0);
}
}
});
引擎提供了cc.EventListener.create統一來創建各類型的事件監聽器,可以通過指定不同的 event
來設置想要創建的監聽器類型,如上例中的cc.EventListener.TOUCH_ONE_BY_ONE
爲單點觸摸事件監聽器。
可選event
類型列表:
- cc.EventListener.TOUCH_ONE_BY_ONE (單點觸摸)
- cc.EventListener.TOUCH_ALL_AT_ONCE (多點觸摸)
- cc.EventListener.KEYBOARD (鍵盤)
- cc.EventListener.MOUSE (鼠標)
- cc.EventListener.ACCELERATION (加速計)
- cc.EventListener.CUSTOM (自定義)
將事件監聽器添加到事件管理器中
// 添加監聽器到管理器
cc.eventManager.addListener(listener1, sprite1);
cc.eventManager.addListener(listener1.clone(), sprite2);
cc.eventManager.addListener(listener1.clone(), sprite3);
這裏的cc.eventManager 是一個單例對象,可直接拿來使用。通過調用 addListener
函數可以將listener加入到管理器中,需要注意的是第二個參數,如果傳入的是一個Node對象,則加入的是SceneGraphPriority(精靈以顯示優先級)
類型的listener,如果是一個數值類型的參數,則加入到的是FixedPriority 類型的listener。
注意: 這裏當我們想給不同的節點使用相同的事件監聽器時,需要使用 clone()
函數克隆出一個新的監聽器,因爲在使用 addListener
方法時,會對當前使用的事件監聽器添加一個已註冊的標記,這使得它不能夠被添加多次。另外,有一點非常重要,FixedPriority
類型的 listener添加完之後需要手動刪除,而SceneGraphPriority 類型的 listener是跟node綁定的,在node調用cleanup時會被移除。具體的示例用法可以參考引擎自帶的tests。
更快速的添加事件監聽器到管理器的方式
下面提交一種更快捷綁定事件到節點的方式, 不過這樣做就不會得到監聽器的引用,無法再對監聽器進行其他操作,適用於一些簡單的事件操作, 代碼如下:
cc.eventManager.addListener({
event: cc.EventListener.TOUCH_ALL_AT_ONCE,
onTouchesMoved: function (touches, event) {
var touch = touches[0];
var delta = touch.getDelta();
var node = event.getCurrentTarget().getChildByTag(TAG_TILE_MAP);
var diff = cc.pAdd(delta, node.getPosition());
node.setPosition(diff);
}
}, this);
cc.eventManager的 addListener
的第一個參數也支持兩種類型的參數: cc.EventListener
類型對象和json格式的對象,如果是json格式對象,方法會根據傳入的event
屬性來創建對應的監聽器。
新的觸摸機制
以上的步驟相對於 2.x 版本觸摸機制實現,稍顯複雜了點。在老的版本中只需在節點中重載onTouchBegan
/onTouchesBegan
等方法,
處理對應的觸摸事件邏輯,然後調用cc.registerTargetedDelegate
或cc.registerStandardDelegate
將節點加入到觸摸事件分發器中就可以了,甚至有些已封裝的類只需要調用setTouchEnabled
,
就可以開啓觸摸事件,比如:cc.Layer
。
而新機制將事件處理邏輯獨立出來,封裝到一個 監聽器(listner) 中,使得不同對象可以使用同一份監聽器代碼(使用clone()
來達到目的)。另外,cc.eventManager加入了精靈以顯示優先級
(SceneGraphPriority)排序的功能,以這種類型註冊的監聽器,事件管理器會根據屏幕顯示的情況來決定事件會優化分發給哪個事件監聽器。 而2.x要實現這樣的功能卻非常的麻煩,需要用戶自己通過調用setPriority
來管理節點的事件響應優先級。
注意:與 SceneGraphPriority 所不同的是 FixedPriority 將會依據手動設定的 Priority
值來決定事件相應的優先級,值越小優先級越高,
後面章節中會作更具體的講解。
其它事件派發處理模塊
除了觸摸事件響應之外,還可以使用相同的事件處理方式來處理其他事件。
鍵盤響應事件
除了可以監聽鍵盤按鍵,還可以是終端設備的各個菜單鍵,都能使用同一個監聽器來進行處理。
//給statusLabel綁定鍵盤事件
cc.eventManager.addListener({
event: cc.EventListener.KEYBOARD,
onKeyPressed: function(keyCode, event){
var label = event.getCurrentTarget();
//通過判斷keyCode來確定用戶按下了哪個鍵
label.setString("Key " + keyCode.toString() + " was pressed!");
},
onKeyReleased: function(keyCode, event){
var label = event.getCurrentTarget();
label.setString("Key " + keyCode.toString() + " was released!");
}
}, statusLabel);
加速計事件
在使用加速計事件監聽器之前,需要先啓用此硬件設備, 代碼如下:
cc.inputManager.setAccelerometerEnabled(true);
然後將相應的事件處理監聽器與sprite進行綁定就可以了,如下:
cc.eventManager.addListener({
event: cc.EventListener.ACCELERATION,
callback: function(acc, event){
//這裏處理邏輯
}
}, sprite);
鼠標響應事件
對於PC和超級本,添加鼠標事件的的處理,可以加強用戶的體驗,其處理邏輯與觸摸事件基本一樣,多了一些鼠標特有的事件響應,如滾輪事件(onMouseScroll).
cc.eventManager.addListener({
event: cc.EventListener.MOUSE,
onMouseMove: function(event){
var str = "MousePosition X: " + event.getLocationX() + " Y:" + event.getLocationY();
// do something...
},
onMouseUp: function(event){
var str = "Mouse Up detected, Key: " + event.getButton();
// do something...
},
onMouseDown: function(event){
var str = "Mouse Down detected, Key: " + event.getButton();
// do something...
},
onMouseScroll: function(event){
var str = "Mouse Scroll detected, X: " + event.getLocationX() + " Y:" + event.getLocationY();
// do something...
}
},this);
注意: 由於在PC瀏覽器中,沒有觸摸事件,而此時強制要求用戶寫鼠標事件的響應代碼,必然會讓開發者多寫很多代碼,事實上觸摸響應的邏輯與鼠標相差不大,所以引擎在檢測到不支持觸摸事件時,會讓鼠標事件模擬成觸摸事件進行分發,開發者只需編寫觸摸事件監聽器就能完成大部分工作,而對於針對鼠標操作而設計的遊戲,需要判斷用戶按下什麼鍵,響應滾輪等,這就需要開發者編寫鼠標事件監聽器了。
(開發者反饋,鼠標事件監聽器也需要有swallowTouches這個選項,我們將會有v3.1版本中加入這個項.)
自定義事件
以上是系統自帶的事件類型,這些事件由系統內部自動觸發,如 觸摸屏幕,鍵盤響應等,除此之外,還提供了一種 自定義事件,簡而言之,它不是由系統自動觸發,而是人爲的干涉,如下:
var _listener1 = cc.EventListener.create({
event: cc.EventListener.CUSTOM,
eventName: "game_custom_event1",
callback: function(event){
// 可以通過getUserData來設置需要傳輸的用戶自定義數據
statusLabel.setString("Custom event 1 received, " + event.getUserData() + " times");
}
});
cc.eventManager.addListener(this._listener1, 1);
以上定義了一個 “自定義事件監聽器”,實現了一些邏輯, 並且添加到事件分發器。那麼以上邏輯是在什麼情況下響應呢?請看如下:
++this._item1Count;
var event = new cc.EventCustom("game_custom_event1");
event.setUserData(this._item1Count.toString());
cc.eventManager.dispatchEvent(event);
創建了一個自定義事件(EventCustom
)對象 ,並且設置了其用戶自定義(UserData)數據,手動調用cc.eventManager.dispatchEvent(event);
將此事件分發出去,從而觸發之前監聽器中所實現的邏輯。
cc.eventManager加入自定義事件的處理,開發者就可以很方便的使用該功能來實現觀察者模式。
移除事件監聽器
我們可以通過以下方法移除一個已經被添加了的監聽器。
cc.eventManager.removeListener(listener); //移除一個已添加的監聽器
也可以使用removeListeners
,移除註冊到cc.eventManager
中指定類型的所有監聽器,當然使用該函數時,傳入的參數如果是一個節點(cc.Node及其子類)對象,
事件管理器將移除與該對象相關的所有事件監聽器, 代碼如下:
cc.eventManager.removeListeners(cc.EventListener.TOUCH_ONE_BY_ONE); //移除所有單點觸摸事件監聽器
cc.eventManager.removeListeners(aSprite); //移除所有與aSprite相關的監聽器
事件管理器還提供了函數用來移除已註冊的所有監聽器。
cc.eventManager.removeAllListeners();
當使用 removeAllListeners
的時候,此節點的所有的監聽將被移除,推薦使用 指定刪除的方式。
_注意:調用removeAllListeners
之後 菜單(cc.Menu
)
也不能響應。因爲它內部有一個觸摸事件監聽器,也會從事件管理器中刪除。
暫停/恢復 與場景相關(SceneGraph類型)的監聽器
開發過程中,我們經常會遇到這樣的情況:想要讓一個Layer中所有的Node對象的事件都停止響應。 在響應用戶事件後,又要恢復該Layer的所有事件響應。如: 用戶想要顯示一個模式對話框,顯示對話框後,禁止對話框後所有對象的事件響應。 在用戶關閉對話框後,又恢復這些對象的事件響應。
我們只需要暫停根node的事件,就可以讓根節點以及其子節點暫停事件響應。 代碼如下:
cc.eventManager.pauseTarget(aLayer, true); //讓aLayer對象暫停響應事件
而恢復對象的事件響應也非常簡單:
cc.eventManager.resumeTarget(aLayer, true); //讓aLayer對象恢復響應事件
注意: 第二個參數爲可選參數,默認值爲false, 表示是否遞歸調用子節點的暫停/恢復操作.
進階話題
SceneGraphPriority類型與FixedPriority類型詳解
事件管理器將監聽器類型分爲兩大類:SceneGraphPriority和FixedPriority, 下面將會詳細說明它們之間的區別, 並介紹FixedPriority的使用場景與使用方法。
SceneGraphPriority事件類型是指事件的響應優先級與監聽器關聯對象在場景中顯示順序(zOrder)相關, 比如一個精靈對象在場景的顯示在最上層時,它對事件的響應優先級最高。 這樣開發者就不需要再像v2.x中那樣在場景對象的zOrder變化後,手動再調用setPriority
來改變相應的優先級了,這些事將交由管理器來處理。
而 FixedPriority 事件類型則是相對於 SceneGraphPriority 來定義的,不需要與場景顯示順序相關的事件監聽器 也就是優化級固定的(fixed),就可以註冊成FixedPriority類型事件。 我們的SceneGraphPriority定義的系統優先級是0, 在添加監聽器(addListener
)時,
如果第二個參數設置爲負數時,該監聽器就會擁有比所有場景相關監聽器都高的優先級, 而如果是正數,則反之。
那麼什麼情況下使用FixedPriority類型的監聽器呢? 比如,一個冒險類的遊戲中,遊戲主角應該要最先響應觸摸事件,而UI界面的按鈕往往會安排在界面的最上層。但是,如果主角移動到了按鈕的後面,這時點擊遊戲主角,如果遊戲主角註冊的是SceneGraphPriority類型監聽器,響應的將會是按鈕事件。而如果註冊成FixedPriority類型,並把它的優先級設置爲負數,將會響應遊戲主角的事件。
有開發者反饋想保持他們在v2.x的響應優先級管理機制,因爲他們有特殊的需求,那麼這部分開發者也可以使用FixedPriority來管理,cc.eventManager
也提供了一個setPriority
函數來管理優先級。
UI控件的事件處理詳解
Cocos提供一套UI控件,許多開發者對於控件的事件響應,特別是對於容器類控件(如:ccui.PageView, ccui.ScrollView)的事件響應有些疑惑。這裏將詳細說明控件的事件處理流程。
首先來看一下ccui.Widget
的事件實現, 所有的控件的事件監聽器都是單點觸摸事件,並且會吞食事件,註冊代碼如下:
this._touchListener = cc.EventListener.create({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
swallowTouches: true,
onTouchBegan: this.onTouchBegan.bind(this),
onTouchMoved: this.onTouchMoved.bind(this),
onTouchEnded: this.onTouchEnded.bind(this)
});
cc.eventManager.addListener(this._touchListener, this);
然後看一下它的各事件響應函數,會發現每個函數都會有類似這樣的語句: if (widgetParent) widgetParent.interceptTouchEvent(ccui.Widget.TOUCH_XXXXX, this,
touch);
這句的意思是,在控件處理完自己的觸摸事件之後,都會向父節點(widgetParent)發送事件響應通知。那麼,interceptTouchEvent的實現是什麼呢? 代碼如下:
interceptTouchEvent: function(eventType, sender, touch){
var widgetParent = this.getWidgetParent();
if (widgetParent)
widgetParent.interceptTouchEvent(eventType,sender,touch);
}
對於像ccui.Button, ccui.ImageView這樣的控件,它只是簡單的向父類發送事件通知就行了,而對於像ccui.PageView這樣的容器類控件,會對這些通知做出響應,代碼如下:
interceptTouchEvent: function (eventType, sender, touch) {
var touchPoint = touch.getLocation();
switch (eventType) {
case ccui.Widget.TOUCH_BEGAN:
this._touchBeganPosition.x = touchPoint.x;
this._touchBeganPosition.y = touchPoint.y;
break;
case ccui.Widget.TOUCH_MOVED:
this._touchMovePosition.x = touchPoint.x;
this._touchMovePosition.y = touchPoint.y;
var offset = 0;
offset = Math.abs(sender.getTouchBeganPosition().x - touchPoint.x);
if (offset > this._childFocusCancelOffset) {
sender.setFocused(false);
this._handleMoveLogic(touch);
}
break;
case ccui.Widget.TOUCH_ENDED:
case ccui.Widget.TOUCH_CANCELED:
this._touchEndPosition.x = touchPoint.x;
this._touchEndPosition.y = touchPoint.y;
this._handleReleaseLogic(touch);
break;
}
}
這樣的處理,就能實現在按鈕上滑動時,也能讓其父節點的PageView觸摸事件。不然,如果不採用這種機制,當一個PageView中填滿了子控件時,PageView將無法響應觸摸事件。
屬性與方法列表
cc.Event (事件類)
屬性/方法 | 類型 | 參數說明 | 用法說明 |
---|---|---|---|
getType | Number | no | 返回事件類型,包含:TOUCH, KEYBOARD, ACCELERATION, MOUSE, CUSTOM |
stopPropagation | void | no | 停止當前事件的冒泡 |
isStopped | Boolean | no | 事件是否已停止 |
getCurrentTarget | cc.Node | no | 返回事件相關的Node對象, 如果事件未與cc.Node對象關聯,則返回null |
cc.EventCustom (自定義事件)
cc.EventCustom
繼承自 cc.Event
屬性/方法 | 類型 | 參數說明 | 用法說明 |
---|---|---|---|
setUserData | void | data: 要設置的自定義數據 | 設置用戶自定義數據 |
getUserData | * | no | 返回用戶設置的自定義數據 |
getEventName | String | no | 返回自定義事件名稱 |
cc.EventMouse (鼠標事件)
cc.EventMouse
繼承自 cc.Event
屬性/方法 | 類型 | 參數說明 | 用法說明 |
---|---|---|---|
setScrollData | void | scrollX, scrollY | 設置滾輪數據 |
getScrollX | Number | no | 返回x軸滾輪數據 |
getScrollY | Number | no | 返回y軸滾輪數據 |
setLocation | void | x, y | 設置鼠標光標位置 |
getLocation | cc.Point | no | 獲取鼠標光標位置 |
getLocationInView | cc.Point | no | 返回鼠標光標在屏幕上的位置 |
getDelta | cc.Point | no | 獲取當前光標與上一光標的偏移量 |
setButton | void | button | 設置鼠標按鍵 |
getButton | Number | no | 獲取鼠標按鍵 |
cc.EventTouch ()
cc.EventTouch
繼承自 cc.Event
屬性/方法 | 類型 | 參數說明 | 用法說明 |
---|---|---|---|
getEventCode | Number | no | 獲取觸摸事件類型代碼: BEGAN, MOVED, ENDED, CANCELLED |
getTouches | Array | no | 獲取觸摸事件中所有點信息 |
cc.EventListener (事件監聽器)
屬性/方法 | 類型 | 參數說明 | 用法說明 |
---|---|---|---|
checkAvailable | boolean | no | 檢測監聽器是否有效 |
clone | cc.EventListener | no | 克隆一個監聽器,其子類會重寫本函數 |
create | cc.EventListener | json object | 通過json對象創建事件監聽器 |
cc.EventListener.create
函數參數列表:
創建EventListenerTouchOneByOne對象:
event: cc.EventListener.TOUCH_ONE_BY_ONE
可選參數:
- swallowTouches, boolean, 是否吞下該touch點
- onTouchBegan, function, TouchBegan 事件回調
- onTouchMoved, function, TouchMoved 事件回調
- onTouchEnded, function, TouchEnded 事件回調
- onTouchCancelled, function, TouchCancelled 事件回調
創建EventListenerTouchAllAtOnce對象:
event: cc.EventListener.TOUCH_ALL_AT_ONCE
可選參數:
- onTouchesBegan, function, TouchesBegan 事件回調
- onTouchesMoved, function, TouchesMoved 事件回調
- onTouchesEnded, function, TouchesEnded 事件回調
- onTouchesCancelled, function, TouchesCancelled 事件回調
創建EventListenerKeyboard對象:
event: cc.EventListener.KEYBOARD
可選參數:
- onKeyPressed, function, KeyPressed (鍵按下) 事件回調
- onKeyReleased, function, keyRelease (鍵放開) 事件回調
創建EventListenerMouse對象:
event: cc.EventListener.MOUSE
可選參數:
- onMouseDown, function, MouseDown 事件回調
- onMouseUp, function, MouseUp 事件回調
- onMouseMove, function, MouseMove 事件回調
- onMouseScroll, function, MouseScroll 事件回調
創建EventListenerAcceleration對象:
event: cc.EventListener.ACCELERATION
可選參數:
- callback, function, Acclerometer 事件回調
創建EventListenerCustom對象:
event: cc.EventListener.CUSTOM
可選參數:
- callback, function, 自定義事件回調
cc.eventManager
屬性/方法 | 類型 | 參數說明 | 用法說明 |
---|---|---|---|
pauseTarget | void | node, recursive(是否遞歸調用子類) | 暫停傳入的node相關的所有監聽器的事件響應 |
resumeTarget | void | node, recursive | 恢復傳入的node相關的所有監聽器的事件響應 |
addListener | void | json對象或cc.EventListener, node對象或優化值 | 向事件管理器添加一個監聽器 |
addCustomListener | void | eventName, callback | 向事件管理器添加一個自定義事件監聽器 |
removeListener | void | listener | 移除一個事件監聽器 |
removeListeners | void | listenerType | cc.Node, recursive |
removeCustomListeners | void | customEventName | 移除同一事件名的自定義事件監聽器 |
removeAllListeners | void | no | 移除所有事件監聽器 |
setPriority | void | listener, fixedPriority | 設置FixedPriority類型監聽器的優先集 |
setEnabled | void | enabled | 是否允許分發事件 |
isEnabled | boolean | no | 檢測事件管理器是否分發事件 |
dispatchEvent | void | event | 分發事件 |
dispatchCustomEvent | void | eventName, optionalUserData | 分發自定義事件 |