作者:慧科集团华东校区-朱家聪老师,转载请注明出处及本链接。
用户事件
在Cocos2d-x中,用户事件可以分为:
- 触摸事件
- 鼠标事件
- 键盘输入事件
- 传感器事件(加速计事件)
Cocos2d-x引擎具有跨平台的特点,所以在不同的平台中对于这些事件的支持都不同:
iOS:触摸事件,加速计事件
Android & Windows Phone: 触摸事件,加速计事件,键盘事件
PC & macOS:鼠标事件,键盘事件
事件处理机制
在Cocos2d-x的一个完整的事件触发和响应机制中,有三个需要我们理解的重要角色,事件、事件源和事件处理者。事件指的是用户事件本身;事件源是事件发生的场所,通常是各个图层或者精灵对象;事件处理者指的是接受事件并对其进行处理的操作或行为。
事件
事件类是Event,它的子类有:
- EventMouse 鼠标事件
- EventTouch 触摸事件
- EventAcceleration 加速器事件
- EventKeyboard 键盘事件
- EventCustom 自定义事件
而EventCustom下还有一个子类PhysicsContact(物理引擎中的接触事件)。
事件源
事件源指的是在Cocos2d-x中的精灵、层、菜单、节点等能够触发事件的对象。
事件处理者
在Cocos2d-x中,事件处理者指的是事件监听类EventListener。其下有子类:
- EventListenerMouse 鼠标事件监听器
- EventListenerFocus 焦点事件监听器
- EventListenerTouchAllAtOnce 多点触摸事件监听器
- EventListenerTouchOneByOne 单点触摸事件监听器
- EventListenerController 游戏手柄事件监听器
- EventListenerKeyboard 键盘事件监听器
- EventListenerAcceleration 加速计事件监听器
- EventListenerCustom 自定义事件监听器
而在自定义事件监听器中还有一个子类,物理引擎接触事件监听器EventListenerPhysicsContact。
事件分发器
在事件的触发、监听、处理过程中,每一种监听器和对应的事件类型都是一一对应的。在实际运行过程中,例如一个键盘事件,需要和相应的键盘事件监听器建立连结关系,这一操作被称为“注册监听器”。此处就需要使用到事件分发器(EventDispatcher)来负责管理这种关系,事件分发器负责注册监听器,注销监听器和事件分发。
//获取单例的事件分发器对象
EventDispatcher *dispatcher = Director::getInstance()->getEventDispatcher();
//注册事件监听器
//使用优先级来注册事件监听器,不同的数字参数代表不同的优先级,数字越小优先级越高
dispatcher->addEventListenerWithFixedPriority(cocos2d::EventListener *listener, int fixedPriority);
//使用精灵的显示优先级来作为事件监听器的优先级,并注册事件监听器
//精灵的显示优先级指的是精灵在屏幕中显示的上下层次,优先级越高的精灵显示在越上层
dispatcher->addEventListenerWithSceneGraphPriority(cocos2d::EventListener *listener, cocos2d::Node *node);
//注销事件监听器
//注销指定的事件监听器
dispatcher->removeEventListener(cocos2d::EventListener *listener);
//注销自定义的事件监听器
dispatcher->removeCustomEventListeners(const std::string &customEventName);
//注销全部的事件监听器
dispatcher->removeAllEventListeners();
触摸事件
触摸事件是在移动端中最常见的一种事件,触摸事件所对应的监听器有EventListenerTouchAllOnce和EventListenerTouchOneByOne。一个触摸事件发生的过程可以从时间和空间两个方面来讨论。
从时间上来看
一个完整触摸事件可以理解为是从手指接触屏幕的时候开始,然后手指在屏幕中移动直到手指从屏幕上松开。所以触摸事件有onBegan、onMoved、onEnded三个阶段。以及当出现意外情况(例如电话呼入,手机闹钟等)时造成的触摸事件被强制取消onCancelled。单击触摸事件一共有以上四种状态,可以通过回调函数的形式对这些状态进行分别的监听。
//获取单例的事件分发器对象
EventDispatcher *dispatcher = Director::getInstance()->getEventDispatcher();
//创建多点触摸监听器
auto *listener = EventListenerTouchAllAtOnce::create();
//设置监听器的触发事件
listener->onTouchesBegan = CC_CALLBACK_2(HelloWorld::touchTouchesBeganCallback, this);
listener->onTouchesMoved = CC_CALLBACK_2(HelloWorld::touchTouchesMovedCallback, this);
listener->onTouchesEnded = CC_CALLBACK_2(HelloWorld::touchTouchesEndedCallback, this);
listener->onTouchesCancelled = CC_CALLBACK_2(HelloWorld::touchTouchesCancelledCallback, this);
//注册监听器到事件分发器中
dispatcher->addEventListenerWithFixedPriority(listener, 1);
//触摸事件响应函数
//两个参数分别为触摸点对象和事件对象
//因为在多点触摸中可能存在多点触控所以是以数组的形式传入的touches
void HelloWorld::touchTouchesBeganCallback(const std::vector&touches, cocos2d::Event *event){
log("触摸开始");
}
void HelloWorld::touchTouchesMovedCallback(const std::vector &touches, cocos2d::Event *event){
log("手指移动");
}
void HelloWorld::touchTouchesEndedCallback(const std::vector &touches, cocos2d::Event *event){
log("触摸结束");
}
void HelloWorld::touchTouchesCancelledCallback(const std::vector &touches, cocos2d::Event *event){
log("触摸取消");
}
从空间上看
从空间上来分析,一个触摸事件最主要的就是触摸点在屏幕中的位置。在一次触摸的响应函数中,我们可以从touches中获取当前的触摸点和其坐标位置。
void HelloWorld::touchTouchesBeganCallback(const std::vector&touches, cocos2d::Event *event){
//在Cocos2d-x 3.15版本中,对于单指触摸和多指触摸的响应函数进行了统一,所以单指触摸事件的响应函数中的参数也是一个触摸对象的数组,并且这些处理函数的返回值都设定成了void
//对数组进行遍历来获取单个触摸对象
for (auto &item : touches){
//获取一个触摸对象
auto touch = item;
//在OpenGL坐标系中的坐标点
auto location = touch->getLocation();
log("触摸位置:x=%f,y=%f", location.x, location.y);
//在UI坐标系中的位置
auto locationInView = touch->getLocationInView();
log("触摸在UI坐标系中的位置:x=%f,y=%f", locationInView.x, locationInView.y);
//前一次触摸事件的坐标点,用于计算手指移动方向
auto prevLocation = touch->getPreviousLocation();
auto prevLocationInView = touch->getPreviousLocationInView();
}
log("触摸开始");
}
对于OpenGL坐标系和UI坐标系的区别可以用下图来进行理解,蓝色区域表示当前游戏场景,绿色区域表示屏幕实际显示的范围。OpenGL坐标系是以当前场景的左下角作为原点来计算的(红色标记),而UI坐标系是以当前场景的左上角来作为坐标系原点的(蓝色标记)。虽然两种坐标系获取的数值不同,但是我们可以根据开发中的实际需求来获取我们所需要的一个数据。从图中也可以得出屏幕的左下角点坐标并不是(0,0)。可以通过Vec2 origin = Director::getInstance()->getVisibleOrigin();
来获取屏幕原点坐标。
案例分析:单点触摸
游戏场景搭建
首先构建一个游戏场景。在场景素材中,背景的图片素材是一个128 X 128的png图片,作为背景使用时需要对图片进行平铺处理,所以要设置背景的纹理显示参数为重复。这里用到了OpenGL中对纹理处理的一个纹理参数Texture2D::TexParams
。
TexParams是纹理参数,可以通过它实现纹理变换,前两个参数是纹理过滤规则,后两个参数是纹理环绕模式。
常见的几种:
* 过滤规则
* GL_LINEAR:使用邻近像素点来插值补点
* GL_NEAREST:最邻近点过滤
* 环绕模式
* GL_REPEAT:重复纹理
* GL_CLAMP:边缘像素复制
注意纹理的长宽必须是2的n次方,否则会报错
在我的示例代码中,GL_CLAMP不能正常使用,提示错误为:未定义的标识符“GL_CLAMP”
//背景层
Sprite *bg = Sprite::create("BackgroundTile.png", Rect(0, 0, visibleSize.width, visibleSize.height));
//设置背景层的纹理显示参数
Texture2D::TexParams params = {GL_LINEAR, GL_LINEAR, GL_REPEAT, GL_REPEAT};
bg->getTexture()->setTexParameters(params);
bg->setPosition(visibleCenter);
this->addChild(bg, 0);
//精灵对象
Sprite *boxA = Sprite::create("BoxA2.png");
boxA->setPosition(visibleCenter + Vec2(-50,50));
boxA->setContentSize(Size(100,100));
this->addChild(boxA, 10, kBoxATag);
Sprite *boxB = Sprite::create("BoxB2.png");
boxB->setPosition(visibleCenter);
boxB->setContentSize(Size(100,100));
this->addChild(boxB, 20, kBoxBTag);
Sprite *boxC = Sprite::create("BoxC2.png");
boxC->setPosition(visibleCenter + Vec2(60, 60));
boxC->setContentSize(Size(100,100));
this->addChild(boxC, 30, kBoxCTag);
触摸事件监听器注册
一般来说,我们会在一个场景的onEnter中注册相关事件的监听器。而在onExit函数中移除这些监听器。
void HelloWorld::onEnter(){
Scene::onEnter();
//创建单点触摸监听器
//设置监听器的事件吞没为true,这样当一个onBegan函数中返回true时,事件就不会传递给下一个监听器
auto listener = EventListenerTouchOneByOne::create();
listener->setSwallowTouches(true);
listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::touchTouchBeganCallback, this);
listener->onTouchMoved = CC_CALLBACK_2(HelloWorld::touchTouchMovedCallback, this);
listener->onTouchEnded = CC_CALLBACK_2(HelloWorld::touchTouchEndedCallback, this);
listener->onTouchCancelled = CC_CALLBACK_2(HelloWorld::touchTouchCancelledCallback, this);
//获取事件分发器
auto dispatcher = Director::getInstance()->getEventDispatcher();
//注册事件
//使用精灵对象的优先级来设置监听器的优先级,这里使用getChildByTag()来获取场景中某个精灵对象
dispatcher->addEventListenerWithSceneGraphPriority(listener, getChildByTag(kBoxATag));
//一个监听器只能被注册一次,所以在注册第二个时需要对原本的监听器进行克隆
dispatcher->addEventListenerWithSceneGraphPriority(listener->clone(), getChildByTag(kBoxBTag));
dispatcher->addEventListenerWithSceneGraphPriority(listener->clone(), getChildByTag(kBoxCTag));
}
void HelloWorld::onExit(){
Scene::onExit();
//移除所有监听器
Director::getInstance()->getEventDispatcher()->removeAllEventListeners();
}
事件的处理
通过onBegan、onMoved和onEnded可以实现一个精灵拖拽的效果。实现思路可以简单概括为,触摸开始时判断触摸到的精灵对象,将这个精灵对象做一个放大动作表示选中。手指移动时移动这个精灵,实现一个跟随移动的效果。触摸结束时将这个精灵还原为原本大小。
bool HelloWorld::touchTouchBeganCallback(cocos2d::Touch *touch, cocos2d::Event*event){
//获取事件中所绑定的精灵对象
Sprite *target = (Sprite *)event->getCurrentTarget();
//将触摸点的坐标,转化为精灵内部坐标系中的坐标值
Vec2 locationInNode = target->convertTouchToNodeSpace(touch);
//获取精灵当前的大小和范围
Size targetSize = target->getContentSize();
Rect rect = Rect(Vec2::ZERO, targetSize);
//判断触摸点是否在当前精灵节点内
if (rect.containsPoint(locationInNode)) {
log("触摸点位置:x=%f,y=%f", locationInNode.x,locationInNode.y);
log("target tag = %i",target->getTag());
//放大动作
auto *ac = ScaleTo::create(0.1, 1.3);
//将被触摸的对象进行放大
target->runAction(ac);
//返回true 吞没这个事件防止后面的精灵再次触发
return true;
}
//不吞没事件,让后续的精灵来尝试触发
//如果在onBegan中返回了false的话,则后续的Moved和Ended都不会再触发
return false;
}
void HelloWorld::touchTouchMovedCallback(cocos2d::Touch *touch, cocos2d::Event*event){
//获取事件中所绑定的精灵对象
Sprite *target = (Sprite *)event->getCurrentTarget();
//跟随移动效果
target->setPosition(target->getPosition() + touch->getDelta());
}
void HelloWorld::touchTouchEndedCallback(cocos2d::Touch *touch, cocos2d::Event*event){
//获取事件中所绑定的精灵对象
Sprite *target = (Sprite *)event->getCurrentTarget();
//将触摸点的坐标,转化为精灵内部坐标系中的坐标值
Vec2 locationInNode = target->convertTouchToNodeSpace(touch);
//获取精灵当前的大小和范围
Size targetSize = target->getContentSize();
Rect rect = Rect(Vec2::ZERO, targetSize);
//判断触摸点是否在当前精灵节点内
if (rect.containsPoint(locationInNode)) {
log("触摸点位置:x=%f,y=%f", locationInNode.x,locationInNode.y);
log("target tag = %i",target->getTag());
//还原原本大小
auto *ac = ScaleTo::create(0.1, 1);
//将被触摸的对象进行放大
target->runAction(ac);
}
}
由这个案例可以大致的推测Cocos2d-x中触摸事件的分发逻辑如下:
- 事件分发器接收到手机屏幕传来的触摸事件后,按照优先级依次触发触摸事件的监听器。
- 优先级高的监听器(对应的精灵优先级高)先获取到这个触摸事件。
- 判断触摸点是否在监听器所对应的精灵中,如果在则吞没这个事件防止低优先级的监听器获取这个事件,如果不在精灵中则传递给下一个监听器。这一过程可以被称为事件拦截。
- 拦截到触摸事件的监听器可以对这个事件进行处理,来改变相对应的精灵对象的位置或者大小。
使用Lambda表达式优化代码
在Cocos2d-x 3.0版本后提供了对C++11标准的支持,使用新标准中的Lambda表达式能够简化以上的代码。Lambda类似于JavaScript中的匿名函数,可以通过Lambda表达式来快速简介的完成触摸事件的响应操作。以onTouchBegan为例,代码可以如下修改。
//源代码
listener->onTouchBegan = CC_CALLBACK_2(HelloWorld::touchTouchBeganCallback, this);
//修改为
listener->onTouchBegan = [](Touch* touch, Event *event){
//获取事件中所绑定的精灵对象
Sprite *target = (Sprite *)event->getCurrentTarget();
//将触摸点的坐标,转化为精灵内部坐标系中的坐标值
Vec2 locationInNode = target->convertTouchToNodeSpace(touch);
//获取精灵当前的大小和范围
Size targetSize = target->getContentSize();
Rect rect = Rect(Vec2::ZERO, targetSize);
//判断触摸点是否在当前精灵节点内
if (rect.containsPoint(locationInNode)) {
log("触摸点位置:x=%f,y=%f", locationInNode.x,locationInNode.y);
log("target tag = %i",target->getTag());
//放大动作
auto *ac = ScaleTo::create(0.1, 1.3);
//将被触摸的对象进行放大
target->runAction(ac);
//返回true 吞没这个事件防止后面的精灵再次触发
return true;
}
//不吞没事件,让后续的精灵来尝试触发
//如果在onBegan中返回了false的话,则后续的Moved和Ended都不会再触发
return false;
};
在使用Lambda表达式时,需要注意的是匿名函数的参数列表和函数的返回值必须与要求的一致,否则会出现编译错误的现象。巧妙的使用Lambda表达式能够减少当前类中回调函数的使用次数,缩减代码量。
键盘事件
相对于触摸事件来说,键盘事件明显要简单很多。因为键盘事件没有空间方面的信息,我们只需要关注键盘中按键的按下弹起状态即可。键盘事件监听器不仅可以监听键盘的事件,还能够响应设备的菜单。使用Lambda可以很简单的来监听键盘事件。
//键盘事件监听器
auto keyboardListener = EventListenerKeyboard::create();
//键盘按键按下事件
keyboardListener->onKeyPressed = [](EventKeyboard::KeyCode code, Event *event){
log("%d 按钮被按下", code);
};
//按键弹起事件
keyboardListener->onKeyReleased = [](EventKeyboard::KeyCode code, Event *event){
log("%d 按钮被弹起", code);
};
//注册键盘事件监听器
dispatcher->addEventListenerWithSceneGraphPriority(keyboardListener, this);
鼠标事件
鼠标事件在Cocos2d-x 3.0版本之后添加的,常用在PC平台或者Mac平台中。通过鼠标事件监听器可以监听鼠标的点击、移动、滚轮等操作。
auto mouseListener = EventListenerMouse::create();
mouseListener->onMouseDown = [](Event *event){
log("鼠标被按下");
};
mouseListener->onMouseMove = [](Event *event){
log("鼠标正在移动");
};
mouseListener->onMouseUp = [](Event *event){
log("鼠标松开");
};
mouseListener->onMouseScroll = [](Event *event){
log("鼠标滚轮滑动");
};
//注册事件监听器
dispatcher->addEventListenerWithSceneGraphPriority(mouseListener, this);
层内的事件监控
除了使用事件监听器进行手动的事件监听之外,还可以在层内部进行事件的监听。首先创建一个子类化的层。
class MyLayer : public cocos2d::Layer
{
public:
virtual bool init();
// implement the "static create()" method manually
CREATE_FUNC(MyLayer);
//One Touch Event In Layer
virtual bool onTouchBegan(Touch *touch, Event *event);
virtual void onTouchCancelled(Touch *touch, Event *event);
virtual void onTouchMoved(Touch *touch, Event *event);
virtual void onTouchEnded(Touch *touch, Event *event);
/*
//Multiple Touch Event In Layer
virtual void onTouchesBegan(const std::vector &touches, Event *event);
virtual void onTouchesCancelled(const std::vector &touches, Event *event);
virtual void onTouchesMoved(const std::vector &touches, Event *event);
virtual void onTouchesEnded(const std::vector &touches, Event *event);
*/
};
然后在cpp文件中实现相关的事件监听。
#include "MyLayer.h"
USING_NS_CC;
bool MyLayer::init()
{
if ( !Layer::init() )
{
return false;
}
//开启触摸事件
setTouchEnabled(true);
//设置触摸类型为单指触摸
setTouchMode(Touch::DispatchMode::ONE_BY_ONE);
//多指触摸
/*setTouchMode(Touch::DispatchMode::ALL_AT_ONCE);*/
return true;
}
#pragma mark - Touch Event In Layer
bool MyLayer::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *event){
log("touch began");
return true;
}
void MyLayer::onTouchCancelled(Touch *touch, Event *event){
log("Cancelled");
}
void MyLayer::onTouchMoved(Touch *touch, Event *event){
log("moved");
}
void MyLayer::onTouchEnded(Touch *touch, Event *event){
log("ended");
}
/*
#pragma mark - Multiple Touch Event In Layer
void MyLayer::onTouchesBegan(const std::vector &touches, Event *event){
log("Multiple touch began");
}
void MyLayer::onTouchesCancelled(const std::vector &touches, Event *event){
log("Multiple touch Cancelled");
}
void MyLayer::onTouchesMoved(const std::vector &touches, Event *event){
log("Multiple touch moved");
}
void MyLayer::onTouchesEnded(const std::vector &touches, Event *event){
log("Multiple touch ended");
}
*/
加速度计事件
在Cocos2d-x中提供了加速度计事件的相关监听器,通过对手机的加速度计设备的事件处理,我们能够实现更多种类的游戏操控模式。不论是在iPhone还是Android设备中,都是通过一个三轴加速度计来对手机的加速度状态来进行测量的。三轴指的是在三个方向上对加速度分别进行监视,分别是水平从左至右的X轴,竖直从下至上的Y轴以及垂直于手机屏幕,相向用户方向的Z轴。和触摸事件一样,在Cocos2d-x中,加速度计事件也可以由事件分发器和层内加速度计事件来访问加速度计的数据。
使用事件分发器
对于加速度计事件监听器来说,使用方法和之前所使用的触摸事件监听器有一些略微的不同。首先在使用之前需要开启当前设备的加速度计,然后才能监听加速度计事件。
//启用设备的加速度计
Device::setAccelerometerEnabled(true);
//创建加速度计事件监听器,并且添加事件处理匿名函数
auto listener = EventListenerAcceleration::create([](Acceleration *acc, Event *event){
//获取加速度计数据
log("onAcceleration:\n\tx = %f\n\ty = %f\n\tz = %f", acc->x, acc->y, acc->z);
});
//事件分发器
EventDispatcher *dispatcher = Director::getInstance()->getEventDispatcher();
dispatcher->addEventListenerWithSceneGraphPriority(listener, this);
使用层内事件
类似于触摸事件,我们也可以在一个层中对加速度计事件进行监听和处理,所使用的方式和触摸事件类似。
class AccelerationLayer : public cocos2d::Layer
{
public:
virtual bool init();
virtual void onAcceleration(cocos2d::Acceleration *acc, cocos2d::Event *event);
// implement the "static create()" method manually
CREATE_FUNC(AccelerationLayer);
};
bool AccelerationLayer::init()
{
if ( !Layer::init() )
{
return false;
}
Device::setAccelerometerEnabled(true);
return true;
}
void AccelerationLayer::onAcceleration(cocos2d::Acceleration *acc, cocos2d::Event *event){
log("onAcceleration:\n\tx = %f\n\ty = %f\n\tz = %f", acc->x, acc->y, acc->z);
}