Cocos2d-x 3.0 事件系统【转】

事件系统,是一个软件的核心组成部分。从小处讲它是应用程序内部各模块交互的设计模式,从大处讲,它是软件架构的组成模块。在现代软件开发中,操做系统一般经过一些预约义的事件,告知应用程序发生的一些事情如用户输入,内存不足等。
然而,一般咱们并不直接使用系统事件,例如一个界面可能不一样区域的元素对触摸事件的理解都不同,在某些状况下须要优先处理某些逻辑,这就须要对系统事件再包装,以应对界面复杂的元素和逻辑。另外一方面,咱们须要一个事件系统用来在应用程序内部分发消息,例如当敌人进入攻击范围时通知英雄射击,当敌人血量低于0时播放死亡动画等等。这些都须要游戏引擎提供一个灵活的事件系统,它既能管理分发系统事件,还能借助其简单管理自定义事件。

 

Cocos2d-x 3.0将全部的事件统一集中到EventDispatcher中处理,它不光改进了触摸等系统事件的管理和使用方式,还使得咱们能够借助其处理程序自定义的事件。本章将学习相关的内容。
5.1 事件类型
要处理一个事件,首先得定义一个事件类型。事件系统老是按类型而不是实例来处理事件的订阅和分发,这样使得同一个事件能够有多个订阅者。Event是全部事件的基类,它用一个字符串来表示该事件的类型。咱们不该该直接使用Event,而应该从它继承实现自定义事件。事件类型一般不是一个变量,以保证相同类型的事件实例拥有相同的类型,但EventCustom除外,它能够在初始化的时候指定不一样的类型,这是为了简化编写事件类型。如下是Event类的定义:

class Event
{
protected:
Event(const std::string& type);node

public:
virtual ~Event();
inline const std::string& getType() const { return _type; };
inline void stopPropagation() { _isStopped = true; };
inline bool isStopped() const { return _isStopped; };
inline Node* getCurrentTarget() { return _currentTarget; };
protected:
inline void setCurrentTarget(Node* target) { _currentTarget = target; };std::string _type;
bool _isStopped;
Node* _currentTarget;
friend class EventDispatcher;
};
实际上Event的成员应该仅包含一个表示类型的字符串,然而在Cocos2d-x中有些事件的分发如触摸可能和Node的层级相关,因此它还包含一个获取关联元素的方法:getCurrentTarget();另外它仍是EventDispatcher的友元,这是为了方便处理触摸等事件分发,这些都会在本章后面分析。
一个Event实例其实是事件传递过程当中的数据,它由事件触发者构造,并传递给事件分发器,事件分发器根据其类型分别通知全部订阅该类型事件的订阅者,并将其做为参数传递给订阅者。由于事件是一种异步通讯机制,它一般没有回调,甚至一个类型的事件可能不包含任何订阅者,这就须要事件的触发者向接受者传递相关的上下文数据,接受者才能正确处理,例如EventTouch对象中会包含触摸点的信息,以便于订阅者处理逻辑。
Cocos2d-x引擎自带的事件类型包括:EventTouch,EventKeyboard,EventAcceleration,以及便于开发者自定义事件的EventCustom。
5.2 事件的订阅者
订阅者负责处理事件,它的成员包含一个订阅事件的类型(这个类型应该和对应的Event的类型一致),以及一个回调方法用来处理事件。这两个成员都应该只被事件分发器(EventDispatcher)使用,因此它们被定义为受保护的成员,同时EventListener被定义为EventDispatcher的友元:

