zoukankan      html  css  js  c++  java
  • Cocos事件分发系统

    事件系统是一个软件中的核心组成部分,是一个软件系统的底层支持模块。今天我们就来讲一讲Cocos的事件分发是如何做的。

    订阅者模式

    所谓的事件,指的是当一个程序逻辑完成后,需要触发的逻辑。与一般的模块直接调用相比,事件可以不依赖于事件响应者的实现而预先定义一组事件类型,甚至可以在响应事件的时候动态添加事件或移除,大大增强了程序逻辑的灵活性。事件分发的逻辑一般是采用订阅者模式实现的。

    如图,系统中会有一个统一的调度中心负责事件的收发,订阅者订阅相应的事件,由调度中心存储;当发布者发送事件时,调度中心会查找所有订阅了该事件的订阅者,并按照一定顺序依次调用订阅者的响应函数。Cocos也是采用了这种逻辑。

    Cocos事件相关类定义

    class CC_DLL EventListener : public Ref
    {
    public:
        /** Type Event type.*/
        enum class Type
        {
            UNKNOWN,
            TOUCH_ONE_BY_ONE,
            TOUCH_ALL_AT_ONCE,
            KEYBOARD,
            MOUSE,
            ACCELERATION,
            FOCUS,
    		GAME_CONTROLLER,
            CUSTOM
        };
    
        typedef std::string ListenerID;
    
    protected:
        std::function<void(Event*)> _onEvent;   /// Event callback function
    
        Type _type;                             /// Event listener type
        ListenerID _listenerID;                 /// Event listener ID
        bool _isRegistered;                     /// Whether the listener has been added to dispatcher.
    
        int   _fixedPriority;   // The higher the number, the higher the priority, 0 is for scene graph base priority.
        Node* _node;            // scene graph based priority
        bool _paused;           // Whether the listener is paused
        bool _isEnabled;        // Whether the listener is enabled
        friend class EventDispatcher;
    };
    

    首先是EventListener,它就是Cocos中事件的订阅者。它包含了订阅者类型,以及listenerID。另外,_onEvent是事件相应时的回调函数。

    class CC_DLL Event : public Ref
    {
    public:
        /** Type Event type.*/
        enum class Type
        {
            TOUCH,
            KEYBOARD,
            ACCELERATION,
            MOUSE,
            FOCUS,
            GAME_CONTROLLER,
            CUSTOM
        };
        
    CC_CONSTRUCTOR_ACCESS:
        /** Constructor */
        Event(Type type);
    public:
        /** Destructor.
         */
        virtual ~Event();
    
        /** Gets the event type.
         *
         * @return The event type.
         */
        Type getType() const { return _type; }
        
        
    protected:
        Type _type;     ///< Event type
        bool _isStopped;       ///< whether the event has been stopped.
        friend class EventDispatcher;
    };
    

    然后是Event类,它有一个类型属性_type,调度中心通过该属性来定位到相应的listenerID,从而调用相应的事件响应。

    class CC_DLL EventDispatcher : public Ref
    {
    public:
        void addEventListenerWithSceneGraphPriority(EventListener* listener, Node* node);
        void addEventListenerWithFixedPriority(EventListener* listener, int fixedPriority);
        void removeEventListener(EventListener* listener);
        void dispatchEvent(Event* event);
    protected:
        /** Listeners map */
        std::unordered_map<EventListener::ListenerID, EventListenerVector*> _listenerMap;
        
        /** The map of dirty flag */
        std::unordered_map<EventListener::ListenerID, DirtyFlag> _priorityDirtyFlagMap;
        
        /** The map of node and event listeners */
        std::unordered_map<Node*, std::vector<EventListener*>*> _nodeListenersMap;
        
        /** The map of node and its event priority */
        std::unordered_map<Node*, int> _nodePriorityMap;
        
        /** key: Global Z Order, value: Sorted Nodes */
        std::unordered_map<float, std::vector<Node*>> _globalZOrderNodeMap;
        
        /** The listeners to be added after dispatching event */
        std::vector<EventListener*> _toAddedListeners;
    
        /** The listeners to be removed after dispatching event */
        std::vector<EventListener*> _toRemovedListeners;
    
        /** The nodes were associated with scene graph based priority listeners */
        std::set<Node*> _dirtyNodes;
    

    最后来看一下Cocos中负责事件调度的EventDispatcher。addEventListenerWithSceneGraphPriority和addEventListenerWithFixedPriority是负责注册事件监听的函数,前者和具体的node绑定,后者和一个优先级ID绑定,这决定了响应事件的次序。_listenerMap存储了listenerID和EventListener对象的映射关系,该类就是通过它来根据传入的event来找到对应的listener的。

    事件分发逻辑

    接下来我们重点来看一下事件分发的代码:

    void EventDispatcher::dispatchEvent(Event* event)
    {
        if (!_isEnabled)
            return;
        
        // 1. 更新_priorityDirtyFlagMap
        updateDirtyFlagForSceneGraph();
        
        // 2. 增加嵌套事件的深度
        DispatchGuard guard(_inDispatch);
        
        // 3. 处理触屏事件
        if (event->getType() == Event::Type::TOUCH)
        {
            dispatchTouchEvent(static_cast<EventTouch*>(event));
            return;
        }
        
        auto listenerID = __getListenerID(event);
        
        // 4.对要接收消息的listener排序
        sortEventListeners(listenerID);
        
        auto pfnDispatchEventToListeners = &EventDispatcher::dispatchEventToListeners;
        if (event->getType() == Event::Type::MOUSE) {
            pfnDispatchEventToListeners = &EventDispatcher::dispatchTouchEventToListeners;
        }
        auto iter = _listenerMap.find(listenerID);
        // 5. 调用事件响应
        if (iter != _listenerMap.end())
        {
            auto listeners = iter->second;
            
            auto onEvent = [&event](EventListener* listener) -> bool{
                event->setCurrentTarget(listener->getAssociatedNode());
                listener->_onEvent(event);
                return event->isStopped();
            };
            
            (this->*pfnDispatchEventToListeners)(listeners, onEvent);
        }
        
        // 6. 更新事件分发的listner
        updateListeners(event);
    }
    

    这段代码大致做了以下的事情:
    1.更新_priorityDirtyFlagMap,也就是记录了当前有哪些listener所绑定节点的属性被更改
    2.增加嵌套事件的深度。一个事件的响应中是可以再次发送一个事件的,由DispatchGuard类来记录嵌套事件的深度

    class DispatchGuard
    {
    public:
        DispatchGuard(int& count):
                _count(count)
        {
            ++_count;
        }
    
        ~DispatchGuard()
        {
            --_count;
        }
    
    private:
        int& _count;
    };
    
    }
    

    3.处理触屏事件
    4.对要接收消息的listener排序。排序的逻辑是:首先检查listenerID是否在_priorityDirtyFlagMap中,若是的话,说明listener的顺序可能被更改,因此需要重新排序。排序的顺序是:对于优先级小于0的订阅者,按照优先级从小到大排序;然后是所有与Node关联的订阅者,按照在UI场景中的层级从前往后排序;最后是优先级大于0的订阅者,按优先级从小到大排序
    5.调用订阅者的事件响应
    6.更新事件分发的listener。因为在调用事件的过程中可能出现增加或减少订阅者的情况,因此需要在处理完之后,更新相应的数据结构(例如_listenerMap),以供下次事件调用使用

    为了应对在调用响应事件时更改订阅者属性、影响之后的响应的情况,Cocos设置了_priorityDirtyFlagMap来记录更改过后(例如setLocalZOrder,或者调换节点的UI排布)的订阅者,在每次发送事件前检查,如果被更改过,则需要重新排序。另外,在事件响应的函数内,如果增添或删除订阅者,那么会暂时存储在_toAddedListeners和_toRemovedListeners中,直到本次响应结束后,在updateListeners里重新计算_listenerMap等属性,以供下次事件时使用。

    最后我想说,我把这篇文章给公主讲了一遍,她表示看懂了=w=

  • 相关阅读:
    帧间编码的预测自适应量化系数扫描排序
    调试vp8编码和解码程序
    WPF学习——制作一个简单的录入界面(2):用C#编程实现所有控件的功能
    自适应变异引用(AWR)方法(an adaptive warped reference (AWR) method )
    vp8编解码调试(环境vs2005)
    加油
    开始工作blog了
    vs2010编译vp8
    WPF学习——制作一个简单的录入界面(1): 添加需要的控件
    JPEG编码
  • 原文地址:https://www.cnblogs.com/wickedpriest/p/12240573.html
Copyright © 2011-2022 走看看