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=