事件系统,是一个软件的核心组成部分。从小处讲它是应用程序内部各模块交互的设计模式,从大处讲,它是软件架构的组成模块。在现代软件开发中,操作系统通常通过一些预定义的事件,告知应用程序发生的一些事情如用户输入,内存不足等。 然而,通常我们并不直接使用系统事件,例如一个界面可能不同区域的元素对触摸事件的理解都不一样,在某些情况下需要优先处理某些逻辑,这就需要对系统事件再包装,以应对界面复杂的元素和逻辑。另一方面,我们需要一个事件系统用来在应用程序内部分发消息,例如当敌人进入攻击范围时通知英雄射击,当敌人血量低于0时播放死亡动画等等。这些都需要游戏引擎提供一个灵活的事件系统,它既能管理分发系统事件,还能借助其简单管理自定义事件。
Cocos2d-x 3.0将所有的事件统一集中到EventDispatcher中处理,它不光改进了触摸等系统事件的管理和使用方式,还使得我们可以借助其处理程序自定义的事件。本章将学习相关的内容。 5.1 事件类型 要处理一个事件,首先得定义一个事件类型。事件系统总是按类型而不是实例来处理事件的订阅和分发,这样使得同一个事件可以有多个订阅者。Event是所有事件的基类,它用一个字符串来表示该事件的类型。我们不应该直接使用Event,而应该从它继承实现自定义事件。事件类型通常不是一个变量,以保证相同类型的事件实例拥有相同的类型,但EventCustom除外,它可以在初始化的时候指定不同的类型,这是为了简化编写事件类型。以下是Event类的定义:
class Event
{
protected:
Event(const std::string& type);
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::functioncallback);
public:
virtual ~EventListener();
virtual bool checkAvaiable() = 0;
virtual EventListener* clone() = 0; protected:
std::function _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 callback);
virtual bool checkAvaiable() override;
virtual CollisionListener* clone() override;
protected:
CollisionListener();
bool init(std::function callback);
std::function _onCollisionEvent;
}; 接下来,我们需要定义订阅者,在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);
Cocos2d-x 3.0将所有的事件统一集中到EventDispatcher中处理,它不光改进了触摸等系统事件的管理和使用方式,还使得我们可以借助其处理程序自定义的事件。本章将学习相关的内容。 5.1 事件类型 要处理一个事件,首先得定义一个事件类型。事件系统总是按类型而不是实例来处理事件的订阅和分发,这样使得同一个事件可以有多个订阅者。Event是所有事件的基类,它用一个字符串来表示该事件的类型。我们不应该直接使用Event,而应该从它继承实现自定义事件。事件类型通常不是一个变量,以保证相同类型的事件实例拥有相同的类型,但EventCustom除外,它可以在初始化的时候指定不同的类型,这是为了简化编写事件类型。以下是Event类的定义:
class Event
{
protected:
Event(const std::string& type);
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::functioncallback);
public:
virtual ~EventListener();
virtual bool checkAvaiable() = 0;
virtual EventListener* clone() = 0; protected:
std::function _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 callback);
virtual bool checkAvaiable() override;
virtual CollisionListener* clone() override;
protected:
CollisionListener();
bool init(std::function callback);
std::function _onCollisionEvent;
}; 接下来,我们需要定义订阅者,在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的机制之前,我们来总结一下一般在游戏中使用事件还有哪些特殊的需求:
- 设置订阅者的优先级,一个类型的事件可能拥有多个订阅者,因此有必要设置处理顺序,例如当碰撞事件完成之后,其中一个订阅者负责处理伤害计算,而另一个订阅者可能做一些UI的操作,例如播放声音或者粒子效果。前者的优先级肯定需要更高,因为后者的处理可能需要依赖于生命值的计算。
- 修改订阅者的优先级。
- 停止事件的继续分发,使后续的订阅者不用再处理该事件。
-
根据屏幕上元素的层级,而不是手动设定的优先级来处理事件分发,这在触摸事件的分发中尤其重要。 带着这些目标,我们来分析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()方法,可以总结排序规则如下:- 分发fixedPriority小于0的订阅者,fixedPriority越小则优先分发。
- 分发所有fixedPriority值为0的订阅者,并且没有与Node相关联的。
- 分发所有与Node相关联的订阅者,其关联Node的eventPriority越高(越处于屏幕最上层)则优先级越高。
-
我们看到,EventListenerKeyboard重新包装了listener,由此可见,我们程序中定义的订阅者实例并不一定是最终EventDispatcher中引用的实例,而这里更有趣的是订阅者中包含了订阅者。这就是事件分发使用方法指针,而不是继承实现某个Delegate的好处。