cocos2dx 3.10 事件机制
cocos2dx的事件机制里存在三类:Event、EventListener、EventDispatcher
先理解一下它们之间的关系
当我们按下按钮时(Event),会触发一个特定的事件(EventListener相当于回调函数),而这个特定的事件又存储在EventDispatcher里,可能按下这个按钮会触发多个事件,而事件的先后就是靠EventDispatcher来决定的。
Event的相关类
Event(基类), EventCustom(自定义事件), EventTouch(触摸事件), EventMouse(鼠标事件), EventKeyboard(键盘事件), EventFocus(控件获取焦点事件), EventAcceleration(加速计事件)
1. Event
1.1 事件是什么
当出现来自鼠标,键盘,触屏,摇杆等输入源的输入时,这个事实称之为事件
1.2 cocos2dx是如何处理事件
引擎无时无刻都在感受事件。
在循环中每一帧会调用pollEvents来检测外部事件,一旦有事件发生,就会调用EventDispatcher::dispatchEvent(Event* event),来判断事件是否需要处理,给谁处理的后续问题。
1.3 源码分析
Event
enum class Type
{
TOUCH,
KEYBOARD,
ACCELERATION,
MOUSE,
FOCUS,
CUSTOM
};
Type _type; ///< Event type
bool _isStopped; ///< whether the event has been stopped.
Node* _currentTarget;
可以看出Event主要包含三个变量,一个是事件类型_type(也就是定义的枚举类型:触摸、键盘等),isStopped判断事件是否停止,只要事件停止,其相关的Listener都要停止callback调用。
EventTouch
它对应于四种触摸操作,不同的EventCode可以告诉Listener来调用不同的callback。
enum class EventCode
{
BEGAN,
MOVED,
ENDED,
CANCELLED
};
EventCustom
它是用户自定义事件,userData记录用户自定义数据,另一个eventName是用户给事件取的别名
void* _userData; ///< User data
std::string _eventName;
2. EventListener
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
上面的源码都有英文注释,我就不多解释了,我只说一个最重要的_isRegistered,它判断事件有没有被注册,如果没有被注册就不会触发。(如何注册事件?将事件加入dispatcher)
3. EventDispatch
在讲它之前,我们先了解一下它的一个重要变量。
std::vector<EventListener*>* _fixedListeners;
std::vector<EventListener*>* _sceneGraphListeners;
sceneGraphListeners:一个事件(比如说触摸事件),需要按照一定的响应序列,依次对这些Node进行事件响应,所以该类型的事件都会绑定一个与此相关联的node,并且响应顺序是与node在scene下的zorder相关的。该类型下的事件优先级统一为0。(与渲染树有关)
fixedListeners:优先级根据 fixedPriority 的数值从小往大排序、
3.1 事件是如何分发的(非触摸事件)
- 先获取事件的监听ID
- 根据这个监听ID对这个事件的所有监听器进行排序
先分发事件到 fixedPriority < 0 的监听器中,然后再分发到 = 0 的监听器(SceneGraphPriority)中,最后在分发到 > 0 的监听器中,如果中途出现 onEvent 返回为 true 的结果,则终止分发 - 获取事件的所有监听器,根据排序一个一个触发回调函数
- 如果对当前监听ID,新增加或删除监听器,那么DirtyFlag就会标记为true,该监听ID就需要重新进行排序。如果当时正在分发事件,会把当前需要添加的监听器添加到待添加向量(_toAddedListeners)中,在事件分发完毕之后监听器需要从toAddedListeners中转移到正式向量中。
void EventDispatcher::dispatchEvent(Event* event)
{
...
// 先通过event获取到事件的标志ListenerID
auto listenerID = __getListenerID(event);
// 排序此事件的所有的监听器
sortEventListeners(listenerID);
// 分发事件逻辑的函数指针
auto pfnDispatchEventToListeners = &EventDispatcher::dispatchEventToListeners;
if (event->getType() == Event::Type::MOUSE) {
// 如果是鼠标事件重新赋值分发事件的函数指针
pfnDispatchEventToListeners = &EventDispatcher::dispatchTouchEventToListeners;
}
// 获取改事件的所有的监听器
auto iter = _listenerMap.find(listenerID);
if (iter != _listenerMap.end())
{
// 如果有,取出里面监听器的Vector
auto listeners = iter->second;
// 找到对应的监听器的时候会触发的回调函数
auto onEvent = [&event](EventListener* listener) -> bool{
event->setCurrentTarget(listener->getAssociatedNode());
// 触发onEvent回调
listener->_onEvent(event);
return event->isStopped();
};
// 调用函数指针分发事件
(this->*pfnDispatchEventToListeners)(listeners, onEvent);
}
...
}
3.2 dispatchTouchEvent(触摸事件的分发机制)
- listener根据Node的globlal ZOrder优先级排序后,依次响应。
- 当进入TouchEvent Began后,所有监听事件都会依次影响Touch Began。然后再依次响应Touch Move等,而不是一个一个由Began->Move->End
3.3 cocos监听事件的bug
只要出现了删除,修改,添加监听器的时候,监听器列表需要重新排序,都需要设置相应的 DirtyFlag 操作。但是 Cocos-2dx v3.10 里面的 updateListeners 函数有删除监听器的操作,然而并没有设置相应的 DirtyFlag 操作。
会抛出下面的异常
CCASSERT(listeners->getGt0Index() <= static_cast<ssize_t>(fixedPriorityListeners->size()), "Out of range exception!");
Gt0Index() 方法其实就是获取到当前监听器中 fixedPriority == 0 的监听器在监听器向量中的位置,它只有在给 Listener 排序的时候会设置,但是如果更新了对应 ListenerID 的向量(EventListenerVector),但是没有重新排序,就会出现 _gt0Index 未及时更新的情况,导致抛出这个异常。
引用:
Cocos2dx游戏引擎(3.x)----新的事件分发机制
cocos2dx之event事件(一)
cocos2dx之event事件(三):事件分发器EventDispatcher
Cocos2dx-v3.10 事件分发机制源码解析