class EventListener : public Object
{
protected:
EventListener();
bool init(const std::string& t, std::function<void(Event*)>callback);ios

public:
virtual ~EventListener();
virtual bool checkAvaiable() = 0;
virtual EventListener* clone() = 0;
protected:
std::function<void(Event*)> _onEvent;
    std::string _type;
bool _isRegistered;friend class EventDispatcher;
friend class Node;
};
在Cocos2d-x之前的版本中,订阅者以继承的方式定义,订阅者和处理逻辑的对象是同一个实体,例如CCLayer实现了CCTouchDelegate。而在3.0中EventListener被定义为一个变量,其好处是能够将处理方法定义为lambda表达式,这是3.0支持C++11的一个重要方面,它改变了使用事件的编程习惯,可是带来了lambda表达式的好处,编程更加灵活,你甚至能够在一个EventListener的处理程序中再定义一个EventListener变量。
与事件类型相对应,Cocos2d-x中自带的订阅者包括:EventListenerTouch,EventListenerKeyboard,EventListenerAcceleration以及EventListenerCustom。
5.3 事件的工做流程
在定义了事件和订阅者以后,应用程序只须要向事件分发器注册一个订阅者实例,便可在事件发生的时候获得通知。在Cocosd-x中负责事件的订阅,注销,分发的是EventDispatcher,它是一个单例,应用程序可经过EventDispatcher::getInstance()方法获取其实例。
下面经过一个示例来演示事件的工做方式,在这个示例中当CollisionSystem检测到两个Node之间发生碰撞时,将触发碰撞事件,而HitSystem是碰撞事件的其中一个订阅者,它会响应碰撞事件并修改敌人的生命值:

class CollisionEvent:public Event
{
public:
static const char* COLLISION_EVENT_TYPE;编程

CollisionEvent(Entity* l,Entity* r);设计模式

Entity* getLeft(){return _left;}
Entity* getRight(){return _right;}
private:
Entity* _left;
Entity* _right;安全

};
上述代码首先添加一个碰撞事件类CollisionEvent,它继承自Event,并用一个常量COLLISION_EVENT_TYPE定义其类型。
CollisionEvent做为事件传递的数据,应该向订阅者传递相关的上下文,这里须要传递的是发生碰撞的两个Entity实例,关于Entity Component System会在本书后面的章节讲述。

class CollisionListener : public EventListener
{
public:
static CollisionListener* create(std::function<void(CollisionEvent*)> callback);
virtual bool checkAvaiable() override;
virtual CollisionListener* clone() override;架构

protected:
CollisionListener();
bool init(std::function<void(CollisionEvent*)> callback);异步

std::function<void(CollisionEvent*)> _onCollisionEvent;ide

};
接下来,咱们须要定义订阅者,在CollisionListener的init()方法中,声明了它订阅事件的类型,经过查看CollisionListener的实现部分代码,能够看到它引用的是上面CollisionEvent定义的COLLISION_EVENT_TYPE。

void HitSystem::configure()
{
auto listener=CollisionListener::create(
[this](CollisionEvent* event){
this->hit(event);
});
EventDispatcher::getInstance()->addEventListenerWithFixedPriority(listener, 1);
}函数

而后,咱们须要向EventDispatcher注册订阅者。HitSystem会响应碰撞事件,因此咱们在HitSystem初始化的时候向EventDispatcher注册,并传递一个lambda表达式做为处理程序。

void CollisionSystem::update(float dt)
{学习

    if (collide()) {
CollisionEvent* event=new CollisionEvent(entity,collisionEntity);
EventDispatcher::getInstance()->dispatchEvent(event);
}
}
最后,是触发事件的程序。因为CollisionSystem负责碰撞检测,因此它会在检测到两个Node之间发生碰撞时,通知EventDispatcher分发此碰撞事件,并将发生碰撞的两个Entity做为数据保存在Event参数中。EventDispatcher在接受到事件通知的时候,首先根据Event参数的类型,查找与此类型相符的订阅者,在本示例程序中CollisionListener的类型与CollisionEvent的类型一致,因此将会执行CollisionListener中的回调方法。
因此,经过EventDispatcher咱们就能自定义各类事件,在应用程序的各个模块之间灵活通讯,大大简化了事件的处理,同时下降了模块间的耦合。
固然通常状况下并不须要像这样定义每个事件,能够直接使用EventCustom,它的构造函数接受一个类型参数,使得一样的EventCustom实例能够分发不一样类型的事件。同理,EventListenerCustom也接受一个类型参数,使得其能够处理不一样的事件类型。
5.4 深刻分析EventDispatcher
经过前面的学习,咱们应该初步学会了在Cocos2d-x中怎样使用通常的事件。然而更灵活熟练地使用事件,还须要深刻学习更多的知识,在进一步分析EventDispatcher的机制以前,咱们来总结一下通常在游戏中使用事件还有哪些特殊的需求:
  1. 设置订阅者的优先级,一个类型的事件可能拥有多个订阅者,所以有必要设置处理顺序,例如当碰撞事件完成以后,其中一个订阅者负责处理伤害计算,而另外一个订阅者可能作一些UI的操做,例如播放声音或者粒子效果。前者的优先级确定须要更高,由于后者的处理可能须要依赖于生命值的计算。
  2. 修改订阅者的优先级。
  3. 中止事件的继续分发,使后续的订阅者不用再处理该事件。
  4. 根据屏幕上元素的层级,而不是手动设定的优先级来处理事件分发,这在触摸事件的分发中尤为重要。
