Cocos2dx 事件响应机制(2):事件处理机制

Cocos2dx 事件响应机制(2):事件处理机制,第1张

概述1 事件的工作机制 图1 传统事件系统如上图,模块A为事件触发者,模块B为事件响应者。A的实现依赖于模块B的实现,如果B的实现发生变化,A也可能需要作出相应调整。Cocos用订阅者模式将事件的触发者和响应者分开。触发者向一个公共的事件分发器发送一个事件消息,事件响应者向事件分发器订阅一个特定类型的消息来响应事件。以图1为例,B创建一个订阅者(ListenerB)并将此订阅者注册至事件分发器中,其中 1 事件的工作机制


图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,进而找到所有该事件的订阅者。

2.2 注册订阅者 2.2.1 事件优先级

指定事件优先级有两个作用: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):事件处理机制所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1084947.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-27
下一篇 2022-05-27

发表评论

登录后才能评论

评论列表(0条)

保存