上一篇博文講到,在沒有修改 優先級 的情況 和 沒有設定 setSwallowTouches 為true 的情況下,事件機制是 從下到上,從後到前 的順序來分發事件的。
其實這樣描述還是不夠準确,是以就來研究一下這個事件分發的優先級到底受什麼因素影響,要怎麼做可以達到想要的效果。
本博文基于以下環境:
Cocos2D-x v3.2
vc++ 2012
win7
=============================正文開始=====================================
第一種情況,使用 addEventListenerWithSceneGraphPriority() 方法注冊監聽。
在官網上,對這個方法的解釋是,通過添加事件監聽器,将精靈以顯示優先級 (SceneGraphPriority) 添加到事件分發器。這就是說,當我們點選精靈按鈕時,根據螢幕顯示的“遮蓋”實際情況,進行有序的函數回調。
什麼意思呢,就是 在前面顯示的 ,優先得到回調(也就是事件分發)。比如遊戲中,顯示在前面的UI,比地圖優先得到回調;同一個場景中,顯示在上面的層,比下面的優先得到回調。
我們來解釋一下其中的原理。
這個函數所添加的所有事件,優先級都預設為0,也就是最小。
而事件分發的先後由所綁定的節點與螢幕的遠近來判斷,也就是離螢幕近,該節點就顯示在上層,優先得到事件分發。
如何影響節點的繪制遠近?
有兩個函數:setLocalZOder(int localZOder) 和 setGlobalZOrder(int globalZOder)。
globalZOrder 是用于 渲染器 中用來給“繪制指令”排序的。
localZOrder 是用于父節點的子節點數組中給 節點 對象排序的。
這兩個函數都會影響到 節點在螢幕上的層次分布,而globalZOder更徹底,直接控制着渲染的次序,值越大,離螢幕就越近。
說說兩個函數用法:
localZOder:
第一個,在 addchild 時,之間傳入。
auto sprite1 = Sprite::create("CyanSquare.png");
sprite1->setAnchorPoint(Vec2(0.5,0.5));
sprite1->setPosition(Vec2(origin.x + visibleSize.width/2 +50 , origin.y + visibleSize.height/2 + 50 ));
this->addChild(sprite1,1); //後一個參數為 localZOder,即 z 軸大小。
第二個修改localZOder的方法是直接傳入。
auto sprite1 = Sprite::create("CyanSquare.png");
sprite1->setAnchorPoint(Vec2(0.5,0.5));
sprite1->setPosition(Vec2(origin.x + visibleSize.width/2 -75 , origin.y + visibleSize.height/2 - 75 ));
sprite1->setLocalZOrder(1);//效果等同。
this->addChild(sprite1);
要注意的是,子節點的Z軸受父節點影響,如果一個父節點 Z軸值 比 另一個父節點的 Z軸值 大,那麼不論第二個父節點的子節點設定多大的 localZOder值,都會顯示在第一個父節點以及它的子節點下面。
而子節點,會顯示在父節點上面。
請看下面代碼:
auto sprite1 = Sprite::create("CyanSquare.png");
sprite1->setAnchorPoint(Vec2(0.5,0.5));
sprite1->setPosition(Vec2(origin.x + visibleSize.width/2 -75 , origin.y + visibleSize.height/2 - 75 ));
sprite1->setLocalZOrder(1);
this->addChild(sprite1);
auto sprite2 = Sprite::create("MagentaSquare.png");
sprite2->setAnchorPoint(Vec2(0.5,0.5));
sprite2->setPosition(Vec2(origin.x , origin.y ));
auto sprite3 = Sprite::create("YellowSquare.png");
sprite3->setAnchorPoint(Vec2(0.5,0.5));
sprite3->setPosition(Vec2(origin.x + visibleSize.width/2 -50, origin.y + visibleSize.height/2 - 50));
sprite3->addChild(sprite2);
this->addChild(sprite3);
auto listener1 = EventListenerTouchOneByOne::create();
listener1->setSwallowTouches(false);
listener1->onTouchBegan = [](Touch* touch , Event* event ) {
log( "sprite1 is on touch began");
return true;
};
listener1->onTouchEnded = [](Touch* touch , Event* event) {
log( "sprite1 is on touch ended ");
};
listener1->onTouchMoved = [](Touch* touch , Event* event ) {
log( "sprite1 is on touch moved" );
};
auto listener2 = EventListenerTouchOneByOne::create();
listener2->setSwallowTouches(false);
listener2->onTouchBegan = [](Touch* touch , Event* event ) {
log( "sprite2 is on touch began");
return true;
};
listener2->onTouchEnded = [](Touch* touch , Event* event) {
log( "sprite2 is on touch ended ");
};
listener2->onTouchMoved = [](Touch* touch , Event* event ) {
log( "sprite2 is on touch moved" );
};
auto listener3 = EventListenerTouchOneByOne::create();
listener3->onTouchBegan = [](Touch* touch , Event* event ) {
log("sprite3 is on touch");
return true;
};
listener3->onTouchMoved = [](Touch* touch , Event* event ) {
log("sprite3 is on touch move" );
};
listener3 -> onTouchEnded = [](Touch* touch , Event* event ) {
log( "sprite3 is on touch end ");
};
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener1 , sprite1 );
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener2 , sprite2 );
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener3 ,sprite3 );
在上面代碼中,sprite1的 localZOder 比 sprite3 要大,而sprite2是 sprite3 的子節點。
是以顯示如圖:
sprite1 在 sprite2 和 sprite3 上面,第一個顯示,而sprite2 作為 sprite3 的節點在第二位顯示,最後是sprite3顯示。
點選螢幕任意一點,得到 事件監聽的回報如圖:
sprite1、2、3分别依次調用,和它們在螢幕上的顯示排序一緻。
globalZOrder :
auto sprite3 = Sprite::create("YellowSquare.png");
sprite3->setAnchorPoint(Vec2(0.5,0.5));
sprite3->setPosition(Vec2(origin.x + visibleSize.width/2 -50, origin.y + visibleSize.height/2 - 50));
sprite3->addChild(sprite2);
this->addChild(sprite3);
sprite3->setGlobalZOrder(100);
和上面的程式唯一不同的地方就是,在添加sprite3之後,将它的 globalZOrder 設定為100。
看效果:
可以看到,原本在底下的黃色sprite3跑到了最上面,也第一個觸發了事件監聽。
*******************************************************************************************************
說完第一種用 addEventListenerWithSceneGraphPriority() 方法注冊監聽的方法,說說第二種,用addEventListenerWithFixedPriority()。
第二種方法的使用方式和第一種不一樣 。
這個方法的傳參是(EventListener * listener,int fixedPriority),第一個是 事件監聽器,第二個是事件優先級。它沒有像第一種方法那樣,直接綁定事件對象,它的預設綁定對象是目前節點,如果這條語句寫在layer裡面,那麼注冊的節點就是這個layer的執行個體,sprite等同理。
也就是說,這個方法大多數時候都用在我們的自定義類裡面,比如繼承sprite,寫我們自己的sprite,隻要傳入一個 priority(優先級)作為參數,就可以讓這個自定義的顯示類自帶一個事件監聽器而不用在外面進行注冊了。
看源碼:NewEventDispatcherTest.cpp
class TouchableSprite : public Sprite //繼承sprite的自定義類
{
public:
static TouchableSprite* create(int priority = 0)//靜态建立函數,傳入優先級,傳回一個自定義的sprite
{
auto ret = new TouchableSprite(priority);
if (ret && ret->init())
{
ret->autorelease();
}
else
{
CC_SAFE_DELETE(ret);
}
return ret;
}
protected:
TouchableSprite(int priority)
: _listener(nullptr)
, _fixedPriority(priority)
, _removeListenerOnTouchEnded(false)
{
}
public:
void onEnter() override //在建立到舞台的時候調用
{
Sprite::onEnter();
auto listener = EventListenerTouchOneByOne::create();//建立事件監聽
listener->setSwallowTouches(true);
listener->onTouchBegan = [=](Touch* touch, Event* event){
Vec2 locationInNode = this->convertToNodeSpace(touch->getLocation());
Size s = this->getContentSize();
Rect rect = Rect(0, 0, s.width, s.height);
if (rect.containsPoint(locationInNode))
{
this->setColor(Color3B::RED);
return true;
}
return false;
};
listener->onTouchEnded = [=](Touch* touch, Event* event){
this->setColor(Color3B::WHITE);
if (_removeListenerOnTouchEnded)
{
_eventDispatcher->removeEventListener(listener);
}
};
if (_fixedPriority != 0) //如果傳入的優先級不等于0,那麼就調用addEventListenerWithFixedPriority方法
{
_eventDispatcher->addEventListenerWithFixedPriority(listener, _fixedPriority);//注冊事件監聽器,傳入優先級
}
else
{
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);//優先級為0,綁定目前對象
}
_listener = listener;
}
void onExit() override //離開舞台的時候調用
{
_eventDispatcher->removeEventListener(_listener);
Sprite::onExit();
}
void removeListenerOnTouchEnded(bool toRemove) { _removeListenerOnTouchEnded = toRemove; };
inline EventListener* getListener() { return _listener; };
private:
EventListener* _listener;
int _fixedPriority;
bool _removeListenerOnTouchEnded;
};
不可以在注冊第二種事件監聽器的時候傳入優先級 0 ,因為有着第一個方法對 0 值進行注冊,這兩個方法不能混淆。
依據官方文檔,事件分發的優先級次序為 (<0, scene graph (0 priority), >0 )。
即 優先級 priority值 越小,那麼就越先被調用。
如此一來,我們可知,使用addEventListenerWithSceneGraphPriority() 方法注冊的事件監聽器,優先級高于addEventListenerWithFixedPriority()。
好了,兩種事件注冊方法都說完了,事件分發的次序也分析了一遍。
是以,原來的話應該改成:相同優先級的,受 渲染次序 影響,離螢幕越近,越優先被觸發事件監聽;而優先級不同的,受 priority值影響,值越小,越先被觸發。
****************************************************自定義事件***********************************************************
感覺口水說了一堆,隻說了一半的目标啊,接下來說說自定義事件。
我沒有去研究鍵盤事件和滑鼠事件,而對自定義事件興趣滿滿,這也是AS3的影響。
個人認為自定義事件最大的作用就是解耦,傳遞資料。
試想一下,兩個子產品間的資訊傳遞,是通過事件,一邊隻需要分發事件,一邊隻需要接收,耦合大大減少。
隻是,完全使用自定義事件來解耦不大現實,單單調試就是個大問題哈哈,還是依據需要吧。
自定義事件的注冊和分發都十分簡單,看代碼:
<span style="white-space:pre"> </span>_listener = EventListenerCustom::create("game_custom_event1", [=](EventCustom* event){ //建立一個自定義事件,傳入事件名稱 和 lambda 函數
std::string str("Custom event 1 received, ");
char* buf = static_cast<char*>(event->getUserData()); //從event中獲得傳遞的自定義資料,不限于字元。
str += buf;
str += " times";
statusLabel->setString(str.c_str());
});
_eventDispatcher->addEventListenerWithFixedPriority(_listener, 1);//注冊
分發:
<span style="white-space:pre"> </span>static int count = 0;
++count;
char* buf = new char[10];
sprintf(buf, "%d", count);
EventCustom event("game_custom_event1"); <span style="white-space:pre"> </span>// 建立事件,傳入約定好的事件名稱
event.setUserData(buf);<span style="white-space:pre"> </span>//傳入資料
_eventDispatcher->dispatchEvent(&event);<span style="white-space:pre"> </span>//分發事件
CC_SAFE_DELETE_ARRAY(buf);
真是簡單粗爆。
約定一個自定義事件名稱,然後建立事件監聽器,傳入事件名稱和lambda函數,注冊。
分發隻要建立一個自定義事件,傳入名稱和資料,再使用分發器分發出去就ok了,随時随地,無所顧忌。
重要的代碼隻是注釋的那些,其他都可以自由變化。
以上,能力有限,錯漏難免,歡迎指正。