带着这些目标,咱们来分析EventDispatcher是怎样实现它们,以及咱们在应用程序中应该怎样使用它们。
首先,EventDispatcher提供了两种注册订阅者的方法:

void addEventListenerWithSceneGraphPriority(EventListener* listener, Node* node);

    void addEventListenerWithFixedPriority(EventListener* listener, int fixedPriority);
第一种提供一个相关联的Node,这样事件的处理将会依据该Node的绘制顺序来决定分发的优先级。第二种则是手动设定一个优先级,这样EventDispatcher将根据该优先级直接决定分发顺序。同时,经过第二种方法注册的订阅者还能够经过调用setPriority()方法修改优先级。
其次,EventDispatcher是怎样作到根据元素的绘制顺序来计算订阅者的优先级的呢?在Cocos2d-x引擎内部,每一个EventListener都被封装为一个EventListenerItem的结构体:

struct EventListenerItem
{
int            fixedPriority;
Node*          node;
EventListener* listener;
~EventListenerItem();

    };
若是订阅者与某个Node相关联,则node成员将被赋值,同时fixedPriority被设置为0。而且该listener变量会被添加到该Node的关联订阅者列表中。这样的设置会影响订阅者的排序,找到sortAllEventListenerItemsForType()方法,能够总结排序规则以下:
  1. 分发fixedPriority小于0的订阅者,fixedPriority越小则优先分发。
  2. 分发全部fixedPriority值为0的订阅者,而且没有与Node相关联的。
  3. 分发全部与Node相关联的订阅者,其关联Node的eventPriority越高(越处于屏幕最上层)则优先级越高。
  4. 分发全部fixedPriority大于0的订阅者,一样fixedPriority越小则优先分发。
这里有两个地方须要注意:首先,两种订阅方式的优先级进行比较并无太大意义,一般咱们应该避免同一个事件类型的订阅者混用两种注册方式;其次,Node的eventPriority变量不须要咱们操心,只要设置为根据Node计算优先级,则引擎会保证其与绘制顺序一致。
若是读者对此感兴趣,能够看到Node类有一个静态变量_globalEventPriorityIndex,在每一帧开始的时候,Director会将其重置为0,而后在Node的visit()方法中,每此调用draw()方法以后会调用updateEventPriorityIndex()方法,该方法以下:

inline void updateEventPriorityIndex() {
_oldEventPriority = _eventPriority;
_eventPriority = ++_globalEventPriorityIndex;
if (_oldEventPriority != _eventPriority)
{
setDirtyForAllEventListeners();
}

    };
