昨天写的博客中的一些说法和理解有点问题,今天根据《我所理解的Cocos2d-x》一书做出修订,在此向《我所理解的Cocos2d-x》一书的作者表示感谢。
在应用程序中,由于玩家的输入或者程序内部的某个处理逻辑完成,需要等待其他模块针对该行为进行一些响应操作的时候,我们可以定义一个事件,例如用户单击了屏幕、游戏状态的变更、当一个角色的血量低于0时触发的死亡事件等。
与一般的模块直接调用相比,事件可以不用依赖于事件响应者的实现而预先定义一 组事件类型,事件的响应者甚至可以在运行时动态地添加或者移除,从而增强了事件分发的灵活性。例如玩家单击了屏幕,程序中任何的元素或者逻辑可以对其作出响应。
事件系统有一些优秀的特点,这些特点减少了软件内模块之间的内聚,同时能够保持模块之间高效的通信。首先,事件系统使得系统或者中间件可以提前预定义一些事件。其次,事件系统解除了模块之间的耦合,使得模块之间更加独立,这对于单元测试尤其重要。
一个事件可以对应多个订阅者,多个订阅者可以对一个事件源进行响应,以执行不同职能上的逻辑处理。例如一个物理碰撞事件发生时,物理引擎需要计算碰撞后的位置,AI系统需要作一些数值计算,而动画特效系统可能会播放一些动画特效。
事件机制虽然能处理模块之间的通信,但它也有其应用范围,基于一些原因,应程序不能把所有的模块通信都使用事件机制来实现。比如对性能要求非常高的部分,一个AI算法可能要在每帧实时更新上百个角色的游戏状态,这就不适合使用事件来分发,因为事件分发会作一些查询、排序等操作,会影响实时性能;另外,事件机制不能很好地处理回调,虽然可以在事件参数中加入回调的方法地址,但是直接调用会更直观得多。
一个事件由触发到完成响应,主要由以下三部分组成:
在Cocos2d-x中,一个事件监听器是一个EventListener的子类,如果某个处理程序关心某个事件,则创建一个对应EventListener子类的实例,例如EventListenerTouch用来监听和响应触摸事件。
每个EventListener由一个回调函数、一个订阅者类型type,以及一个listenerlD组成。当然,有些事件类型对应多个处理函数,例如EventListener-Keyboard就根据其键的按下(Pressed)和释放(Released)状态提供两个回调函数。
在Cocos2d-x中定义以下的几种事件监听器:
enum class Type
{
// 未知的事件监听器
UNKNOWN,
// 单点触摸事件监听器创建方法与回调函数形参
TOUCH_ONE_BY_ONE,
EventListenerTouchOneByOne::create();
typedef std::function ccTouchBeganCallback;
typedef std::function ccTouchCallback;
// 多点触摸事件监听器创建方法与回调函数形参
TOUCH_ALL_AT_ONCE,
EventListenerTouchAllAtOnce::create();
typedef std::function&, Event*)> ccTouchesCallback;
// 键盘事件监听器创建方法与回调函数形参
KEYBOARD,
EventListenerKeyboard::create();
std::function onKeyPressed;
std::function onKeyReleased;
// 鼠标事件监听器创建方法与回调函数形参
MOUSE,
EventListenerMouse::create();
std::function onMouseDown;
std::function onMouseUp;
std::function onMouseMove;
std::function onMouseScroll;
// 加速器事件监听器创建方法与回调函数形参
ACCELERATION,
EventListenerAcceleration::create();
std::function onAccelerationEvent;
// 焦点事件监听器创建方法与回调函数形参
FOCUS,
EventListenerFocus::create();
std::function onFocusChanged;
// 自定义事件监听器创建方法与回调函数形参
CUSTOM
EventListenerCustom::create();
std::function _onCustomEvent;
}
事件分发器EventDispatcher能够根据事件的类型找到对应的UstenerlD,进而找到所有处理该事件的事件监听器。
这里有两种类型:type和listenerlD。listenerlD对应一个事件源,它可以根据一个事件源的类型找到一个对应的listenerlD;而type是Cocos2d-x用来区分EventListener类型的,它只有下表列出的7种类型,但是listenerlD根据自定义的事件类型在数量上更多,主要体现在开发者定义的所有事件类型的type都是EventUstenec Type::CUSTOM。
EventListener::Type | listenerlD | 描述 |
---|---|---|
TOUCHONE_BY_ONE | _cc_touch_one_by_one | 单点触摸 |
TOUCH_ALL_AT_ONCE | _cc_touch_all_at_once | 多点触摸 |
KEYBOARD | _cc_keyboard | 键盘事件 |
MOUSE | _cc_mouse | 鼠标事件 |
ACCELERATION | _cc_acceleration | 重力加速度事件 |
FOUSE | _cc_focus_event | 焦点亊件 |
CUSTOM | eventName | 自定义亊件 |
一个事件类型用一个Event的子类描述,它也是事件分发到事件监听器时事件源传递给事件监听器的参数,里面包含一些处理该事件相关的信息,例如EventAcceleration就包含了x、y和z3个方向的加速度数据。Event的子类由一个类型Event::Type和一些事件数据组成,示例如下:
在Cocos2d-x中定义了以下几种事件类型:
class Event : public Ref
{
public:
enum class Type
{
TOUCH, // 触摸事件
KEYBOARD, // 键盘事件
ACCELERATION, // 加速器事件
MOUSE, // 鼠标事件
FOCUS, // 焦点事件
CUSTOM // 自定义事件
}
protected:
Event(Type type);
public:
inline Type getTypeO const { return _type; };
};
Event::Type可以用来查找listenerlD,从而将事件分发到正确的订阅者进行处理。Event::Type与listenerlD的对应关系如下表所示。
Event::Type | listenerlD | 描述 |
---|---|---|
TOUCH | _cc_touch_one_by_one;_cc_touch_all_at_once | 触摸事件对应两个listenerID,触摸事件被特殊处理 |
KEYBOARD | _cc_keyboard | 按键事件 |
MOUSE | _cc_mouse | 鼠标事件 |
ACCELERATION | _cc_acceleration | 重力加速度事件 |
FOUSE | _cc_focus_event | 焦点事件 |
CUSTOM | ->getEventName() | 自定义事件,以eventName参数作为listenerID |
这里需要注意的是,对于触摸事件,它实际上对应TOUCH_ONE_BYONE和 TOUCH_ALL_AT_ONCE两个不同的事件监听者,这是经过EventDispatcher特殊处理的,后面将会专门讲述触摸事件。对于自定义事件,参数eventName作为listenerlD,对应EventListener::Type-::CUSTOM中的eventName,所以每个不同eventName的自定义事件都是一个新的事件类型,但是它们共享一个type名称CUSTOM。
事件分发器,就相当于是所有事件的“总长官”;它负责调度和管理所有的事件监听器;当有事件发生时,它负责调度对应的事件;一般调用Director类中的getEventDispatcher获得一个事件调度器,在游戏启动时,就会创建一个默认的EventDispatcher对象。
事件监听器与事件具有对应关系。例如,键盘事件(EvemKeyboard)只能由键盘事件监听器(EventListenerKeyboard)处理,它们之间需要在程序中建立关系,这种关系的建立过程被称为“注册监听器”。CoCos2d-x提供一个事件分发器(EvemDispatcher)负责管理这种关系,具体说事件分发器负责注册监听器、注销监听器和事件分发。EventDispatcher 类采用单例模式设计,通过 Director::getlnstance()->getEventDispatcher()语句获得事件分发器对象。
在明白了事件监听器的结构、事件监听器与事件源之间的关联之后,最后一步开发者需要通过EventDispatcher注册以接收事件通知。Event-Dispatcher提供了一些注册和管理事件监听器的接口,对事件监听器的管理大概可以分为3组:注册、删除和修改。
EventDispatcher类中注册事件监听器到事件分发器函数如下:
// 指定固定的事件优先级注册监听器,事件优先级决定事件响应的优先级别,值越小优先级越髙。
(1) void addEventListenerWithFixedPriority (EventListener* listener, int fixedPriority)
// 把精灵显示优先级作为事件优先级,参数node是要触摸的精灵对象。
(2) void addEventListenerWithSceneGraphPriority(EventListener* listener, Node* node)
这里除了传递一个listener参数外,还需要指定一个关联的Node对象或者一个整数的优先级(注意fixedPriority不可为0),这些都用来决定对同一个事件源的多个事件监听器应该按照怎样的顺序分发事件。所谓同一个事件源的多个事件监听器,就是指利用同一个EventListener创造出来的监听器,用来监听相同的事件。比如用EventListenerTouchOneByOne::create()创造出多个监听器listener1、listener2等,这些监听器都会监听单点触摸事件。
优先级越高的事件监听者listener越优先响应其回调函数。
指定整数的优先级,通常要求开发者记住大量的优先级数字,而有时候一个处于更 下层的UI元素却被错误地指定了一个更高的优先级,给例如触摸的管理带来了不少麻烦。因此,Cocos2d-x 3.0新增了一种将分发优先级与一个Node元素关联的方式, EventDispatcher将自动根据当前该Node元素绘制的相反顺序来决定分发优先级,即使该UI元素的层级发生变更,它也能正确处理,这样就大大简化了对触摸优先级的管理。
EventDispatcher分发事件的顺序如下:
优先级从高到低:priority<0, scene graph(priority=0), priority >0
实际上,所有与Node关联的事件监听器优先级都被设置为0,而开发者无法注册一个优先级为0的事件监听器。即addEventListener-WithSceneGraphPriority的事件监听器优先级是0,且在addEventListenerWithFixedPriority中的事件监听器的优先级不可以设置为0,因为这个是保留给SceneGraphPriority使用的。
当一个事件监听器不再需要接收事件通知,以及该事件监听器被销毁的时候,开发者需要向EventDispatcher删除该事件监听器,否则将导致事件监听器的指针为空,导致野指针操作。EventDispatcher提供了一组方法用于删除一个或者多个事件监听器,示例如下。
void removeEventListener(EventListener* listener);
void removeEventListenersForType(EventListener::Type listenerType);
void removeEventListenersForTarget (Node* target, bool recursive = false);
void removeCustomEventListeners(const std::strings customEventName);
void removeAllEventListeners();
其中,removeEventListenerForType会删除所有类型为listenerType的事件监听器,例如所有的触摸事件。需要注意的是,虽然每个自定义事件的事件监听器可以有不同的eventName,但是它们的类型都是EventListener::Type:CUSTOM,因此,当移除type为 EventLiStener::Type::CUSTOM的事件监听器时,会移除所有自定义事件的事件监听器。但是我们可以使用customEventName单独删除某一类自定义事件类型的事件监听器。
removeAllEventListener()会注销所有事件监听器,需要注意的是,使用该函数之后,菜单也不能响应事件了,因为它也需要接受触摸事件。
另外,FixedPriority listener添加完之后需要手动remove。但对于与一个Node元素关联的事件监听器,它们会在该Node元素被移除的时候自动删除与该Node关联的所有事件监听器,但是也可以提前手动删除某个Node关联的所有事件监听器。所以,Node对象除非需要提前移除所有事件监听器,否则可以不用管理事件监听器的移除。
当Node元素的onEnter方法和onExit方法被调用时,它将恢复和暂停所有的动画、计时更新以及所有与之关联的事件事件监听器。对于事件事件监听器,它使用以下两个方法来关闭和开启事件监听器是否接收事件通知。
void pauseEventListenersForTarget(Node* target, bool recursive = false);
void resumeEventListenersForTarget(Node* targetrbool recursive= false);
上述两个方法仅对与Node关联的事件监听器有效。如果开发者想开启和关闭一个使用优先级定义的事件监听器,则需要使用setEnabled()方法。
class EventListener : public Ref
{
public:
inline void setEnabled(bool enabled) { —isEnabled = enabled; };
inline bool isEnabled() const { return isEnabled; };
};
然而比较遗憾的是,我们无法动态判断一个事件监听器是通过优先级定义的还是通过与 Node元素关联定义的,因为getAssociateNode是私有方法。
最后,可以通过setPriority()方法来修改事件监听器的优先级。这里仍然需要确保该事件监听器是一个通过优先级定义的事件监听器。
void setPriority(EventListener* listener, int fixedPriority);
触摸事件是手持可触摸设备最重要的系统事件,也是最复杂的事件,它除了要区分多个触摸点,同一个点的事件还要通过多次分发以表示开始、移动、结束、取消等触摸状态。
Cocos2d-x使用EventTouch来表示一个触摸事件,每个EventTouch记录了当前屏幕上处于相同状态的一组触摸点的集合,触摸点的状态使用EventCode表示,示例如下。
class EventTouch : public Event
{
public:
static const int MAX_T0UCHES = 5;
enum class EventCode
{
BEGAN,
MOVED,
ENDED,
CANCELLED
}
EventTouch();
inline EventCode getEventCode() const { return _eventCode; };
inline const std::vector& getTouches() const { return _touches; };
};
根据状态不同,同一个触摸点会经历多次事件分发。为了在多次触摸事件中跟踪同 一个触摸点,每个Touch点包含一个唯一 ID,可以通过getID()来获取。
应用程序中通常更多地是使用单点触摸,为了简化单点触摸的处理,CoCoS2d-x将一个触摸事件分为单点触摸和多点触摸两种类型,相应地对应单点和多点两种事件监听器类型。
EventListenerTouchAllAtOnce表示一个多点触摸事件的事件监听器,它包含4个回调函数,分别用来处理触摸点的开始、移动、结束及取消4种不同的状态。每一个状态的回调函数都包含当前所有处于该种状态的触摸点,开发者需要使用触摸点的ID来区分每一个触摸点。其回调函数见2-1。
与EventListenerTouchAIIAtOnce相反,EventListenerTouchOneByOne则将触摸某个状态的多个触摸点分成多次事件通知。其回调函数见2-1。
// 创建一个单点触摸事件监听器
auto listener = EventListenerTouchOneByOne::create();
// 设置监听器回调函数
listener->setSwallowTouches(true);// 设置是否吞没事件,在onTouchBegan方法返回true时吞没,事件不会传递给下一个Node
listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::touchBegan, this);
listener->onTouchMoved = CC_CALLBACK_2(HelloWorld::touchMoved, this);
listener->onTouchEnded = CC_CALLBACK_2(HelloWorld::touchEnded, this);
// 添加监听器
// 其中listener->clone()获得listener对象,使用clone()函数是因为每—事件监听器只能被注册一次,
// addEventListenerWithSceneGraphPriority和addEventListenerWithFixedPriority会在注册事件监听器时设置一个注册标
// 识,一旦设置了注册标识,该监听器就不能再用于注册其他事件监听了,因此需要使用listener->clone()克隆一个新的监听器对象,
// 把这个新的监听器对象用于注册。
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, pLayer1);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener->clone(), pLayer2);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener->clone(), pLayer3);
// 触摸回调函数
bool HelloWorld::TouchBegan(Touch *touch, Event *unused_event)
{
// 获取事件所绑定的对象
auto target = static_cast(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, 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;
}
理解:
(1):onTouchBegan函数
onTouchBegan(Touch *touch, Event *event)是每次触摸事件发生时最先调用的函数,返回一个bool值,且onTouchBegan是必须实现的,否则将接收不到任何触摸事件通知。onTouchBegan的返回值用来告诉EventDispatcher是否应该将该触摸点后续的触摸状态传递给该事件监听器。
如果返回true,说明此次触摸事件己经找到目标对象并被处理,之后的onTouchMoved、onTouchEndcd和onTouchCancelled函数将会接着响应。若设置吞没事件setSwallowTouches(true),事件分发器对象EventDispatcher将会停止此次事件的分发,在事件分发器中其他的事件监听器对象则不会再去进行监听该次触摸事件,也不能再接收到此次用户操作数据。
如果返回false, 之后的onTouchMoved、onTouchEnded和onTouchCancelled函数将不会响应,車件分发器则会将此次事件继续交给其他添加过事件的监听器进行处理。
如果我们希望阻止一个触摸点向后面的事件监听器继续分发,可以使用setSwallowTouches(true)来实现。例如通常两个按钮不能同时处理同一个触摸点。注意:swallowTouches设置需要在onTouchBegan返回true的情况下才生效。
(2):对setSwallowTouches吞没事件的理解:
正如上文所说的那样,如果一个监听者设置吞没事件为true,当其onTouchBegan返回true时,相当于给监听者将此次触摸事件吞没,而事件分发器对象EventDispatcher将会停止此次事件的分发,在事件分发器中其他的事件监听器对象则不会再去进行监听该次触摸事件,也不能再接收到此次用户操作数据。
若一个监听者设置吞没事件为false,即使onTouchBegan返回true,事件分发器对象EventDispatcher仍会按照zOrder顺序将本次触摸事件分发给别的监听者,响应其onTouchBegan函数,只有遇到设置吞没事件为true且onTouchBegan函数返回true的监听者时,事件分发器对象EventDispatcher才会停止此次事件的分发。
(3):其它理解
监听器必须加入到EventListener中才有效;
只要发生触摸事件,_eventDispatcher事件分发器会根据pLayer的zOrder顺序调用listener监听者的onTouchBegan函数(zOrder大的优先调用,相同的话后加入的节点优先调用),再根据onTouchBegan函数的返回值处理,同时相应的pLayer便作为Event传入onTouchBegan函数。这段话的理解可以举个例子,一个精灵对象sprite的zorder值高于一个菜单对象menu,当点击屏幕任何一处时,系统先响应sprite对象所绑定的listener监听者的回调函数onTouchBegan,若其返回false,则响应menu对象的回调函数;若其返回true,则不再响应menu对象的回调函数
只要发生触摸事件,listener响应高zOrder值player的onTouchBegan函数,同时player作为event事件传入onTouchBegan函数,通过unused_event->getCurrentTarget()获取。
(4):触摸位置判断
下面以以触摸对象为精灵pLayer1为例说明判断是否触摸到某目标的方法:
通过addEventListenerWithSceneGraphPriority将listener与pLayer绑定,触摸发生时,pLayer会作为event事件传入onTouchBegan函数,程序中可以获得pLayer尺寸与世界坐标系下的触摸坐标,通过convertToNodeSpace函数转会为节点坐标,最后进行判断。
touch->getLocation()即获取世界坐标系下的触摸坐标(世界坐标系是指以设计分辨率左下角为原点,上为Y轴正方向,右为X轴正方向的坐标系(OpenGL坐标系)),convertToNodeSpace函数是以对象的左下角为原点将世界坐标系下的触摸坐标转化为节点坐标系下的触摸坐标,若要以对象锚点为原点进行计算,则要用convertToNodeSpaceAR函数。
Rect rect = Rect(0, 0, s.width, 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上。
(2)第二种情况,一个TouchPoint点击在多个Target上。
(3)第三种情况,多个TouchPoint点击在多个Target上。
在EventDispatcher内部,对于每次触摸事件,它首先将每个触摸点单独作用在每个 EventListenerTouchOneByOne事件监听器上面,然后将所有触摸点的集合作用在每个 EventListenerTouchAllAtOnce 事件监听器上面。
但是单点触摸的操作会通过swallowTouches影响多点触摸的操作。下表列出了它们之间的一些影响,仅当onTouchBegan和swallowTouches同时为true时才会阻止事件的分发。
Touch ID | onTouchBegan | swallowTouches | OneByOne | AIIAtOnce |
---|---|---|---|---|
1 | true | true | 调用 | 无调用 |
2 | true | false | 调用 | 调用 |
3 | false | true | 无调用 | 调用 |
4 | false | fase | 无调用 | 调用 |
当然,和任何其他事件一样,任何时候我们都可以使用stopPropagation来停止该次 事件的分发。不过需要注意的是,stopPropagation只停止当前当次触摸状态下的所有分 发,例如MOVED状态会触发多次,则第二次不受前一次的影响;某个状态也不会影响 另一个状态的分发,例如MOVED不影响ENDED事件的分发。所有这些只需要明白, 每个状态每次分发都是一次独立的事件通知。