图1
传统事件系统如上图,模块A为事件触发者,模块B为事件响应者。A的实现依赖于模块B的实现,如果B的实现发生变化,A也可能需要作出相应调整。Cocos用订阅者模式将事件的触发者和响应者分开。触发者向一个公共的事件分发器发送一个事件消息,事件响应者向事件分发器订阅一个特定类型的消息来响应事件。以图1为例,B创建一个订阅者(ListenerB)并将此订阅者注册至事件分发器中,其中ListenerB附带了响应事件时需要执行的回调函数地址callBackFunc。当事件发生时,A使得事件发生器发出消息通知,以此触发B中的回调函数。
// 事件触发者A 以按下事件 touchesBegin 为例 voID GLVIEw::handletouchesBegin(int num,intptr_t IDs[],float xs[],float ys[]) { // ... touchEvent._eventCode = Eventtouch::EventCode::BEGAN; // 当游戏发生 touchesBegin 事件时,通知分发器发送相关事件 auto dispatcher = Director::getInstance()->getEventdispatcher(); dispatcher->dispatchEvent(&touchEvent); } // 事件响应者B voID Widget::settouchEnabled(bool enable) { if (enable == _touchEnabled) { return; } _touchEnabled = enable; if (_touchEnabled) { // 为Widget创建一个订阅者 _touchListener = EventListenertouchOneByOne::create(); CC_SAFE_RETAIN(_touchListener); _touchListener->setSwallowtouches(true); // 将响应事件时需要回调的函数加至订阅者中 _touchListener->ontouchBegan = CC_CALLBACK_2(Widget::ontouchBegan,this); _touchListener->ontouchmoved = CC_CALLBACK_2(Widget::ontouchmoved,this); _touchListener->ontouchended = CC_CALLBACK_2(Widget::ontouchended,this); _touchListener->ontouchCancelled = CC_CALLBACK_2(Widget::ontouchCancelled,this); // 将此订阅者注册至事件分发器中 _eventdispatcher->addEventListenerWithSceneGraPHPriority(_touchListener,this); } else { _eventdispatcher->removeEventListener(_touchListener); CC_SAFE_RELEASE_NulL(_touchListener); } }2 Cocos2dx中的事件分发器 2.1 基本组成
事件处理机制的组成部分包括:事件源、订阅者与分发者。事件源包含了该事件的类型Type与ListenerID。订阅者包含了订阅者类型与ListenerID,因此三者之间的关系是:分发器Eventdispatch根据事件的类型找到对应的ListenerID,进而找到所有该事件的订阅者。
指定事件优先级有两个作用:1)让某些元素优先处理,并不再向后面的订阅者传递;2)控制元素间逻辑处理上的优先级。
// 方式1:通过关联UI指定事件分发优先级voID Eventdispatcher::addEventListenerWithSceneGraPHPriority(EventListener* Listener,Node* node){ if (!Listener->checkAvailable()) return; // 关联到特定node Listener->setAssociatednode(node); // 优先级数字默认为0 Listener->setFixedPriority(0); Listener->setRegistered(true); addEventListener(Listener);}// 方式2:指定一个整数的优先级voID Eventdispatcher::addEventListenerWithFixedPriority(EventListener* Listener,int fixedPriority){ if (!Listener->checkAvailable()) return; Listener->setAssociatednode(nullptr); Listener->setFixedPriority(fixedPriority); Listener->setRegistered(true); Listener->setPaused(false); addEventListener(Listener);}
关联到具体UI的优先级指定方式很多时候要优于指定整数优先级的方式。后者需要开发者创建并关注一堆毫无意义的优先级枚举变量,这很有可能会导致某些问题,如:UI层级低的事件优先级数值比层级高的数值大,具体表现为:被遮挡的UI响应了触发事件而位于前面的UI却无任何反应。这肯定是不合理的。实际上,通过UI设置事件优先级的机制是在Cocos2dx 3.x之后引入的。
2.2.2 添加订阅者上述关联代码需要关注的一个函数是addEventListener
。事件的分发是可以嵌套的,即可以在一个事件中触发另一个事件。_indispatch记录了事件的嵌套数目,0表示没有事件需要分发。何时会发生事件的循环嵌套?举例说明:当按下A节点时,分发touch事件,执行相关ontouchEvent
函数,该函数内实现了一个自定义事件,并再次调用事件分发函数,此时就产生了嵌套。
voID Eventdispatcher::addEventListener(EventListener* Listener){ // _indispatch:事件嵌套数 if (_indispatch == 0) { // 无嵌套时调用 forceAddEventListener(Listener); } else { // 存在嵌套时调用 _toAddedListeners.push_back(Listener); } Listener->retain();}
无嵌套时的添加过程
voID Eventdispatcher::forceAddEventListener(EventListener* Listener){ EventListenerVector* Listeners = nullptr; EventListener::ListenerID ListenerID = Listener->getListenerID(); auto itr = _ListenerMap.find(ListenerID); if (itr == _ListenerMap.end()) { // 如果_ListenerMap中没有当前订阅者类型 创建一个新的订阅者数组 Listeners = new (std::nothrow) EventListenerVector(); _ListenerMap.emplace(ListenerID,Listeners); } else { Listeners = itr->second; } // 将订阅者加入至当前类型的容器中 Listeners->push_back(Listener); // 订阅者与UI绑定 if (Listener->getFixedPriority() == 0) { // 标记当前订阅者类型 该标记用于加速事件排序 setDirty(ListenerID,DirtyFlag::SCENE_GRAPH_PRIORITY); auto node = Listener->getAssociatednode(); // 添加至nodeListenerMap,该map的key为node,value为所有关联到该node的订阅者 associateNodeAndEventListener(node,Listener); if (node->isRunning()) { // 节点关联的所有订阅者:setPause(false)|setDirty() resumeEventListenersForTarget(node); } } else { setDirty(ListenerID,DirtyFlag::FIXED_PRIORITY); }}
该过程主要做了两件事:
将传入的订阅者添加至两个容器中:_ListenerMap 与 _nodeListenersMap。前者以ListenerID 为key,后者以node为key。使用两个容器,以空间开销换时间检索效率;
标记订阅器:1) 将当前类型的订阅器做标记;2) 将node关联到的所有订阅器做标记。为何要做标记?这是用于加速订阅器的排序。订阅器的优先级随时会发生变动,为了保证事件分发能够按照正确顺序进行,事件分发时必须首先进行订阅器的排序。但为了避免频繁且重复的排序导致的性能问题,在订阅器发生变动时打上标记。排序时仅 *** 作标记过的订阅者。具体排序实现后续会介绍。
// 标记传入类型的订阅者voID Eventdispatcher::setDirty(const EventListener::ListenerID& ListenerID,DirtyFlag flag){ // 标记的类型存入 _priorityDirtyFlagMap 映射表 auto iter = _priorityDirtyFlagMap.find(ListenerID); if (iter == _priorityDirtyFlagMap.end()) { // 映射表未找到 存入 _priorityDirtyFlagMap.emplace(ListenerID,flag); } else { //映射表找到 基于位的或 *** 作更新 int ret = (int)flag | (int)iter->second; iter->second = (DirtyFlag) ret; }}// 标记传入node关联到的所有订阅者voID Eventdispatcher::setDirtyForNode(Node* node){ // Mark the node dirty only when there is an eventListener associated with it. if (_nodeListenersMap.find(node) != _nodeListenersMap.end()) { _dirtyNodes.insert(node); } // Also set the dirty flag for node's children const auto& children = node->getChildren(); for (const auto& child : children) { setDirtyForNode(child); }}事件嵌套时的添加过程
当事件嵌套时,传入的订阅者不会立即被立即添加至相关容器中,而是先放置在待处理容器_toAddedListeners中。这些待处理的订阅者将在当前分发过程结束时加入,具体实现在后文的事件分发中描述。 2.3 事件分发
touch事件是所有类型中最常用也是最复杂的一种事件,下文将以touch事件为例详细剖析事件分发的核心过程。
2.3.1 事件触发源touch事件的触发源在GLVIEw中发生。
// touch begin事件voID GLVIEw::handletouchesBegin(int num,float ys[]){ touchEvent._eventCode = Eventtouch::EventCode::BEGAN; auto dispatcher = Director::getInstance()->getEventdispatcher(); dispatcher->dispatchEvent(&touchEvent);}// touch move事件voID GLVIEw::handletouchesMove(int num,float ys[],float fs[],float ms[]){ touchEvent._eventCode = Eventtouch::EventCode::MOVED; auto dispatcher = Director::getInstance()->getEventdispatcher(); dispatcher->dispatchEvent(&touchEvent);}// end or cancel ...2.3.2 分发过程
事件分发的入口为dispatchEvent
,这一函数包含了事件分发的主要过程。我们将逐步研究函数内部细节实现。
voID Eventdispatcher::dispatchEvent(Event* event){ if (!_isEnabled) return; updateDirtyFlagForSceneGraph(); dispatchGuard guard(_indispatch); if (event->getType() == Event::Type::touch) { dispatchtouchEvent(static_cast<Eventtouch*>(event)); return; } // ... 其他类型事件处理 略}
updateDirtyFlagForSceneGraph
当关联的UI发生层级变化时,需要更新该UI节点对应的所有事件分发顺序。如在父控件上有A B两个子节点。起初A遮盖B,之后基于逻辑调整,B层级被调整并高过A,此时B事件响应等级也应当高于A。
voID Eventdispatcher::updateDirtyFlagForSceneGraph(){ if (!_dirtyNodes.empty()) { for (auto& node : _dirtyNodes) { auto iter = _nodeListenersMap.find(node); if (iter != _nodeListenersMap.end()) { for (auto& l : *iter->second) { // 标记node中所有订阅者 setDirty(l->getListenerID(),DirtyFlag::SCENE_GRAPH_PRIORITY); } } } _dirtyNodes.clear(); }}
_dirtyNodes存放了一堆需要更新其订阅者的节点。这些节点什么时候会被加入至_dirtyNodes中呢?1) 节点的某个订阅者发生变化,如向该节点加入一个订阅者;2)节点的层级发生变化,实现过程如下。
voID Node::setLocalZOrder(int z){ if (getLocalZOrder() == z) return; // 设置父节点下的层次 _setLocalZOrder(z); if (_parent) { _parent->reorderChild(this,z); } _eventdispatcher->setDirtyForNode(this);}
dispatchGuard
在函数内部创建一个dispatchGuard,该变量被分配在栈上,创建时_indispatch嵌套数量加1,当前事件分发函数结束时变量自动析构,嵌套数减1。
class dispatchGuard{public: dispatchGuard(int& count):_count(count) { ++_count; } ~dispatchGuard() { --_count; }private: int& _count;};
dispatchtouchEvent
该函数包含了touch触摸事件分发的全部过程,包括:订阅者排序、将事件处理函数分发至订阅者以及更新订阅者。
voID Eventdispatcher::dispatchtouchEvent(Eventtouch* event){ // 不同类型分开排序 sortEventListeners(EventListenertouchOneByOne::ListENER_ID); sortEventListeners(EventListenertouchAllAtOnce::ListENER_ID); auto oneByOneListeners = getListeners(EventListenertouchOneByOne::ListENER_ID); auto allAtOnceListeners = getListeners(EventListenertouchAllAtOnce::ListENER_ID); // If there aren't any touch Listeners,return directly. if (nullptr == oneByOneListeners && nullptr == allAtOnceListeners) return; bool isNeedsMutableSet = (oneByOneListeners && allAtOnceListeners); const std::vector<touch*>& originaltouches = event->gettouches(); std::vector<touch*> mutabletouches(originaltouches.size()); std::copy(originaltouches.begin(),originaltouches.end(),mutabletouches.begin()); // process the target handlers 1st if (oneByOneListeners) { auto mutabletouchesIter = mutabletouches.begin(); for (auto& touches : originaltouches) { bool isSwallowed = false; auto ontouchEvent = [&](EventListener* l) -> bool { // Return true to break EventListenertouchOneByOne* Listener = static_cast<EventListenertouchOneByOne*>(l); // Skip if the Listener was removed. if (!Listener->_isRegistered) return false; event->setCurrentTarget(Listener->_node); bool isClaimed = false; std::vector<touch*>::iterator removedIter; Eventtouch::EventCode eventCode = event->getEventCode(); if (eventCode == Eventtouch::EventCode::BEGAN) { if (Listener->ontouchBegan) { isClaimed = Listener->ontouchBegan(touches,event); if (isClaimed && Listener->_isRegistered) { Listener->_claimedtouches.push_back(touches); } } } else if (Listener->_claimedtouches.size() > 0 && ((removedIter = std::find(Listener->_claimedtouches.begin(),Listener->_claimedtouches.end(),touches)) != Listener->_claimedtouches.end())) { isClaimed = true; switch (eventCode) { case Eventtouch::EventCode::MOVED: if (Listener->ontouchmoved) { Listener->ontouchmoved(touches,event); } break; case Eventtouch::EventCode::ENDED: if (Listener->ontouchended) { Listener->ontouchended(touches,event); } if (Listener->_isRegistered) { Listener->_claimedtouches.erase(removedIter); } break; case Eventtouch::EventCode::CANCELLED: if (Listener->ontouchCancelled) { Listener->ontouchCancelled(touches,event); } if (Listener->_isRegistered) { Listener->_claimedtouches.erase(removedIter); } break; default: CCASSERT(false,"The eventcode is invalID."); break; } } // If the event was stopped,return directly. if (event->isstopped()) { updateListeners(event); return true; } if (isClaimed && Listener->_isRegistered && Listener->_needSwallow) { if (isNeedsMutableSet) { mutabletouchesIter = mutabletouches.erase(mutabletouchesIter); isSwallowed = true; } return true; } return false; }; // dispatchtouchEventToListeners(oneByOneListeners,ontouchEvent); if (event->isstopped()) { return; } if (!isSwallowed) ++mutabletouchesIter; } } // ... allAtOnceListeners 处理部分 略 updateListeners(event);}1)订阅者排序
EventListenertouchOneByOne 与 EventListenertouchAllAtOnce的相关逻辑是分开处理的,因此排序方面也是独立进行。排序前,首先判断当前订阅者类型是否被记录在标记映射表内,如未标记则不进行任何 *** 作;之后基于标记类型判断订阅者需要进行何种类型(数值指定优先级类型 与 节点赋予的优先级类型)的排序。
voID Eventdispatcher::sortEventListeners(const EventListener::ListenerID& ListenerID){ DirtyFlag dirtyFlag = DirtyFlag::NONE; // 先检测当前类型订阅器是否需要排序 auto dirtyIter = _priorityDirtyFlagMap.find(ListenerID); if (dirtyIter != _priorityDirtyFlagMap.end()) { dirtyFlag = dirtyIter->second; } // 仅当当前类型被标记时 再排序,这一优化能较大的提升排序效率 if (dirtyFlag != DirtyFlag::NONE) { // Clear the dirty flag first,if `rootNode` is nullptr,then set its dirty flag of scene graph priority dirtyIter->second = DirtyFlag::NONE; // 订阅者优先级通过数值指定且被标记 if ((int)dirtyFlag & (int)DirtyFlag::FIXED_PRIORITY) { sortEventListenersOfFixedPriority(ListenerID); } // 订阅者优先级通过node指定且被标记 if ((int)dirtyFlag & (int)DirtyFlag::SCENE_GRAPH_PRIORITY) { auto rootNode = Director::getInstance()->getRunningScene(); if (rootNode) { sortEventListenersOfSceneGraPHPriority(ListenerID,rootNode); } else { dirtyIter->second = DirtyFlag::SCENE_GRAPH_PRIORITY; } } }}
下面来看如何对Node指定优先级类型的订阅者排序过程进行分析。该过程首先将需要排序的订阅者筛选出来。可以发现,检索 *** 作十分频繁,检索得事件复杂度至多为O(n),而排序的最优效率最高为O(nlog(n)),因此排序相较于检索要更加耗时。为保证排序高效完成,在排序前要采用响应手段尽量将无需参与排序的元素剔除;完成筛选后,更新_nodePriorityMap,该表内存储了所有节点对应的优先级,作为后续排序的凭证。为保证节点优先级的实时性与有效性,每次进行排序时都需要从根节点深度遍历一次所有UI;最后基于最新的优先级排序订阅者。
voID ventdispatcher::sortEventListenersOfSceneGraPHPriority(const EventListener::ListenerID& ListenerID,Node* rootNode){ auto Listeners = getListeners(ListenerID); if (Listeners == nullptr) return; auto sceneGraphListeners = Listeners->getSceneGraPHPriorityListeners(); if (sceneGraphListeners == nullptr) return; // reset priority index _nodePriorityIndex = 0; _nodePriorityMap.clear(); visitTarget(rootNode,true); // After sort: priority < 0,> 0 std::stable_sort(sceneGraphListeners->begin(),sceneGraphListeners->end(),[this](const EventListener* l1,const EventListener* l2) { return _nodePriorityMap[l1->getAssociatednode()] > _nodePriorityMap[l2->getAssociatednode()]; } );}
遍历一遍UI树并获得最新的层级表_nodePriorityMap。为什么要做层级表更新?主要有两个原因:1)保证节点局部层级的准确性:一个父节点下包含一些层级小于0的子节点与一些层级大于等于0的子节点,绘制的时候先绘制层级小于0的,之后是父节点,之后是层级大于等于0的;2)保证节点全局层级的准确性:cocos2dx 3.x版本之后可指定任意节点的全局层级,绘制时会优先绘制全局层级高的。如何满足第一点?很简单,首先对相当节点所有子节点排序以保证层次有序,之后采用中序遍历遍历。巧妙之处在于:中序遍历的结果与节点绘制顺序完全一致。如何满足第二点?借助于_globalZOrderNodeMap映射表容器,以全局层次为key,node为value。没有设置全局层次的默认为0,在中序遍历是按照访问顺序被依次加入容器中,设置了全局层次的,也同理。在最后的节点优先级统计阶段,首先将_globalZOrderNodeMap依照key排序,然后从小到大遍历,将每个关联了订阅器的节点优先级加1。
// 从根节点中序(深度)遍历UI树,更新_nodePriorityMapvoID Eventdispatcher::visitTarget(Node* node,bool isRootNode){ // 排序子节点 node->sortAllChildren(); int i = 0; auto& children = node->getChildren(); auto childrenCount = children.size(); // 有子节点 向下遍历 if(childrenCount > 0) { Node* child = nullptr; // visit children zOrder < 0 for( ; i < childrenCount; i++ ) { child = children.at(i); if ( child && child->getLocalZOrder() < 0 ) visitTarget(child,false); else break; } // 判断该节点是否关联了订阅器 if (_nodeListenersMap.find(node) != _nodeListenersMap.end()) { _globalZOrderNodeMap[node->getGlobalZOrder()].push_back(node); } // visit children zOrder >= 0 for( ; i < childrenCount; i++ ) { child = children.at(i); if (child) visitTarget(child,false); } } else { if (_nodeListenersMap.find(node) != _nodeListenersMap.end()) { _globalZOrderNodeMap[node->getGlobalZOrder()].push_back(node); } } // 所有子节点均访问结束 if (isRootNode) { std::vector<float> globalZOrders; globalZOrders.reserve(_globalZOrderNodeMap.size()); for (const auto& e : _globalZOrderNodeMap) { globalZOrders.push_back(e.first); } std::stable_sort(globalZOrders.begin(),globalZOrders.end(),[](const float a,const float b){ return a < b; }); for (const auto& globalZ : globalZOrders) { for (const auto& n : _globalZOrderNodeMap[globalZ]) { // 依照遍历顺序 优先级+1 _nodePriorityMap[n] = ++_nodePriorityIndex; } } _globalZOrderNodeMap.clear(); }}总结
以上是内存溢出为你收集整理的Cocos2dx 事件响应机制(2):事件处理机制全部内容,希望文章能够帮你解决Cocos2dx 事件响应机制(2):事件处理机制所遇到的程序开发问题。
如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)