cocos2d-x 事件分发机制
新事件分发机制概述
在 cocos2d-x 的 2.x 版本中,事件处理是将将要触发的事件交给代理(delegate)处理,开发者通过实现代理里面的 onTouchBegan 等方法来接收事件,最终完成事件的响应。而在新的事件分发机制里,开发者只需创建一个事件监听器,在其中实现各种触发后的逻辑,然后将其添加到事件分发器 _eventDispatcher 中,所有事件监听器由这个分发器统一管理,即可完成事件响应。
事件监听器类型
事件监听器主要有以下几种:
- 触摸事件 (EventListenerTouch)
- 键盘响应事件 (EventListenerKeyboard)
- 鼠标响应事件 (EventListenerMouse)
- 自定义事件 (EventListenerCustom)
- 加速记录事件 (EventListenerAcceleration)
_eventDispatcher 的组成部分
_eventDispatcher 的工作由三部分组成:
- 事件分发器 (EventDispatcher):负责统一管理和分发事件。
- 事件类型:如
EventTouch、EventKeyboard等,代表不同类型的事件。 - 事件监听器:如
EventListenerTouch、EventListenerKeyboard等,实现了各种触发后的逻辑。在适当的时候,事件分发器会分发事件类型,然后调用相应类型的监听器。
用户输入事件处理
触摸事件
在处理触摸事件时,开发者既可以重写 onTouchBegan、onTouchMoved 和 onTouchEnded 这三个方法,也可以直接通过 Lambda 表达式完成响应逻辑。
2.x 版本与 3.0 版本的差异
在 2.x 版本中,若要开启多点触摸,需要在 AppController.mm 中的 application didFinishLaunchingWithOptions:launchOptions 里添加 [__glView setMultipleTouchEnabled: YES],并且还需重载 5 个相应函数:
virtual void registerWithTouchDispatcher(void);
virtual void ccTouchesBegan(cocos2d::CCSet* pTouches, cocos2d::CCEvent* pEvent);
virtual void ccTouchesMoved(cocos2d::CCSet* pTouches, cocos2d::CCEvent* pEvent);
virtual void ccTouchesEnded(cocos2d::CCSet* pTouches, cocos2d::CCEvent* pEvent);
virtual void ccTouchesCancelled(cocos2d::CCSet* pTouches, cocos2d::CCEvent* pEvent);
而在 3.0 版本中,只需创建多点触摸事件监听器,并将其添加到事件分发器中即可。
示例代码
以下代码展示了在一个界面中添加三个相互遮挡且都能触发触摸事件的按钮的实现:
// 创建按钮精灵
auto sprite1 = Sprite::create("Images/CyanSquare.png");
sprite1->setPosition(origin + Point(size.width / 2, size.height / 2) + Point(-80, 80));
addChild(sprite1, 10);
// sprite2 和 sprite3 的创建代码省略
// ...
// 创建一个事件监听器类型为 OneByOne 的单点触摸
auto listener1 = EventListenerTouchOneByOne::create();
// 设置是否吞没事件,在 onTouchBegan 方法返回 true 时吞没
listener1->setSwallowTouches(true);
// 使用 lambda 实现 onTouchBegan 事件回调函数
listener1->onTouchBegan = [](Touch* touch, Event* event) {
// 获取事件所绑定的 target
auto target = static_cast<Sprite*>(event->getCurrentTarget());
// 获取当前点击点所在相对按钮的位置坐标
Point locationInNode = target->convertToNodeSpace(touch->getLocation());
Size s = target->getContentSize();
Rect rect = Rect(0, 0, s.width, s.height);
// 点击范围判断检测
if (rect.containsPoint(locationInNode)) {
log("sprite began… x = %f, y = %f", locationInNode.x, locationInNode.y);
target->setOpacity(180);
return true;
}
return false;
};
// 触摸移动时触发
listener1->onTouchMoved = [](Touch* touch, Event* event) {
// 逻辑代码省略
// ...
};
// 点击事件结束处理
listener1->onTouchEnded = [=](Touch* touch, Event* event) {
// 逻辑代码省略
// ...
};
// 添加监听器
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener1, sprite1);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener1->clone(), sprite2);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener1->clone(), sprite3);
需要注意的是,_eventDispatcher 是 Node 的属性,通过它可以管理当前节点(场景、层、精灵等)的所有事件的分发。它本身是一个单例模式值的引用,在 Node 的构造函数中,通过 Director::getInstance()->getEventDispatcher() 获取,有了这个属性,就能方便地处理事件。
另外,当再次使用 listener1 时,需要使用 clone() 方法创建一个新的克隆,因为在使用 addEventListenerWithSceneGraphPriority 或者 addEventListenerWithFixedPriority 方法时,会对当前使用的事件监听器添加一个已注册的标记,这使得它不能够被添加多次。同时,FixedPriority listener 添加完之后需要手动 remove,而 SceneGraphPriority listener 是跟 Node 绑定的,在 Node 的析构函数中会被移除。具体的示例用法可以参考引擎自带的 tests。
移除监听器
可以通过以下方法移除一个已经被添加了的监听器:
_eventDispatcher->removeEventListener(listener);
也可以使用如下方法,移除当前事件分发器中所有监听器:
_eventDispatcher->removeAllEventListeners();
当使用 removeAll 时,此节点的所有的监听将被移除,推荐使用指定删除的方式。因为 removeAll 之后菜单也不能响应,因为它也需要接受触摸事件。
键盘响应事件
键盘响应事件和处理触摸事件使用了相同的处理方式,以下代码演示了如何处理键盘响应事件:
// 初始化并绑定
auto listener = EventListenerKeyboard::create();
listener->onKeyPressed = CC_CALLBACK_2(KeyboardTest::onKeyPressed, this);
listener->onKeyReleased = CC_CALLBACK_2(KeyboardTest::onKeyReleased, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);
// 键位响应函数原型
void KeyboardTest::onKeyPressed(EventKeyboard::KeyCode keyCode, Event* event) {
log("Key with keycode %d pressed", keyCode);
}
void KeyboardTest::onKeyReleased(EventKeyboard::KeyCode keyCode, Event* event) {
log("Key with keycode %d released", keyCode);
}
鼠标响应事件
在 3.0 版本中多了鼠标捕获事件派发,这可以在不同的平台上,丰富游戏的用户体验。下面代码实现了鼠标响应事件的步骤:
// 创建监听器
_mouseListener = EventListenerMouse::create();
// 事件响应逻辑
_mouseListener->onMouseMove = [=](Event* event) {
EventMouse* e = (EventMouse*)event;
std::string str = "Mouse Move detected";
// 其他逻辑代码省略
// ...
};
_mouseListener->onMouseUp = [=](Event* event) {
// 逻辑代码省略
// ...
};
_mouseListener->onMouseDown = [=](Event* event) {
// 逻辑代码省略
// ...
};
_mouseListener->onMouseScroll = [=](Event* event) {
// 逻辑代码省略
// ...
};
// 添加到事件分发器
_eventDispatcher->addEventListenerWithSceneGraphPriority(_mouseListener, this);
自定义事件
以上介绍的是系统自带的事件类型,这些事件由系统内部自动触发,如触摸屏幕、键盘响应等。除此之外,cocos2d-x 还提供了一种自定义事件,它不是由系统自动触发,而是需要人为干涉,示例如下:
_listener = EventListenerCustom::create("game_custom_event1", [=](EventCustom* event) {
std::string str("Custom event 1 received, ");
char* buf = static_cast<char*>(event->getUserData());
str += buf;
str += " times";
statusLabel->setString(str.c_str());
});
_eventDispatcher->addEventListenerWithFixedPriority(_listener, 1);
// 触发自定义事件
static int count = 0;
++count;
char* buf = new char[10];
sprintf(buf, "%d", count);
EventCustom event("game_custom_event1");
event.setUserData(buf);
if (...) {
_eventDispatcher->dispatchEvent(&event);
}
CC_SAFE_DELETE_ARRAY(buf);
上述代码定义了一个 “自定义事件监听器”,实现了相关逻辑,并且添加到事件分发器。通过手动创建 EventCustom 对象,设置其 UserData 数据,再通过 _eventDispatcher->dispatchEvent(&event) 将此事件分发出去,从而触发之前所实现的逻辑。
加速计事件
除了触摸,移动设备上一个很重要的输入源是设备的方向,因此大多数设备都配备了加速计,用于测量设备静止或匀速运动时所受到的重力方向。重力感应来自移动设备的加速计,通常支持 X、Y 和 Z 三个方向的加速度感应,所以又称为三向加速计。在实际应用中,可以根据 3 个方向的力度大小来计算手机倾斜的角度或方向。
在 3.0 版本的新事件机制下,需要通过创建一个加速计监听器 EventListenerAcceleration,其静态 create 方法中有个 Acceleration 的参数需要注意。Acceleration 是一个类,包含了加速计获得的 3 个方向的加速度,相关代码如下:
class Acceleration {
public:
double x;
double y;
double z;
double timestamp;
Acceleration(): x(0), y(0), z(0), timestamp(0) {}
};
该类中每个方向的加速度大小都为一个重力加速度大小。
在使用加速计事件监听器之前,需要先启用此硬件设备:
Device::setAccelerometerEnabled(true);
然后创建对应的监听器,在创建回调函数时,可以使用 lambda 表达式创建匿名函数,也可以绑定已有的函数逻辑实现,示例如下:
auto listener = EventListenerAcceleration::create([=](Acceleration* acc, Event* event) {
// 逻辑代码段
// ...
});
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, this);