事件系统
这个基本都是采用观察者模式实现的,本文记录下cocos2dx的实现逻辑。
监听的注册
addEventListener
分2种情况:如果有listener
正在触发,先加入缓存_toAddedListeners
中(在onevent
的监听触发完成后,调用updateListeners
将其加入orceAddEventListener
),否则调用forceAddEventListener
直接加入。
void EventDispatcher::addEventListener(EventListener* listener)
{
if (_inDispatch == 0)
{
forceAddEventListener(listener);
}
else
{
_toAddedListeners.push_back(listener);
}
listener->retain();
}
void EventDispatcher::forceAddEventListener(EventListener* listener)
- 获取
listener
的事件名,这里叫ListenerID:typedef std::string ListenerID;
- 判断
_listenerMap
是否有该事件,没有创建一个EventListenerVector
并加入_listenerMap
- 将
listener
加入EventListenerVector
- 如果
istener
的FixedPriority
是0,说明是scene graph priority listener,加入_nodeListenersMap
- 设置对应事件的监听队列的脏标,触发监听时根据这个排序
void EventDispatcher::forceAddEventListener(EventListener* listener)
{
EventListenerVector* listeners = nullptr;
EventListener::ListenerID listenerID = listener->getListenerID();
auto itr = _listenerMap.find(listenerID);
if (itr == _listenerMap.end())
{
listeners = new (std::nothrow) EventListenerVector();
_listenerMap.emplace(listenerID, listeners);
}
else
{
listeners = itr->second;
}
listeners->push_back(listener);
if (listener->getFixedPriority() == 0)
{
setDirty(listenerID, DirtyFlag::SCENE_GRAPH_PRIORITY);
auto node = listener->getAssociatedNode();
CCASSERT(node != nullptr, "Invalid scene graph priority!");
associateNodeAndEventListener(node, listener);
if (!node->isRunning())
{
listener->setPaused(true);
}
}
else
{
setDirty(listenerID, DirtyFlag::FIXED_PRIORITY);
}
}
针对UI节点添加的监听,cocos2dx进行了优化:
指定整数的优先级,通常要求开发者记住大量的优先级数字,而有时候一个处于更下层的 UI元素却被错误地指定了一个更高的优先级,给例如触摸的管理带来了不少麻烦。因此,Cocos2d-x3.0新增了一种将分发优先级与一个Node元素关联的方式,EventDispatcher将自动根据当前该Node元素绘制的相反顺序来决定分发优先级,即使该UI元素的层级发生变更,它也能正确处理,这样就大大简化了对触摸优先级的管理。
EventDispatcher分发事件的顺序如下:
- priority<0
- scene graph (priority=0)
- priority >0
即首先分发优先级小于0的订阅者,它们按优先级从小到大的顺序分发;
然后分发所有与Node元素关联的订阅者,它们按照自身关联的Node在UI场景中的层级从前往后分发;
最后分发所有优先级大于0的订阅者,按优先级从小到大的顺序分发。
实际上所有与Node关联的订阅者优先级都被设置为0,而开发者无法注册一个优先级为0的订阅者。
事件触发 dispatchEvent
_inDispatch ++
;方法也很巧妙,使用RAII,弄了个DispatchGuard类,在构造函数里加一,析构里减一dispatchTouchEvent
,对触摸事件,使用dispatchTouchEvent接口单独处理sortEventListeners
排序,从_priorityDirtyFlagMap
中判断对应事件的监听列表是否需要重新排序,采用stable_sort
进行稳定排序,设置了下优先级大于0的listner的索引位置index。额外的,使用宏来控制了log的输出,方便查找问题:
#if DUMP_LISTENER_ITEM_PRIORITY_INFO
log("-----------------------------------");
for (auto& l : *fixedListeners)
{
log("listener priority: node (%p), fixed (%d)", l->_node, l->_fixedPriority);
}
#endif
- 触发,遍历
EventListenerVector
,执行每个listener
的_onEvent
接口,可以使用stop
控制是否继续执行后面的listener
。
auto pfnDispatchEventToListeners = &EventDispatcher::dispatchEventToListeners;
if (event->getType() == Event::Type::MOUSE) {
pfnDispatchEventToListeners = &EventDispatcher::dispatchTouchEventToListeners;
}
auto iter = _listenerMap.find(listenerID);
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);
}
updateListeners
,处理监听触发过程中需要删除的监听,限制了只能在触发程度为1即第一次触发事件时才进行删除;新加的监听也是同样处理,新加的监听不会被本次事件触发。
事件和Node
到目前为止,在Cocos2d-x中,Node元素除了用来显示场景,还用来执行3种逻辑相关的操作:动画(Action)、更新回调(Schedule)和事件(Event)。
这些操作在Node元素从场景移除时将变得毫无意义。而在另外一些状态下,开发者可能也需要让一个Node元素停止处理这些操作,即使它仍然显示在场景中。
Cocos2d-x提供了两个方法用来暂停与恢复这些操作的执行,分别是 pause()方法和resume()方法,示例如下。
void Node::resume()
{
_scheduler->resumeTarget(this);
_actionManager->resumeTarget(this);
_eventDispatcher->resumeEventListenersForTarget(this);
}
void Node::pause()
{
_scheduler->pauseTarget(this);
_actionManager->pauseTarget(this);
_eventDispatcher->pauseEventListenersForTarget(this);
}
这可以在任何时间阻止或允许任何与该Node相关联的订阅者接收事件通知,但是这会同时影响更新回调和动画的执行。如果只是想控制与事件相关的逻辑,则可以直接调用resumeEventListenersForTarget()和 pauseEventListenerForTarget()方法。
在Node元素内部,当onEnter()和onExit()方法被调用时,会分别自动调用resume()和pause()方法。这也会导致所有子元素的动画、更新回调及事件订阅者都会被恢复与暂停。