Cocos事件分发系统

事件系统是一个软件中的核心组成部分,是一个软件系统的底层支持模块。今天我们就来讲一讲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=
Cocos事件分发系统

原文链接: https://www.cnblogs.com/wickedpriest/p/12240573.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    Cocos事件分发系统

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/325835

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年3月1日 下午3:30
下一篇 2023年3月1日 下午3:31

相关推荐