Cocos2d-x 3.0将所有的事件统一集中到Eventdispatcher中处理,它不光改进了触摸等系统事件的管理和使用方式,还使得我们可以借助其处理程序自定义的事件。本章将学习相关的内容。 5.1 事件类型 要处理一个事件,首先得定义一个事件类型。事件系统总是按类型而不是实例来处理事件的订阅和分发,这样使得同一个事件可以有多个订阅者。Event是所有事件的基类,它用一个字符串来表示该事件的类型。我们不应该直接使用Event,而应该从它继承实现自定义事件。事件类型通常不是一个变量,以保证相同类型的事件实例拥有相同的类型,但EventCustom除外,它可以在初始化的时候指定不同的类型,这是为了简化编写事件类型。以下是Event类的定义:
classEvent
{
protected:
Event(conststd::string& type);
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。 5.2 事件的订阅者 订阅者负责处理事件,它的成员包含一个订阅事件的类型(这个类型应该和对应的Event的类型一致),以及一个回调方法用来处理事件。这两个成员都应该只被事件分发器(Eventdispatcher)使用,所以它们被定义为受保护的成员,同时EventListener被定义为Eventdispatcher的友元:
classEventListener :publicObject
{
protected:
EventListener();
boolinit(conststd::string& t,std::function<voID(Event*)>callback);
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;
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);
}
voIDCollisionSystem::update(floatdt)
{
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();
inlinevoIDupdateEventPriorityIndex() {
_oldEventPriority=_eventPriority;
_eventPriority= ++_globalEventPriorityIndex;
if(_oldEventPriority!=_eventPriority)
{
setDirtyForAllEventListeners();
}
classEventtouch :publicEvent
{
public:
enumclassEventCode{
BEGAN,
MOVED,
ENDED,
CANCELLED
};
EventCodegetEventCode() {return_eventCode; };
std::vector<touch*> gettouches() {return_touches; };
#if touch_PERF_DEBUG
voIDsetEventCode(EventCodeeventCode) {_eventCode= eventCode; };
voIDsettouches(conststd::vector<touch*>& touches) {_touches= touches; };
#endif
};
classCC_DLLtouch :publicObject
{
public:
/** 触摸点在OpenGL坐标系中的位置 */
PointgetLocation()const;
/** 触摸点在OpenGL坐标系中的上一个位置 */
PointgetPrevIoUsLocation()const;
/** 触摸点在OpenGL坐标系的起点位置 */
PointgetStartLocation()const;
/** 在OpenGL坐标系中当前位置与上一个位置的差 */
PointgetDelta()const;
/** 触摸点在屏幕坐标系中的位置 */
PointgetLocationInVIEw()const;
/** 触摸点在屏幕坐标系中的上一个位置 */
PointgetPrevIoUsLocationInVIEw()const;
/** 触摸点在屏幕坐标系的起点位置 */
PointgetStartLocationInVIEw()const;
intgetID()const{return_ID;}
}; 有了这些信息,我们就可以在程序中进行精准的触摸判断,例如判定是否点中某个区域,以及是否在触摸事件结束的时候离开了某个区域,还可以在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:
bool_needSwallow;
touch::dispatchMode_dispatchMode;
}; 首先通过touch::dispatchMode将订阅者分为单点和多点触摸的订阅者,而对于单点的情况,还可以通过设置setSwallowtouches来决定是否需要禁止后续的订阅者继续处理某个触摸点。EventListener与Node的关联只是导致了事件的分发与绘制的顺序相反,而对于触摸事件来说,一般情况下它可能只需要被处理一次,这个时候Eventdispatcher就要根据_needSwallow属性来决定是否需要继续分发。 根据这些触摸事件处理的一些需求,dispatchtouchEvent()方法的逻辑就比较清晰了: 首先,找到所有单点触摸的订阅者,然后分别用每一个触摸点分别询问ontouchBegan是否需要处理,如果需要则将该触摸点保存到该订阅者中以供后续的move,end,cencelled等方法处理。同时,如果该订阅者的_needSwallow设置为true,则该触摸点将不再被任何订阅者处理。 其次,对上述执行后剩下的所有触摸点,找到所有多点触摸的订阅者,分别调用各个多点触摸的方法。 值得注意的是,如果同时有大于1个的触摸点,则单点触摸的订阅者将会执行多次,所以如果玩家同时将两个手指点击在一个按钮上,则按钮将被触发两次点击事件,Eventdispatcher并不保证单点事件的订阅者只被点击一次,程序逻辑需要实现状态记录,我们可以参看后面的Menu分析对此的处理,它用Menu::State来记录按钮当前状态。 最后,我们来分析两个引擎中的使用触摸的例子,让读者了解触摸的常用处理方法。 5.5.1.1 Layer对系统事件的支持 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);
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;
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的好处。原文:http://hIElvis.com/2013/11/16/cocos2d-x-event/
总结以上是内存溢出为你收集整理的Cocos2d-x 3.0 事件系统全部内容,希望文章能够帮你解决Cocos2d-x 3.0 事件系统所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)