1. 程式人生 > >【cocos2d-js官方文件】十七、事件分發機制

【cocos2d-js官方文件】十七、事件分發機制

簡介

遊戲開發中一個很重要的功能就是互動,如果沒有與使用者的互動,那麼遊戲將變成動畫,而處理使用者互動就需要使用事件監聽器了。

總概:

  • 事件監聽器(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);

touchable_sprite

建立一個單點觸控事件監聽器(事件型別: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型別列表:

  1. cc.EventListener.TOUCH_ONE_BY_ONE (單點觸控)
  2. cc.EventListener.TOUCH_ALL_AT_ONCE (多點觸控)
  3. cc.EventListener.KEYBOARD (鍵盤)
  4. cc.EventListener.MOUSE (滑鼠)
  5. cc.EventListener.ACCELERATION (加速計)
  6. 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.registerTargetedDelegatecc.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

屬性/方法 型別 引數說明