由此计算,第一个绘制的Node其_eventPriority变量值为1,第二个Node其_eventPriority值为2…,一旦有新的Node被添加或者旧的Node被移除,这些值会从新计算,但始终能保证其和绘制顺序一致。而一旦某个Node的绘制顺序号发生了改变,则从新计算该Node关联的全部订阅者在EventDispatcher中的排列顺序,从而保证其处理顺序始终和绘制顺序一致。
每一个Event在被处理的时候,若是订阅者是经过与Node关联注册的,EventDispatcher还会将当前订阅者关联的Node临时保存在Event中,这样咱们还能够在事件处理程序中获取该Node,这就是前面看到的getCurrentTarget()方法。这在某些时候比较有用,由于EventListener能够和任何Node实例关联,事件处理代码所在的对象可能并无保留其引用。
最后是关于事件的禁止分发,因为一个事件会被多个订阅者处理,所以同一个Event实例会被传递给多个处理程序,这样每一个处理程序就能够修改这个共同的Event实例。若是其中一个处理程序调用Event的stopPropagation()方法,将其_isStopped设置为true,EventDispatcher就会中止对该事件的分发,后续的订阅者将接受不到事件通知。
此外,EventDispatcher还包含一些例如保证安全的代码,以及在须要的时候对订阅者重新排序等方法,dispatchEvent()方法还包含一个forceSortListeners的默认为false的参数它能够在分发事件以前对订阅者从新排序,但彷佛用处不大,由于通常影响优先级的因素如添加,移除节点,直接修改fixedPriority等都会致使订阅者从新排序,也许可能还存在一些特殊状况。
5.5 Cocos2d-x中的系统事件
对于系统事件,有的Cocos2d-x采起了特殊的处理,而有的咱们能够从中学习到使用事件的一些高级技巧,固然在实际使用过程当中最重要的仍是知道怎么熟练使用它们,以及明白它们在何时被触发。
5.5.1 EventTouch
触摸是Coco2d-x中最复杂的事件,为了简化最经常使用的单点操做,它要区分多点和单点触摸,对于单点的状况它还要在整个触摸操做过程当中记录一些状态。这些复杂的状况使得上面学习的分发机制不能直接处理,因此Cocos2d-x对于触摸事件作了特殊处理,咱们能够在EventDispatcher中找到一个私有的dispatchTouchEvent()方法,它专门用来处理触摸事件。
EventTouch事件分为两种,一种是PC或者Mac上的鼠标点击,另外一种是移动设备上面的触摸,其中前者只有一个触摸点。可是Cocos2d-x并不处理右键点击,而是直接交给父窗口或者系统处理。这些事件都由比较底层的EGLViewProtocol的实现者从系统捕获,而后封装成Cocos2d-x中的信息格式传递给EventDispatcher。值得注意的是,在这个封装的过程当中EGLViewProtocol会根据程序设置的分辨率方案对触摸点的位置进行调整。同时在ios平台默认状况下Cocos2d-x并不开启多点支持,读者须要在AppController.cpp中设置:
     [__glView setMultipleTouchEnabled:YES];
首先找到EventTouch的定义,它仅包含两个成员,_eventCode用来表面当前触摸事件的状态,它在EventTouch:EventCode中定义;_touches则保存者当前触摸状态下的触摸点信息:

class EventTouch : public Event
{
public:
enum class EventCode{
BEGAN,
MOVED,
ENDED,
CANCELLED
};

EventCode getEventCode() { return _eventCode; };
std::vector<Touch*> getTouches() { return _touches; };

#if TOUCH_PERF_DEBUG
void setEventCode(EventCode eventCode) { _eventCode = eventCode; };
void setTouches(const std::vector<Touch*>& touches) { _touches = touches; };
#endif
};

从定义能够看出,咱们还能够直接设置触摸信息模拟系统事件用于测试。Touch封装了一个触摸点的信息,应用程序能够从这里获取不少有用的信息:

class CC_DLL Touch : public Object
{
public:
/** 触摸点在OpenGL坐标系中的位置 */
Point getLocation() const;
/** 触摸点在OpenGL坐标系中的上一个位置 */
Point getPreviousLocation() const;
/** 触摸点在OpenGL坐标系的起点位置 */
Point getStartLocation() const;
/** 在OpenGL坐标系中当前位置与上一个位置的差 */
Point getDelta() const;
/** 触摸点在屏幕坐标系中的位置 */
Point getLocationInView() const;
/** 触摸点在屏幕坐标系中的上一个位置 */
Point getPreviousLocationInView() const;
/** 触摸点在屏幕坐标系的起点位置 */
Point getStartLocationInView() const;

int getID() const{ return _id; }

};
有了这些信息,咱们就能够在程序中进行精准的触摸判断,例如断定是否点中某个区域,以及是否在触摸事件结束的时候离开了某个区域,还能够在cancelled事件发生时根据触摸点发生的位移还原一些元素的位置等等,后面将分析一些实际例子。
在dispatchTouchEvent()方法中咱们不再用为订阅者的优先级操心了,由于在这方面,触摸事件和其余事件的处理是一致的。这里须要特殊处理的是触摸事件要根据不一样的触摸状态调用订阅者的不一样响应方法,和其余订阅者只有一个处理方法不一样,触摸事件的订阅者须要提供每一个触摸状态下的方法:

