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

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();
}
  1. 无嵌套时的添加过程

    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);
          }
      }
  2. 事件嵌套时的添加过程
    当事件嵌套时,传入的订阅者不会立即被立即添加至相关容器中,而是先放置在待处理容器_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树,更新_nodePriorityMap
    void 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();
        }
    }

相关文章

操作步骤 1、创建cocos2d-x工程 2、新建 Scene1.cpp Scene1.h Scene1.h代码 #ifndef __SCENE1_H__#defi...
开发环境:OS(WINDOWS 8.1 X64 企业版) cocos2d-x 2.2.1 vs2010 想给vs安装上cocos的模版,执行Install...
把创建项目做成一个批处理,当创建项目时可以省时省力很多。 操作步骤 1、在 E:cocos2d-x-2.2.1toolspr...
https://www.cnblogs.com/JiaoQing/p/3906780.html 四个响应函数 1 EventListenerPhysicsContact* evC...
转载于 http://www.cnblogs.com/kenkofox/p/3926797.html 熟悉js的dom事件或者flash事件的,基本都能立...
ScrollView(滚动容器)加载大量item时会导致游戏界面的卡顿,严重时整个界面会出现卡死的情况。最近项...