这一章,我们来分析Cocos2d-x 事件机制相关的源码, 根据Cocos2d-x的工程目录,我们可以找到所有关于事件的源码都存在放在下图所示的目录中。
从这个event_dispatcher目录中的文件命名上分析 cocos2d-x与事件相关的类一共有四种, Event,EventListener,EventDispatcher,Touch分别为 事件,事件侦听器,事件分发器,触摸
我们先从Event类开始。
打开CCEvent.h文件
@H_403_22@/** * Base class of all kinds of events. */ class Event : public Ref { : enum class Type { TOUCH,KEYBOARD,ACCELERATION,MOUSE,CUSTOM }; protected* Constructor */ Event(Type type); * Destructor */ virtual ~Event(); * Gets the event type inline Type getType() const { return _type; }; * Stops propagation for current event inline void stopPropagation() { _isStopped = true; }; * Checks whether the event has been stopped bool isStopped() _isStopped; }; * @brief Gets current target of the event * @return The target with which the event associates. * @note It onlys be available when the event listener is associated with node. * It returns 0 when the listener is associated with fixed priority. inline Node* getCurrentTarget() { _currentTarget; }; * Sets current target void setCurrentTarget(Node* target) { _currentTarget = target; }; Type _type; ///< Event type bool _isStopped; < whether the event has been stopped. Node* _currentTarget; < Current target friend EventDispatcher; };
这个类并且不复杂,先看一下类的注释,Event类是所有事件类的基类。
类定义的最上面有一个枚举,定义了事件的类型
事件的各类分别为 ,触摸事件, 键盘事件, 加速器事件,鼠标事件, 用户自定义事件。
再看一下Event类的成员变量
_type 描述当前对象的事件类型。
_isStopped 描述当前事件是否已经停止
_currentTarget 是侦听事件的Node类型的对象
就这三个成员变量,含义很简单,Event类的几个方法也没什么特别的,就是成员变量的get set方法。
下面我们再看一下用户自定义事件 EventCustom 类的定义,来了解一下。
在自定义事件类中,多出了两个成员变量 一个是 _userData用来记录用户自定义数据,另一个_eventName用户给这个事件起的别名。
其它的关于Event的子类,大家可以自己看一下,内容都差不多。
下面我们来分析 EventListener 这个类。
这个类看着挺长,其实没什么内容,先看下类的定义。
EventListener也同样定义了一个类型
这个类型与Event的类型有一小点不同,就是将触摸事件类型分成了 One by One (一个接一个) 与 All At Once (同时一起)两种。
再看 EventListener 的属性
std::function<void(Event*)> _onEvent; // 用来记录侦听器回调函数 Type _type; 侦听器的类型 ListenerID _listenerID; 侦听器的ID 其实是个字符串 bool _isRegistered; 标记当前侦听器是否已经加入到了事件分发器中的状态变量 int _fixedPriority; 侦听器的优先级别,数值越高级别越高.默认为0 Node* _node; 场景结点(这里这个变量的作用还没能理解好,后面我们再进行分析) bool _paused; 标记此侦听器是否为暂停状态。 bool _isEnabled; 标记此侦听器是否有效
上面分析了属性的功能 。EventListener的很简单,大部分都是属性的读写方法(get/set)这里就不多说了,下面我们重点看一下init方法及实现。
这个init函数 也很简单,就是一些成员变量的赋值操作。
在EventListener定义中有两个纯虚函数,我们看一下。
* Checks whether the listener is available. virtual bool checkAvailable() = 0; * Clones the listener,its subclasses have to override this method. virtual EventListener* clone() = 0;
通过注释了解这两个函数 一个是验证listener是否有效 别一个是clone方法。
EventListener是抽象类,那么咱们找一个它的子类的具体实现。
我们看一下EventListenerCustom这个类的定义。
EventListenerCustom又增加了一个成员变量,_onCustomEvent 接收一个EventCustom类型的事件为参数的回调函数。
我们可以看到,这个类的构造函数不是public形式的,所以这个类不能直接被实例化,
找到了EventListenerCustom提供了一个静态函数 create 所以实例化这个在的对象一定要使用这个create方法。
我们看一下create方法。
EventListenerCustom* EventListenerCustom::create( callback) { EventListenerCustom* ret = new EventListenerCustom(); if (ret && ret->init(eventName,callback)) { ret->autorelease(); } else { CC_SAFE_DELETE(ret); } ret; }
这个Create函数的结构,与Node的Create结构一样,新创建的EventListener加入到了autorelease列表里面,在Create的时候调用了init函数,我们再看一下EventListenerCustom::init方法。
这个函数也没什么特别的,但值得注意的是,CustomEvent的回调与基类的回调函数是怎么关联的。
在EventListenerCustom类中有一个成员变量_onCustomEvent它是一个函数指针来记录事件触发后的回调函数。
在EventListener类中也有一个_onEvent成员变量来记录事件触发的时候的回调函数。
EventListenerCustom::init函数中 有一个匿名函数,listener 在这个匿名函数中 判断了_onCustomEvent是否为空,如果不为空那么调用_onCustomEvent。
后面调用基类的init方法,把这个匿名函数传递给了基类的_onEvent,这样基类的回调函数_onEvent与子类的_onCustomEvent就关联起来了。
下面我们看一下抽象方法在子类中怎么实现的
EventListenerCustom* EventListenerCustom::clone() { EventListenerCustom* ret = init(_listenerID,_onCustomEvent)) { ret-> ret; }
clone的操作与Create方法很象,其实就是重新创建了一个对象,它的_listenerID 、 _onCustomEvent值与原对象一样。返回的是新创建的对象指针。
事件类Event有了,侦听器类EventListener 有了,那么下面我们来分析事件的分发器EventDispatcher。
打开CCEventDispatcher.h文件
这个类也是Ref的子类,从注释上面我们先整体的了解EventDispatcher的功能及作用。这个类管理事件侦听脚本及事件的分发处理。有一个事件侦听器列表,来记录所有侦听的事件。分析这个类我们首先还是从这个类的属性上开始。
* 优先级索引*/
_nodePriorityIndex;
* 内部自定义侦听器索引*/
std::set<std::string> _internalCustomListenerIDs;
上面针对这个类的属性做了字面上的分析,大部分属性目前还不知道具体功能,不过没关系,后面我们一个一个去破解它的含义。
现在 我们来分析一下这个类的构造函数
构造函数不复杂,除了初始几个变量,我们可以看到,向_internalCustomListenerIDs加入了两个自定义的事件,这里还有一行注释,说明,在清除所有事件侦听器的时候内容侦听器是不会被清除的。
我们看一下这两个内容自定义的事件,一个是程序返回到后台,一个是程序返回到前台。估计这在这两个事件里面要做一些暂停的工作。
从EventDispatcher的成员变量上看,都是围绕着侦听器列表来定义的,那么我们就看一下把侦听器加入到侦听器列表的方法。
第一个add方法
从注释上可以知道这个方法的作用是,将一个指定的事件侦听器依照场景图的优先级顺序加入到侦听器列表里面, 这个方法与场景图的绘制顺序有关系,
场景的结点渲染顺序也就是zOrder的顺序,场景中的结点优先级一般都是0,侦听器的存放顺序就是 小于0 等于0 大于0这样一个顺序 。
我们看一下这个方法的实现
1. 先检查了侦听器是否有效
2. 将结点与侦听器做了关联
3. 设置优先级为0,从注释上我们已经得到这个信息了,这个方法加入的侦听器都是显示对象的,所以优先级都为0
4. 设置侦听器已经注册状态
5. 调用了addEventListener方法,将侦听器加入到EventDispatcher的侦听器列表里。
下面我们看一下addEventListener方法,了解是将侦听器加入到侦听器管理列表里的过程
这个方法判断了当前 是否在分发消息,如果没有分发消息那么就调用 forceAddEventListener 把侦听器加入到侦听器列表里面。
如果_indispatch不为0证明现在正在分发消息那么新加入的侦听器就放到了临时数组_toAddedListeners里面
不管管理器是不是在分发消息listener都有一个归宿,那么最后增加了listener一次引用计数。
下面我们看一下forceAddEventListener方法。
这个类里面涉及到了一个EventDispatcher的内部类 EventListenerVector 这样一个数据结构,
这个结构在这里不多分析了,很简单,这个结构里封装了两个数组,_fixedListeners 与_sceneGraphListeners ,分别保存优先级不为0的侦听器指针与优先级为0的侦听器指针。
我们看一下强制将一个侦听器加入到管理列表的过程
- _listenerMap是按照侦听器ID来做分类的,每个侦听器ID都有一个EventListenerVector 数组。在_listenerMap中找 listenerID与要加入的listener相同的侦听器列表
- 如果没找到就他那天个listenerID为listener->getListenerID();项加入到_listenerMap中。找到了就拿到这个ID的列表指针。
- 将要加入管理的侦听器放到列表中。
- 根据加入的侦听器的优先级别是不是0进行设置脏标记操作。
- 当优先级标记为0时肯定这个侦听器是与场景显示对象对象绑定的,找到这个绑定的Node对象与listener做了关联,调用了associateNodeAndEventListener方法,将结点与侦听器加入到了_nodeListenersMap列表里面。
- 因为侦听器有了增加,所以原侦听器列表就不是最新的了,cocos2d-x认为那就是脏数据,这样设置了关于这个侦听器ID的脏标记。
通过上述分析,我们可以进一步理解到EventDispatcher类内的几个侦听器列表变量的作用。
_listenerMap 用以侦听器类型(就是侦听器的ID)索引,值是一个数组,用来储存侦听同一侦听器ID的所有侦听器对象。
_priorityDirtyFlagMap 用来标记一类ID的侦听器列表是对象是否有变化,侦是侦听器ID,值为侦听级别。
_nodeListenersMap 用来记录结点类型数据的侦听器列表,通俗点说就是以结点为索引所有侦听的事件都存在这个map里面。
我们注意这里判断了node->isRunning()属性如果结点是在运行的结点,那么调用了resumeEventListenersForTarget方法。下面看下这个方法都做了些什么。
这个函数两个参数,第一个是目标结点对象,第二个参数是是否递归进行子对象调用。
这个函数过程,先在结点列表中找是否已经有这个结点了,找到之后将它的每个侦听器的暂停状态都 取消。然后设置这个结点为脏结点标记。
上面提到过设置脏的侦听器,这里看一下设置脏结点函数。
这个函数虽然没有递归参数来控制,但从实现 上来分析这经会递归 node结点的子结点,都设置成了脏结点。
这里出现了_dirtyNodes这个类成员变量,现在可以理解什么是脏结点了,就是侦听器有变化的结点。
下面我们分析EventDispatcher类的另一个加入侦听器的方法。
从注释我们先来一个整体的了解。
这个函数的作用是将一个指定优先级的侦听器加入到管理列表里面。
这里强调了,0这个优先级不能被使用,因为这是显示对象侦听器优先级别。如果小于0的优先级那么这个侦听器事件会在画面渲染之前被触发,大于0的优先级会在显示对象渲染之后触发事件回调。
好了,下面看实现过程。
与addEventListenerWithSceneGraPHPriority方法大同小异,就是对listener进行了一些参数赋值,后面还是调用的addEventListener方法。这里就不多说了,值得注意的一点是,这个listener初始也是设置成暂停的,上面分析到在addEventListener调用后会将暂停状态取消的。
这个用户自定义事件的侦听器注册方法。参数为一个事件名称与一个回调函数。
从注释里面可以了解这个侦听器的优先级会被设置成1与就是在场景渲染之后事件才被处理。
EventListenerCustom* EventDispatcher::addCustomEventListener( callback) { EventListenerCustom *listener = EventListenerCustom::create(eventName,callback); addEventListenerWithFixedPriority(listener,128)">1); listener; }
这个函数返回了一个EventListenerCustom对象,并且通过实现过程可以看出这个listener返回的对象已经被注册到了EventDispatcher管理列表里面。
看过了注册侦听器的方法,现在我们集中看一下注销侦听器的几个重载方法。
下面我们看一下其它重载版本的注销侦听器的函数。
这些方法我就不一个一个分析了,因为过程都与第一个版本的相似,就是查找,删除,释放引用 这几个操作。大家可以自行看一下代码。
再看一下侦听器侦听的暂停与恢复方法。
这两个方法也不用多说,就是设置了暂停属性。第二个参数是用来指定是否递归作用于子结点的。
接下来我们看一下 事件分发的方法,
从注释上初步了解这个函数是分发消息并且会注销那些被标记为要删除的侦听器。
下面我们看实现。
这个函数的流程为:
- 调用 updateDirtyFlagForSceneGraph 这个函数我们后面再分析实现,在这里从命名上可以知道这块处理了那些脏标记。
- 将TOUCH事件单独进行了处理,也就将Touch事件调用了dispatchTouchEvent这个方法。这个方法后面我们也单独分析。
- 把所有侦听当前传入的Event事件ID的侦听器进行了排序。sortEventListeners方法。
- 针对每个侦听当前事件的侦听器进行分发,使用了dispatchEventToListeners方法。
- 调用updateListeners 这个方法也后面分析。
其实这个dispatchEvent只是做了一个分拣操作,并没有直接去执行侦听器的回调方法。
上面过程中提到了几个重要的方法,我们下面一个一个分析。
updateDirtyFlagForSceneGraph
这个方法就是遍历了_dirtyNodes列表,看看有没有脏结点,一个一个的结点去设置新的事件优先级。最后将脏结点从_dirtyNodes里面删除。
sortEventListeners
这个方法作用是根据指定的事件ID来对结点进行排序。
函数过程为:
- 在脏列表里面找这个listenerID
- 如果脏列表里有这个事件ID那么才进行排序,这里在脏列表里面找有一个优化,如果脏列表里面没有,那么证明这类开事件没有变化,那么就不用排序,因为上次已经排列过顺序了。这块这么处理是按需来排序。很巧妙。
- 下面根据优先级权限来分别调用了sortEventListenersOfFixedPriority与sortEventListenersOfSceneGraPHPriority两个方法。
- 这里要注意一点,在渲染对象中间传递事件实际上是以当前运行的场景为根结点来进行排序的。
我们再看下这两个方法。
sortEventListenersOfFixedPriority
std::sort(fixedListeners->begin(),fixedListeners->end(),[](const EventListener* l1,255)">const EventListener* l2) { return l1->getFixedPriority() < l2->getFixedPriority(); }); 因为根据优先级排列顺序为 <0 >0 下面这块是找到第一个大于0优先级的索引。也就是分界点,优先级小于0的侦听器在场景渲染之前触发,大于0的侦听器在场景渲染之后触发。所以这个值很有用。 int index = for (auto& listener : *fixedListeners) { if (listener->getFixedPriority() >= ) ; ++index; } listeners->setGt0Index(index); #if DUMP_LISTENER_ITEM_PRIORITY_INFO log(-----------------------------------fixedListeners) { log(listener priority: node (%p),fixed (%d)",l->_node,l->_fixedPriority); } #endif }
sortEventListenersOfSceneGraPHPriority
这个场景渲染对象事件的优先级排列与上面一个函数过程类似,很好理解。不多说了。
dispatchEventToListeners
dispatchTouchEvent
这个函数大家可以自己看一下,这里不详细分析了,基本过程与dispatchEventToListeners 差不多 区别在于它区分了onebyone及all by once的处理方式。
触摸事件后继章节我们会单独分析。
updateListeners
{ empty()) { _priorityDirtyFlagMap.erase(iter->first); delete iter-> _listenerMap.erase(iter); } iter; } } _toAddedListeners.empty())//清理_toAddedListeners里的空项目
listener : _toAddedListeners)
{
forceAddEventListener(listener);
}
_toAddedListeners.clear();
}
}
至此消息分发过程我们分析完了。
下面看一下用户息定义的消息是怎么分发的
dispatchCustomEvent
参数为一个事件名称和一个用户自定义的数据指针,这里面创建了个文化EventCustom对象,然后调用了dispatchEvent(&ev);之后分发的过程与上面的一样了。
好啊,今天又啰嗦这么多,主要分析了三个东东,Cocos2d-x中的 事件、 侦听器、事件分发器
有经验的同学可以看出,其实这里用到了一个常用的设计模式就是观察者模式,采用了注册事件,触发采用回调函数来执行事件过程。
小鱼在这里总结一下:
- Cocos2d-x的事件有几种类型,触摸(TOUCH)、键盘(KEYBOARD)、重力器(ACCELERATION)、鼠标(MOUSE)、用户自定义类型(CUSTOM)
- 侦听器也有几种 TOUCH_ONE_BY_ONE,CUSTOM
- 分发器将事件对象传递给在分发器里注册的事件侦听对象,根据事件类型做匹配,匹配到合适的侦听器后就算事件触发了,调用 侦听器的回调函数来执行事件过程。
- Cocos2d-x引擎中有一个分发器对象,就是在Direct类中的_eventDispatcher这个变量,在创建Direct对象时进行的初始化。
今天Cocos2d-x的事件分发机制源码我们分析到这里,在event_dispatch目录里还有一些关于事件的类我们就不做具体分析了,大同小异,如果理解上面的内容自行阅读那部分源码是没问题的。
下一章 我们来阅读Cocos2d-x3.0有关场景Scene类的源码。