class EventListenerTouch : public EventListener
{
public:
std::function<bool(Touch*, Event*)> onTouchBegan;
std::function<void(Touch*, Event*)> onTouchMoved;
std::function<void(Touch*, Event*)> onTouchEnded;
std::function<void(Touch*, Event*)> onTouchCancelled;

std::function<void(const std::vector<Touch*>&, Event*)> onTouchesBegan;
std::function<void(const std::vector<Touch*>&, Event*)> onTouchesMoved;
std::function<void(const std::vector<Touch*>&, Event*)> onTouchesEnded;
std::function<void(const std::vector<Touch*>&, Event*)> onTouchesCancelled;

void setSwallowTouches(bool needSwallow);

private:

bool _needSwallow;

    Touch::DispatchMode _dispatchMode;

};
首先经过Touch::DispatchMode将订阅者分为单点和多点触摸的订阅者,而对于单点的状况,还能够经过设置setSwallowTouches来决定是否须要禁止后续的订阅者继续处理某个触摸点。EventListener与Node的关联只是致使了事件的分发与绘制的顺序相反,而对于触摸事件来讲,通常状况下它可能只须要被处理一次,这个时候EventDispatcher就要根据_needSwallow属性来决定是否须要继续分发。
根据这些触摸事件处理的一些需求,dispatchTouchEvent()方法的逻辑就比较清晰了:
首先,找到全部单点触摸的订阅者,而后分别用每个触摸点分别询问onTouchBegan是否须要处理,若是须要则将该触摸点保存到该订阅者中以供后续的move,end,cencelled等方法处理。同时,若是该订阅者的_needSwallow设置为true,则该触摸点将再也不被任何订阅者处理。
其次,对上述执行后剩下的全部触摸点,找到全部多点触摸的订阅者,分别调用各个多点触摸的方法。
值得注意的是,若是同时有大于1个的触摸点,则单点触摸的订阅者将会执行屡次,因此若是玩家同时将两个手指点击在一个按钮上,则按钮将被触发两次点击事件,EventDispatcher并不保证单点事件的订阅者只被点击一次,程序逻辑须要实现状态记录,咱们能够参看后面的Menu分析对此的处理,它用Menu::State来记录按钮当前状态。
最后,咱们来分析两个引擎中的使用触摸的例子,让读者了解触摸的经常使用处理方法。
5.5.1.1 Layer对系统事件的支持
Layer常常被用来根据UI的层级组织元素,正如它的名字同样。实际上它的主要目的是方便咱们使用系统事件,触摸,按键,重力加速等事件,它经过提供构造这些事件的订阅者,并向EventDispatcher注册和注销这些订阅者来简化咱们使用系统事件。此外,在3.0中它还新加入了对物理引擎集成的支持,在后面的章节咱们会学习。
Layer对全部事件的均采起与自身相关联的方式向EventDispatcher注册订阅者,便是说被处理的优先级取决于自身的UI层级。要使用某个系统事件基本上只须要调用setXXXEnabled()方法,而后重写相关事件的处理方法便可。固然默认全部的系统事件均没有开启,而且对于触摸事件默认设置为多点触摸。
一下咱们以使用触摸事件为例,在Layer中使用触摸事件:

bool HelloWorld::init()
{
if ( !CCLayer::init()){
return false;
}

setTouchMode(Touch::DispatchMode::ONE_BY_ONE);
setTouchEnabled(true);

return true;

}

class HelloWorld : public cocos2d::Layer
{
public:
virtual bool onTouchBegan(Touch *touch, Event *event);
virtual void onTouchMoved(Touch *touch, Event *event);
virtual void onTouchEnded(Touch *touch, Event *event);

    virtual void onTouchCancelled(Touch *touch, Event *event);
}
上述示例中,咱们首先在Layer的init初始化后调用setTouchEnabled()方法声明须要处理单点触摸事件,而后重写了单点触摸须要实现的4个处理方法。
实际上,在开启每一个系统事件处理以后,Layer向EventDispatcher注册订阅者,并将处理方法声明为虚方法以供子类重写。并在关闭事件,或者元素被移除的时候向EventDispatcher注销订阅者。这样大大简化了咱们使用系统事件。
5.5.1.2 Menu中的触摸处理
触摸更常见的是被用在一些控件中,好比按钮点击,表格拖拽等。其开发中最重要的部分是点击断定,此外对于按钮还须要注意前面提到的状态断定,以免屡次点击。Cocos2d-x为咱们提供了一个经常使用的GUI控件-Menu,用于显式一组按钮。经过分析Menu咱们就能够彻底掌握触摸相关的处理了。
Menu继承自LayerRGB,全部能够很方便的使用触摸事件。经过查看Menu的源代码能够知道Menu注册了单点触摸事件。固然Menu还实现了多个MenuItem的管理,咱们这里关心的是怎么使用触摸点的位置信息。

