public:
virtual~Event();
inlineconststd::string& getType()const{return_type; };
inlinevoidstopPropagation() {_isStopped=true; };
inlineboolisStopped()const{return_isStopped; };
inlineNode* getCurrentTarget() {return_currentTarget; };
protected:
inlinevoidsetCurrentTarget(Node* target) {_currentTarget= target; };std::string_type;
bool_isStopped;
Node* _currentTarget;
friendclassEventDispatcher;
};
实际上Event的成员应该仅包含一个表示类型的字符串,然而在Cocos2d-x中有些事件的分发如触摸可能和Node的层级相关,所以它还包含一个
获取关联元素的
方法:getCurrentTarget();另外它还是EventDispatcher的友元,这是为了方便处理触摸等事件分发,这些都会在本章后面分析。
一个Event实例实际上是事件传递过程中的数据,它由事件触发者构造,并传递给事件分发器,事件分发器根据其类型分别
通知所有
订阅该类型事件的
订阅者,并将其作为参数传递给
订阅者。因为事件是一种异步通信机制,它通常没有回调,甚至一个类型的事件可能不包含任何
订阅者,这就需要事件的触发者向接受者传递相关的上下文数据,接受者才能正确处理,例如EventTouch对象中会包含触摸点的信息,以便于
订阅者处理逻辑。
Cocos2d-x引擎
自带的事件类型
包括:EventTouch,EventKeyboard,EventAcceleration,以及便于开发者
自定义事件的EventCustom。
订阅者负责处理事件,它的成员包含一个
订阅事件的类型(这个类型应该和对应的Event的类型一致),以及一个回调
方法用来处理事件。这两个成员都应该只被事件分发器(EventDispatcher)使用,所以它们被定义为受保护的成员,同时EventListener被定义为EventDispatcher的友元:
classEventListener :publicObject
{
protected:
EventListener();
boolinit(conststd::string& t,std::function<void(Event*)>callback);
public:
virtual~EventListener();
virtualboolcheckAvaiable() =0;
virtualEventListener* clone() =0;
protected:
std::function<void(Event*)> _onEvent;
std::string_type;
bool_isRegistered;friendclassEventDispatcher;
friendclassNode;
};
在Cocos2d-x以前的版本中,
订阅者以继承的方式定义,
订阅者和处理逻辑的对象是同一个实体,例如CCLayer实现了CCTouchDelegate。而在3.0中EventListener被定义为一个变量,其好处是可以将处理
方法定义为lambda表达式,这是3.0
支持C++11的一个重要方面,它改变了使用事件的编程习惯,但是带来了lambda表达式的好处,编程更加灵活,你甚至可以在一个EventListener的处理程序中再定义一个EventListener变量。
与事件类型相对应,Cocos2d-x中
自带的
订阅者
包括:EventListenerTouch,EventListenerKeyboard,EventListenerAcceleration以及EventListenerCustom。
5.3 事件的工作流程
在定义了事件和
订阅者之后,应用程序只需要向事件分发器
注册一个
订阅者实例,即可在事件发生的时候得到
通知。在Cocosd-x中负责事件的
订阅,注销,分发的是EventDispatcher,它是一个单例,应用程序可通过EventDispatcher::getInstance()
方法获取其实例。
下面通过一个示例来演示事件的工作方式,在这个示例中当CollisionSystem检测到两个Node之间发生碰撞时,将触发碰撞事件,而HitSystem是碰撞事件的其中一个
订阅者,它会响应碰撞事件并
修改敌人的生命值:
classCollisionEvent:publicEvent
{
public:
staticconstchar* COLLISION_EVENT_TYPE;
CollisionEvent(Entity* l,Entity* r);
Entity* getLeft(){return_left;}
Entity* getRight(){return_right;}
private:
Entity* _left;
Entity* _right;
};
上述
代码首先
添加一个碰撞事件类CollisionEvent,它继承自Event,并用一个常量COLLISION_EVENT_TYPE定义其类型。
CollisionEvent作为事件传递的数据,应该向
订阅者传递相关的上下文,这里需要传递的是发生碰撞的两个Entity实例,关于Entity Component System会在本书后面的章节讲述。
classCollisionListener :publicEventListener
{
public:
staticCollisionListener* create(std::function<void(CollisionEvent*)> callback);
virtualboolcheckAvaiable() override;
virtualCollisionListener* clone() override;
protected:
CollisionListener();
boolinit(std::function<void(CollisionEvent*)> callback);
std::function<void(CollisionEvent*)> _onCollisionEvent;
};
接下来,我们需要定义
订阅者,在CollisionListener的init()
方法中,声明了它
订阅事件的类型,通过查看CollisionListener的实现部分
代码,可以看到它引用的是上面CollisionEvent定义的COLLISION_EVENT_TYPE。
voidHitSystem::configure()
{
autolistener=CollisionListener::create(
[this](CollisionEvent* event){
this->hit(event);
});
EventDispatcher::getInstance()->addEventListenerWithFixedPriority(listener,1);
}
然后,我们需要向EventDispatcher
注册订阅者。HitSystem会响应碰撞事件,所以我们在HitSystem初始化的时候向EventDispatcher
注册,并传递一个lambda表达式作为处理程序。
voidCollisionSystem::update(floatdt)
{
if(collide()){
CollisionEvent* event=newCollisionEvent(entity,collisionEntity);
EventDispatcher::getInstance()->dispatchEvent(event);
}
}
最后,是触发事件的程序。由于CollisionSystem负责碰撞检测,所以它会在检测到两个Node之间发生碰撞时,
通知EventDispatcher分发此碰撞事件,并将发生碰撞的两个Entity作为数据保存在Event参数中。EventDispatcher在接受到事件
通知的时候,首先根据Event参数的类型,查找与此类型相符的
订阅者,在本示例程序中CollisionListener的类型与CollisionEvent的类型一致,所以将会执行CollisionListener中的回调
方法。
所以,通过EventDispatcher我们就能
自定义各种事件,在应用程序的各个模块之间灵活通信,大大简化了事件的处理,同时降低了模块间的耦合。
当然一般情况下并不需要像这样定义每一个事件,可以直接使用EventCustom,它的构造
函数接受一个类型参数,使得同样的EventCustom实例可以分发不同类型的事件。同理,EventListenerCustom也接受一个类型参数,使得其可以处理不同的事件类型。
5.4 深入分析EventDispatcher
通过前面的学习,我们应该初步学会了在Cocos2d-x中怎样使用一般的事件。然而更灵活熟练地使用事件,还需要深入学习更多的知识,在进一步分析EventDispatcher的机制之前,我们来总结一下一般在游戏中使用事件还有哪些特殊的需求:
- 设置订阅者的优先级,一个类型的事件可能拥有多个订阅者,因此有必要设置处理顺序,例如当碰撞事件完成之后,其中一个订阅者负责处理伤害计算,而另一个订阅者可能做一些UI的操作,例如播放声音或者粒子效果。前者的优先级肯定需要更高,因为后者的处理可能需要依赖于生命值的计算。
- 修改订阅者的优先级。
- 停止事件的继续分发,使后续的订阅者不用再处理该事件。
- 根据屏幕上元素的层级,而不是手动设定的优先级来处理事件分发,这在触摸事件的分发中尤其重要。
带着这些目标,我们来分析EventDispatcher是怎样实现它们,以及我们在应用程序中应该怎样使用它们。
首先,EventDispatcher提供了两种
注册订阅者的
方法:
voidaddEventListenerWithSceneGraPHPriority(EventListener* listener,Node* node);
voidaddEventListenerWithFixedPriority(EventListener* listener,intfixedPriority);
第一种提供一个相关联的Node,这样事件的处理将会依据该Node的绘制顺序来决定分发的优先级。第二种则是手动设定一个优先级,这样EventDispatcher将根据该优先级直接决定分发顺序。同时,通过第二种
方法注册的
订阅者还可以通过
调用setPriority()
方法修改优先级。
其次,EventDispatcher是怎样做到根据元素的绘制顺序来计算
订阅者的优先级的呢?在Cocos2d-x引擎内部,每个EventListener都被封装为一个EventListenerItem的结构体:
structEventListenerItem
{
int fixedPriority;
Node* node;
EventListener* listener;
~EventListenerItem();
};
如果
订阅者与某个Node相关联,则node成员将被赋值,同时fixedPriority被设置为0。并且该listener变量会被
添加到该Node的关联
订阅者列表中。这样的设置会影响
订阅者的排序,找到sortAllEventListenerItemsFor
type()方法,可以总结排序规则如下:
- 分发fixedPriority小于0的订阅者,fixedPriority越小则优先分发。
- 分发所有fixedPriority值为0的订阅者,并且没有与Node相关联的。
- 分发所有与Node相关联的订阅者,其关联Node的eventPriority越高(越处于屏幕最上层)则优先级越高。
- 分发所有fixedPriority大于0的订阅者,同样fixedPriority越小则优先分发。
这里有两个地方需要注意:首先,两种
订阅方式的优先级进行比较并没有太大意义,通常我们应该避免同一个事件类型的
订阅者混用两种
注册方式;其次,Node的eventPriority变量不需要我们操心,只要设置为根据Node计算优先级,则引擎会保证其与绘制顺序一致。
如果读者对此感兴趣,可以看到Node类有一个静态变量_globalEventPriorityIndex,在每一帧开始的时候,Director会将其重置为0,然后在Node的visit()
方法中,每此
调用draw()
方法之后会
调用updateEventPriorityIndex()
方法,该
方法如下:
inlinevoidupdateEventPriorityIndex() {
_oldEventPriority=_eventPriority;
_eventPriority= ++_globalEventPriorityIndex;
if(_oldEventPriority!=_eventPriority)
{
setDirtyForAllEventListeners();
}
};
由此计算,第一个绘制的Node其_eventPriority变量值为1,第二个Node其_eventPriority值为2…,一旦有新的Node被
添加或者旧的Node被移除,这些值会重新计算,但始终能保证其和绘制顺序一致。而一旦某个Node的绘制顺序号发生了改变,则重新计算该Node关联的所有
订阅者在EventDispatcher中的排列顺序,从而保证其处理顺序始终和绘制顺序一致。
每个Event在被处理的时候,如果
订阅者是通过与Node关联
注册的,EventDispatcher还会将当前
订阅者关联的Node临时保存在Event中,这样我们还可以在事件处理程序中
获取该Node,这就是前面看到的getCurrentTarget()
方法。这在某些时候比较有用,因为EventListener可以和任何Node实例关联,事件处理
代码所在的对象可能并没有保留其引用。
最后是关于事件的
禁止分发,由于一个事件会被多个
订阅者处理,因此同一个Event实例会被传递给多个处理程序,这样每个处理程序就可以
修改这个共同的Event实例。如果其中一个处理程序
调用Event的stopPropagation()
方法,将其_isStopped设置为true,EventDispatcher就会停止对该事件的分发,后续的
订阅者将接受不到事件
通知。
此外,EventDispatcher还包含一些例如保证安全的
代码,以及在需要的时候对
订阅者从新排序等
方法,dispatchEvent()
方法还包含一个forceSortListeners的默认为false的参数它可以在分发事件之前对
订阅者重新排序,但似乎用处不大,因为一般影响优先级的因素如
添加,移除节点,直接
修改fixedPriority等都会导致
订阅者重新排序,也许可能还存在一些特殊情况。
5.5 Cocos2d-x中的系统事件
对于系统事件,有的Cocos2d-x采取了特殊的处理,而有的我们可以从中学习到使用事件的一些高级技巧,当然在实际使用过程中最重要的还是知道怎么熟练使用它们,以及明白它们在什么时候被触发。
首先找到EventTouch的定义,它仅包含两个成员,_eventCode用来表面当前触摸事件的状态,它在EventTouch:EventCode中定义;_touches则保存者当前触摸状态下的触摸点信息:
};
有了这些信息,我们就可以在程序中进行精准的触摸判断,例如判定是否点中某个区域,以及是否在触摸事件结束的时候离开了某个区域,还可以在cancelled事件发生时根据触摸点发生的位移还原一些元素的位置等等,后面将分析一些实际例子。
在dispatchTouchEvent()
方法中我们再也不用为
订阅者的优先级操心了,因为在这方面,触摸事件和其他事件的处理是一致的。这里需要特殊处理的是触摸事件要根据不同的触摸状态
调用订阅者的不同响应
方法,和其他
订阅者只有一个处理
方法不同,触摸事件的
订阅者需要提供每个触摸状态下的
方法:
classEventListenerTouch :publicEventListener
{
public:
std::function<bool(Touch*,Event*)> onTouchBegan;
std::function<void(Touch*,Event*)> onTouchMoved;
std::function<void(Touch*,Event*)> onTouchEnded;
std::function<void(Touch*,Event*)> onTouchCancelled;
std::function<void(conststd::vector<Touch*>&,Event*)> onTouchesBegan;
std::function<void(conststd::vector<Touch*>&,Event*)> onTouchesMoved;
std::function<void(conststd::vector<Touch*>&,Event*)> onTouchesEnded;
std::function<void(conststd::vector<Touch*>&,Event*)> onTouchesCancelled;
voidsetSwallowTouches(boolneedSwallow);
private:
Touch::DispatchMode_dispatchMode;
};
首先通过Touch::DispatchMode将
订阅者分为单点和多点触摸的
订阅者,而对于单点的情况,还可以通过设置setSwallowTouches来决定是否需要
禁止后续的
订阅者继续处理某个触摸点。EventListener与Node的关联只是导致了事件的分发与绘制的顺序相反,而对于触摸事件来说,一般情况下它可能只需要被处理一次,这个时候EventDispatcher就要根据_needSwallow
属性来决定是否需要继续分发。
根据这些触摸事件处理的一些需求,dispatchTouchEvent()
方法的逻辑就比较清晰了:
首先,找到所有单点触摸的
订阅者,然后分别用每一个触摸点分别询问onTouchBegan是否需要处理,如果需要则将该触摸点保存到该
订阅者中以供后续的move,end,cencelled等
方法处理。同时,如果该
订阅者的_needSwallow设置为true,则该触摸点将不再被任何
订阅者处理。
其次,对上述执行后剩下的所有触摸点,找到所有多点触摸的
订阅者,分别
调用各个多点触摸的
方法。
值得注意的是,如果同时有大于1个的触摸点,则单点触摸的
订阅者将会执行多次,所以如果玩家同时将两个手指点击在一个按钮上,则按钮将被触发两次点击事件,EventDispatcher并不保证单点事件的
订阅者只被点击一次,程序逻辑需要实现状态记录,我们可以参看后面的Menu分析对此的处理,它用Menu::State来记录按钮当前状态。
最后,我们来分析两个引擎中的使用触摸的例子,让读者了解触摸的常用处理
方法。
Layer经常被用来根据UI的层级组织元素,正如它的名字一样。实际上它的主要目的是方便我们使用系统事件,触摸,按键,重力加速等事件,它通过提供构造这些事件的
订阅者,并向EventDispatcher
注册和注销这些
订阅者来简化我们使用系统事件。此外,在3.0中它还新加入了对物理引擎集成的
支持,在后面的章节我们会学习。
Layer对所有事件的均采取与自身相关联的方式向EventDispatcher
注册订阅者,即是说被处理的优先级取决于自身的UI层级。要使用某个系统事件基本上只需要
调用setXXXEnabled()
方法,然后重写相关事件的处理
方法即可。当然默认所有的系统事件均没有开启,并且对于触摸事件默认设置为多点触摸。
一下我们以使用触摸事件为例,在Layer中使用触摸事件:
boolHelloWorld::init()
{
if( !CCLayer::init()){
returnfalse;
}
setTouchMode(Touch::DispatchMode::ONE_BY_ONE);
setTouchEnabled(true);
returntrue;
}
classHelloWorld :publiccocos2d::Layer
{
public:
virtualboolonTouchBegan(Touch*touch,Event*event);
virtualvoidonTouchMoved(Touch*touch,Event*event);
virtualvoidonTouchEnded(Touch*touch,Event*event);
virtualvoidonTouchCancelled(Touch*touch,Event*event);
}
上述示例中,我们首先在Layer的init初始化后
调用setTouchEnabled()
方法声明需要处理单点触摸事件,然后重写了单点触摸需要实现的4个处理
方法。
实际上,在开启每个系统事件处理之后,Layer向EventDispatcher
注册订阅者,并将处理
方法声明为虚
方法以供子类重写。并在
关闭事件,或者元素被移除的时候向EventDispatcher注销
订阅者。这样大大简化了我们使用系统事件。
5.5.1.2 Menu中的触摸处理
触摸更常见的是被用在一些控件中,比如按钮点击,表格拖拽等。其开发中最重要的部分是点击判定,此外对于按钮还需要注意前面提到的状态判定,以避免多次点击。Cocos2d-x为我们提供了一个常用的GUI控件-Menu,用于显式一组按钮。通过分析Menu我们就可以完全掌握触摸相关的处理了。
Menu继承自LayerRGB,所有可以很方便的使用触摸事件。通过查看Menu的源
代码可以知道Menu
注册了单点触摸事件。当然Menu还实现了多个MenuItem的管理,我们这里关心的是怎么使用触摸点的位置信息。
boolMenu::onTouchBegan(Touch* touch,Event* event)
{
if(_state!=Menu::State::WAITING|| !_visible|| !_enabled){
returnfalse;
}
for(Node*c =this->_parent; c !=NULL; c = c->getParent()){
if(c->isVisible() ==false){
returnfalse;
}
}
_selectedItem=this->itemForTouch(touch);
if(_selectedItem){
_state=Menu::State::TRACKING_TOUCH;
_selectedItem->selected();
returntrue;
}
returnfalse;
}
从这个示例我们可以看到三点有趣的信息:
首先,Menu通过一个Menu::State变量,来防止同时多次点击,如果Menu开始处理一个触摸点,则会将_state设置为Menu::State::TRACKING_TOUCH。
其次,它在UI树上向上查找直到根节点,检查节点是否正在被绘制。这里是因为虽然可以通过设置visible来设置节点的可见性,但其子结点并不能直接知道自己是否被隐藏或者显式了,需要向上遍历至根节点才能做出判断。同时,EventDispatcher虽然可以根据节点UI层级来决定分发顺序,但它并不负责检查节点的可见性,因为这里元素仅用来计算分发顺序,而且并不是所有的事件都依据元素的层级来计算优先级。所以,这里在开发中经常会遇到的一个问题就是,某个节点通过父级被隐藏了,但是其仍然能够收到触摸事件。
最后,Menu通过itemForTouch()
方法来做点击判定:
MenuItem*Menu::itemForTouch(Touch*touch)
{
PointtouchLocation = touch->getLocation();
if(_children&&_children->count() >0)
{
Object* pObject =NULL;
CCARRAY_FOREACH_REVERSE(_children,pObject)
{
MenuItem* child =dynamic_cast<MenuItem*>(pObject);
if(child && child->isVisible() && child->isEnabled())
{
Pointlocal = child->convertToNodeSpace(touchLocation);
Rectr = child->rect();
r.origin=Point::ZERO;
if(r.containsPoint(local)){
returnchild;
}
}
}
}
returnNULL;
}
这里则告诉我们做点击判定的一般
方法,首先我们通过getLocation()
方法取出触摸点在OpenGL坐标系中的世界坐标,然后将其转化到节点的本地坐标系,最后根据节点的尺寸检测其是否落于该区域内。至此,我们就了解了关于触摸的所有知识。
5.5.2 EventKeyboard
键盘输入事件比较简单,它捕捉一个按键动作,它的参数
包括按下的键_keyCode,以及表示按键的两个状态_isPressed:
classEventKeyboard :publicEvent
{
EventKeyboard(KeyCodekeyCode,boolisPressed)
:Event(EVENT_TYPE)
,_keyCode(keyCode)
,_isPressed(isPressed)
{};
private:
KeyCode_keyCode;
bool_isPressed;
friendclassEventListenerKeyboard;
};
classEventListenerKeyboard :publicEventListener
{
public:
std::function<void(EventKeyboard::KeyCode,Event* event)> onKeyPressed;
std::function<void(EventKeyboard::KeyCode,Event* event)> onKeyReleased;
};
有趣的是这里对两个处理
方法的
调用方式,一般事件只有一个处理
方法,我们还记得EventListener定义的_onEvent变量,它被EventDispatcher直接
调用,而这里EventListenerKeyboard做了特殊处理:
boolEventListenerKeyboard::init()
{
autolistener = [this](Event* event){
autokeyboardEvent =static_cast<EventKeyboard*>(event);
if(keyboardEvent->_isPressed){
if(onKeyPressed!=nullptr)
onKeyPressed(keyboardEvent->_keyCode,event);
}
else{
if(onKeyReleased!=nullptr)
onKeyReleased(keyboardEvent->_keyCode,event);
}
};
if(EventListener::init(EventKeyboard::EVENT_TYPE,listener)){
returntrue;
}
returnfalse;
}
我们看到,EventListenerKeyboard重新包装了listener,由此可见,我们程序中定义的
订阅者实例并不一定是最终EventDispatcher中引用的实例,而这里更有趣的是
订阅者中包含了
订阅者。这就是事件分发使用
方法指针,而不是继承实现某个Delegate的好处。