Cocos2d-x学习笔记—事件处理机制
一:事件处理机制
一个事件由触发到完成响应,主要由以下三部分组成:
- 事件分发器EventDispatcher;
- 事件类型EventTouch、EventKeyboard等;
- 事件监听器EventListenerTouch、EventListenerKeyboard等。
在Cocos2d-x v3.x中,关于事件的东西,无非就是围绕上述的三个部分展开来的,掌握了上述的三个部分,也就掌握了Cocos2d-x v3.x中事件处理的精髓。
(1)事件分发器:
事件分发器,就相当于是所有事件的“总长官”;它负责调度和管理所有的事件监听器;当有事件发生时,它负责调度对应的事件;一般调用Director类中的getEventDispatcher获得一个事件调度器,在游戏启动时,就会创建一个默认的EventDispatcher对象。
事件监听器与事件具有对应关系。例如,键盘事件(EvemKeyboard)只能由键盘事件监听器(EventListenerKeyboard)处理,它们之间需要在程序中建立关系,这种关系的建立过程被称为“注册监听器”。CoCos2d-x提供一个事件分发器(EvemDispatcher)负责管理这种关系,具体说事件分发器负责注册监听器、注销监听器和事件分发。EventDispatcher 类采用单例模式设计,通过 Director::getlnstance()->getEventDispatcher()语句获得事件分发器对象。
EventDispatcher类中注册事件监听器到事件分发器函数如下:
(1) void addEventListenerWithFixedPriority (EventListener * listener,int fixedPriority) 指定固定的事件优先级注册监听器,事件优先级决定事件响应的优先级别,值越小优先级越髙。
(2) void addEventListenerWithSceneGraPHPriority(EventListener * listener,Node * node) 把精灵显示优先级作为事件优先级,参数node是要触摸的精灵对象。
当不再进行事件响应时,应该注销事件监听器。主要的注销函数如下:
(1) void removeEventListener(EventListener * listener)。注销指定的事件监听器。
(2) void removeCustomEventListeners(const std:: string&* customEventName)。注销自定义事件监听器。
(3) void removeAllEventListener()。注销所有事件监听器,需要注意的是,使用该函数之后,菜单也不能响应事件了,因为它也需要接受触摸事件。
注意:
(1)addEventListenerWithSceneGraPHPriority的事件监听器优先级是0, 而且在addEventListenerWithFixedPriority中的事件监听器的优先级不可以设置为0,因为这个是保留给SceneGraPHPriority使用的。
(2)另外,有一点非常重要,FixedPriority listener添加完之后需要手动remove,而SceneGraPHPriority listener是跟node绑定的,在node的析构函数中会被移除。 移除方法为dispatcher->removeEventListener(listener);
(2)事件类型:
在Cocos2d-x中定义了以下几种事件类型:
enum class Type { TOUCH,// 触摸事件 KEYBOARD,// 键盘事件 ACCELERATION,// 加速器事件 MOUSE,// 鼠标事件 FOCUS,// 焦点事件 CUSTOM // 自定义事件 }
(3)事件监听器:
事件监听器实现了各种事件触发后对应的逻辑;由事件分发器EventDispatcher调用对应的事件监听器,进而由事件监听者响应所绑定的回调函数。在Cocos2d-x中定义以下的几种事件监听器:
enum class Type { // 未知的事件监听器 UNKNOWN,// 单点触摸事件监听器创建方法与回调函数形参 TOUCH_ONE_BY_ONE,EventListenerTouchOneByOne::create(); typedef std::function<bool(Touch*,Event*)> ccTouchBeganCallback; typedef std::function<void(Touch*,Event*)> ccTouchCallback; // 多点触摸事件监听器创建方法与回调函数形参 TOUCH_ALL_AT_ONCE,EventListenerTouchAllAtOnce::create(); typedef std::function<void(const std::vector<Touch*>&,Event*)> ccTouchesCallback; // 键盘事件监听器创建方法与回调函数形参 KEYBOARD,EventListenerKeyboard::create(); std::function<void(EventKeyboard::KeyCode,Event*)> onKeyPressed; std::function<void(EventKeyboard::KeyCode,Event*)> onKeyReleased; // 鼠标事件监听器创建方法与回调函数形参 MOUSE,EventListenerMouse::create(); std::function<void(EventMouse* event)> onMouseDown; std::function<void(EventMouse* event)> onMouseUp; std::function<void(EventMouse* event)> onMouseMove; std::function<void(EventMouse* event)> onMouseScroll; // 加速器事件监听器创建方法与回调函数形参 ACCELERATION,EventListenerAcceleration::create(); std::function<void(Acceleration*,Event*)> onAccelerationEvent; // 焦点事件监听器创建方法与回调函数形参 FOCUS,EventListenerFocus::create(); std::function<void(ui::Widget*,ui::Widget*)> onFocusChanged; // 自定义事件监听器创建方法与回调函数形参 CUSTOM EventListenerCustom::create(); std::function<void(EventCustom*)> _onCustomEvent; }
二:单点触摸事件处理方法
(1)创建一个单点触摸事件监听器:
auto listener = EventListenerTouchOneByOne::create();
(2)设置监听器回调函数:
// 设置是否吞没事件,在onTouchBegan方法返回true时吞没,事件不会传递给下一个Node对象 listener->setSwallowTouches(true); listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::touchBegan,this); listener->onTouchMoved = CC_CALLBACK_2(HelloWorld::touchMoved,this); listener->onTouchEnded = CC_CALLBACK_2(HelloWorld::touchEnded,this);
(3)添加监听器
// 其中listener->clone()获得listener对象,使用clone()函数是因为每—事件监听器只能被注册一次, // addEventListenerWithSceneGraPHPriority和addEventListenerWithFixedPriority会在注册事件监听器时设置一个注册标 // 识,一旦设置了注册标识,该监听器就不能再用于注册其他事件监听了,因此需要使用listener->clone()克隆一个新的监听器对象, // 把这个新的监听器对象用于注册。 _eventDispatcher->addEventListenerWithSceneGraPHPriority(listener,pLayer1); _eventDispatcher->addEventListenerWithSceneGraPHPriority(listener->clone(),pLayer2); _eventDispatcher->addEventListenerWithSceneGraPHPriority(listener->clone(),pLayer3);
- 监听器必须加入到EventListener中才有效;
- 只要发生触摸事件,_eventDispatcher事件分发器会根据pLayer的zOrder顺序调用listener监听者的onTouchBegan函数(zOrder大的优先调用,相同的话后加入的节点优先调用),再根据onTouchBegan函数的返回值处理,具体处理规则见(4),同时相应的pLayer便作为Event传入onTouchBegan函数。这段话的理解可以举个例子,一个精灵对象sprite的zorder值高于一个菜单对象menu,当点击屏幕任何一处时,系统先响应sprite对象所绑定的listener监听者的回调函数onTouchBegan,若其返回false,则响应menu对象的回调函数;若其返回true,则不再响应menu对象的回调函数
(4)具体触摸事件
bool HelloWorld::TouchBegan(Touch *touch,Event *unused_event) { // 获取事件所绑定的对象 auto target = static_cast<Layer*>(unused_event->getCurrentTarget()); if (target == nullptr) { return false; } // 获取当前点击点相对绑定对象的局部坐标(Node坐标系) // getLocation得到的是openGL坐标系,也就是世界坐标系 Vec2 locationInNode = target->convertToNodeSpace(touch->getLocation()); Size s = target->getContentSize(); Rect rect = Rect(0,s.width,s.height); // 点击范围判断检测 if (rect.containsPoint(locationInNode)) { log("sprite began... x = %f,y = %f",locationInNode.x,locationInNode.y); target->setOpacity(180); return true; } return false; }
关键理解:
-
onTouchBegan(Touch *touch,Event *event)是每次触摸事件发生时最先调用的函数,返回一个bool值。在设置吞噬事件为true的条件下listener->setSwallowTouches(true):如果返回true,说明此次触摸事件己经找到目标对象并被处理,之后的onTouchMoved、onTouchEndcd和onTouchCancelled函数将会接着响应。 而事件分发器对象EventDispatcher将会停止此次事件的分发,在事件分发器中其他的事件监听器对象则不会再去进行监听该次触摸事件,也不能再接收到此次用户操作数据。如果返回false,之后的onTouchMoved、onTouchEnded和onTouchCancelled函数将不会响应,車件分发器则会将此次事件继续交给其他添加过事件的监听器进行处理。
-
只要发生触摸事件,listener响应高zOrder值player的onTouchBegan函数,同时player作为event事件传入onTouchBegan函数,通过unused_event->getCurrentTarget()获取。
对setSwallowTouches吞没事件的理解:
-
正如上文所说的那样,如果一个监听者设置吞没事件为true,当其onTouchBegan返回true时,相当于给监听者将此次触摸事件吞没,而事件分发器对象EventDispatcher将会停止此次事件的分发,在事件分发器中其他的事件监听器对象则不会再去进行监听该次触摸事件,也不能再接收到此次用户操作数据。
-
若一个监听者设置吞没事件为false,即使onTouchBegan返回true,事件分发器对象EventDispatcher仍会按照zOrder顺序将本次触摸事件分发给别的监听者,响应其onTouchBegan函数,只有遇到设置吞没事件为true且onTouchBegan函数返回true的监听者时,事件分发器对象EventDispatcher才会停止此次事件的分发。
判断是否触摸到某目标的方法可以这么理解,以触摸对象为精灵pLayer1为例:
-
通过addEventListenerWithSceneGraPHPriority将listener与pLayer绑定,触摸发生时,pLayer会作为event事件传入onTouchBegan函数,程序中可以获得pLayer尺寸与世界坐标系下的触摸坐标,通过convertToNodeSpace函数转会为节点坐标,最后进行判断。
-
touch->getLocation()即获取世界坐标系下的触摸坐标(世界坐标系是指以设计分辨率左下角为原点,上为Y轴正方向,右为X轴正方向的坐标系(OpenGL坐标系)),convertToNodeSpace函数是以对象的左下角为原点将世界坐标系下的触摸坐标转化为节点坐标系下的触摸坐标,若要以对象锚点为原点进行计算,则要用convertToNodeSpaceAR函数。
-
Rect rect = Rect(0,s.height)为创建一个矩形块,要注意的是该矩形块是一个抽象的,并不是在层上创建了一个实际矩形块。其containsPoint只是在数学意义上判断locationInNode是否在这个矩形块内。
三:层中单点触摸事件处理方法
事件监听的对象是层,而非精灵,对于是否触摸到某个精灵的判断较为复杂,但使用简单,首先在头文件中声明如下虚函数:
virtual bool onTouchBegan(cocos2d::Touch *touch,cocos2d::Event *unused_event); virtual void onTouchMoved(cocos2d::Touch *touch,cocos2d::Event *unused_event); virtual void onTouchEnded(cocos2d::Touch *touch,cocos2d::Event *unused_event);
再在主程序中开启层触摸响应即可
this->setTouchEnabled(true); // 打开触摸监听 this->setTouchMode(Touch::DispatchMode::ONE_BY_ONE); // 设置为单点触摸模式
四:多点触摸
本节分析一下使用多点触摸的一些细节,以及应该如何处理。首先由一个Target,这个Target是一个可点击的对象,实现了几个功能;当点击到该対象的时候,该对象可以拖动,直到手指松开。
(1)第一种情况,多个TouchPoint点击在同一个Target上。
- 假设有3个手指按在了同一个Target上,3个手指同时拖向不同的方向。在拖动的过程中可以看到,Target的位置在3个手指下不断地跳动,从这个手指,跳动到另外一个手指下。原因:一个Target可也同时接受多个Touch的消息,如果只希望接受一个Touch的消息,在onTouchBegan的时候,判断是否已经被点击,如是则直接返回false。
(2)第二种情况,一个TouchPoint点击在多个Target上。
- 假设现在用一个手指摁在了3个重叠的Target上,会怎样?哪个会被拖动?还是3个同时被拖动?答案是优先级最高的那个被拖动,如果它们的优先级相同,那么最先注册的那个被拖动。原因:一个TouchPoint一旦传入某个Target的onTouchBegan返回true,就不会继续传递给其他的Target了,EventDispatcher是按照优先级进行回调的。
(3)第三种情况,多个TouchPoint点击在多个Target上。
- 现在用3个手指同时按在3个重叠的Target上,然后往不同的方向移动,会发生什么情况呢?如果没有做第一个问题的那种修改,就会出现第一个问题,3个点控制同一个Target,如果做了处理的话,3个点备自拖动着一个Target,而不会出现A挡着B,所以B就没点中的情况。