bool Menu::onTouchBegan(Touch* touch, Event* event)
{
if (_state != Menu::State::WAITING || ! _visible || !_enabled){
return false;
}

for (Node *c = this->_parent; c != NULL; c = c->getParent()){
if (c->isVisible() == false){
return false;
}
}

_selectedItem = this->itemForTouch(touch);
if (_selectedItem){
_state = Menu::State::TRACKING_TOUCH;
_selectedItem->selected();

return true;
}

return false;

}
从这个示例咱们能够看到三点有趣的信息:
首先,Menu经过一个Menu::State变量,来防止同时屡次点击,若是Menu开始处理一个触摸点,则会将_state设置为Menu::State::TRACKING_TOUCH。
其次,它在UI树上向上查找直到根节点,检查节点是否正在被绘制。这里是由于虽然能够经过设置visible来设置节点的可见性,但其子结点并不能直接知道本身是否被隐藏或者显式了,须要向上遍历至根节点才能作出判断。同时,EventDispatcher虽然能够根据节点UI层级来决定分发顺序,但它并不负责检查节点的可见性,由于这里元素仅用来计算分发顺序,并且并非全部的事件都依据元素的层级来计算优先级。因此,这里在开发中常常会遇到的一个问题就是,某个节点经过父级被隐藏了,可是其仍然可以收到触摸事件。
最后,Menu经过itemForTouch()方法来作点击断定:

MenuItem* Menu::itemForTouch(Touch *touch)
{
Point touchLocation = touch->getLocation();

if (_children && _children->count() > 0)
{
Object* pObject = NULL;
CCARRAY_FOREACH_REVERSE(_children, pObject)
{
MenuItem* child = dynamic_cast<MenuItem*>(pObject);
if (child && child->isVisible() && child->isEnabled())
{
Point local = child->convertToNodeSpace(touchLocation);
Rect r = child->rect();
r.origin = Point::ZERO;

if (r.containsPoint(local)){
return child;
}
}
}
}

return NULL;

}
这里则告诉咱们作点击断定的通常方法,首先咱们经过getLocation()方法取出触摸点在OpenGL坐标系中的世界坐标,而后将其转化到节点的本地坐标系,最后根据节点的尺寸检测其是否落于该区域内。至此,咱们就了解了关于触摸的全部知识。
5.5.2 EventKeyboard
键盘输入事件比较简单,它捕捉一个按键动做,它的参数包括按下的键_keyCode,以及表示按键的两个状态_isPressed:

class EventKeyboard : public Event
{
EventKeyboard(KeyCode keyCode, bool isPressed)
: Event(EVENT_TYPE)
, _keyCode(keyCode)
, _isPressed(isPressed)
{};

private:
KeyCode _keyCode;
bool _isPressed;

friend class EventListenerKeyboard;

};

class EventListenerKeyboard : public EventListener
{
public:
std::function<void(EventKeyboard::KeyCode, Event* event)> onKeyPressed;
std::function<void(EventKeyboard::KeyCode, Event* event)> onKeyReleased;

};
有趣的是这里对两个处理方法的调用方式,通常事件只有一个处理方法,咱们还记得EventListener定义的_onEvent变量,它被EventDispatcher直接调用,而这里EventListenerKeyboard作了特殊处理:

bool EventListenerKeyboard::init()
{
auto listener = [this](Event* event){
auto keyboardEvent = static_cast<EventKeyboard*>(event);
if (keyboardEvent->_isPressed){
if (onKeyPressed != nullptr)
onKeyPressed(keyboardEvent->_keyCode, event);
}
else {
if (onKeyReleased != nullptr)
onKeyReleased(keyboardEvent->_keyCode, event);
}
};

if (EventListener::init(EventKeyboard::EVENT_TYPE, listener)){
return true;
}

return false;

}
咱们看到,EventListenerKeyboard从新包装了listener,因而可知,咱们程序中定义的订阅者实例并不必定是最终EventDispatcher中引用的实例,而这里更有趣的是订阅者中包含了订阅者。这就是事件分发使用方法指针,而不是继承实现某个Delegate的好处。
相关文章
相关标签/搜索