cocos2d-x 详解之 CCLayer(触摸事件)
CCLayer继承自CCNode,在CCLayer中可以实现单点触摸、多点触摸和重力感应回调3种不同形式的交互。这部分的难点在于,当存在多个层都要去接收触摸时它的响应机制是如何处理的。了解内部的处理机制使用起来才会得心应手,说简单也难,说难也简单,让我们一点点剖析。
从触摸方式上看,触摸分两种类型:单点触摸 和多点触摸。
从触摸事件的响应机制上看,触摸事件又分为:标准触摸事件 和目标触摸事件。
引擎当中处理用户操作的机制是一种由事件驱动的处理机制。所谓的事件驱动,就是指只有当事件发生了,才会执行对应的机制。在引擎当中,用户操作将会经过”接受“、”分发“、”处理“三个步骤,我们先说接收操作。
(一)接收操作
实际上,无论运行在什么设备平台,运行平台所接受的用户操作数据都会保存为统一的数据格式,然后转化为引擎需要的用户操作信息,因此引擎专门设计了一个用来存储用户操作数据的类CCTouch,这个类中保存了用户的操作信息(因此我们可以简单的理解为CCTouch就是信息):
CCTouch();//构造函数 CCPoint locationInView();//用户触摸的视图坐标 CCPoint previousLocationInView();//前一次用户触摸的视图坐标 void setTouchInfo(int id,float x,float y);//设置用户操作数据(id标识,x坐标,y坐标) int getId() const;//返回id标识
至于平台如何接收用户操作数据的,请参考CCEGLViewProtoco.cpp文件中的handleTouchesBegin()函数。
(二)分发机制
分发机制解释了引擎获得了用户的操作数据后是如何将信息传递到能接收响应的层或其他节点。
引擎专门提供了一个负责分发触摸消息的类:分发器--CCTouchDispatcher,它会向所有需要响应的对象发送用户操作信息,那么问题就来了,分发器是如何知道该把信息发给谁呢,即都有谁是需要响应的的对象呢。于是引擎又专门设计了一个触摸代理类CCTouchDelete,凡是实现了触摸代理CCTouchDelete的类都是可响应的对象。CCTouchDelete有三个子类,我们自定义的节点如果要能够接收用户操作通常都要继承这三个子类之一,实现了代理以后,最关键的是还要在分发器中注册,这样分发器才能找到它。
CCStandardTouchDelegate用于处理多点触摸;CCTargetedTouchDelegate用于处理单点触摸。
这里面提到了标准触摸代理和目标触摸代理,对应的就是标准触摸事件和目标触摸事件,下面总结一下:
首先,区分一下“标准触摸事件”和“目标触摸事件”
1)标准触摸事件(通常对应“多点触摸”):即是默认的接收触摸模式。比如CCLayer,它实现了CCTouchDelegate代理,并在onEnter()中注册了标准触摸事件。通常我们自定义一个层继承自CCLayer并且开启触摸(setTouchEnable(true))的话就默认为标准触摸事件,不再需要手动实现代理和注册,用起来比较简单。它的特点是:如果场景中存在多个层,那么他们都会响应触摸,就会出现触摸事件在多个层之间的传递。有时候我们并不希望点击最上层的时候下面的层也跟着响应,这就需要目标触摸事件。
2)目标触摸事件(通常对应“单点触摸”):触摸事件的接收者并不平等,具有优先级,并且有权停止事件的分发,使它不再继续传递给其他接收者而独自吞下了这个触摸事件,这里的接受者就是指定的响应目标。目标代理对象是用来响应单点触摸的,常用在比较小的对象之上,比如一个精灵或者一个按钮。玩家是很难再一个按钮上进行多点触碰操作的,而与之对应的标准代理对象则是用在触碰面积较大的层对象之上。
通常,游戏的菜单按钮、摇杆按钮等元件常使用目标触摸事件,以保证触摸事件不对其他层产生不良影响。要实现目标触摸,则要实现目标触摸代理CCTargetedTouchDelegate,并手动注册触摸事件:
//第二个参数是代理的优先级,如果数值越小优先级越高 //第三个参数是true表示当前图层处理消息后不再进行消息的后续传递,截断触摸消息 CCDirector::sharedDirector()->getTouchDispatcher()->addTargetedDelegate(this,0, true);
(三)处理响应
再来区分一下单点触摸和多点触摸:
多点触碰是可以包含单电触碰的,是一个从多到一的过程。显而易见,多点触摸的处理中必然包含了单点触摸,但是单点触摸却无法实现如捏合、分离的用户操作。
1) 单点触摸
要使用单点触摸,必须要重写以下几个接口:
virtual void onEnter();//必须,并且通常在这里注册触摸事件 virtual void onExit();//必须,并且通常在这里移除触摸代理事件 virtual bool ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent);//在控件上单击时响应,此函数必须要写 virtual void ccTouchMoved(CCTouch *pTouch, CCEvent *pEvent);//在控件上滑动时响应,可选,但一般情况下需要 virtual void ccTouchEnded(CCTouch *pTouch, CCEvent *pEvent);//在控件上抬起触摸时响应,可选,但一般情况下需要 virtual void ccTouchCancelled(CCTouch *pTouch, CCEvent *pEvent);//在控件上按住滑动到控件外抬起触摸时响应,可选,但一般情况下不需要
注意:ccTouchBegan();该接口,细心的人可能会发现,就它是返回bool的。它的返回值决定着,后续的ccTouchMoved();ccTouchEnded();ccTouchCancelled();是否触发。只有返回true时才触发。使用时,如果要监听目标触摸事件,别忘了注册:
//当自定义层进入场景时调用 void MyLayer::onEnter() { CCLayer::onEnter(); //最好写上 CCDirector* pDirector = CCDirector::sharedDirector(); pDirector->getTouchDispatcher()->addTargetedDelegate(this, 0, true); //注册目标触摸事件 } //当自定义层退出场景时调用 void MyLayer::onExit() { CCLayer::onExit(); //最好写上 CCDirector* pDirector = CCDirector::sharedDirector(); pDirector->getTouchDispatcher()->removeDelegate(this); } //因为委托代理一般都是成对出现的,有注册就要对应有移除,若不移除,则有可能造成程序的异常退出。一般情况下,推荐大家将触屏事件的注册于移除代码都写在onEnter于onExit中。
2)多点触摸
CCLayer默认已经帮我们实现了。要使用多点触摸则只需要实现重写如下几个接口即可
//同单点触摸类似,但是不需要onEnter,但需要写onExit virtual void registerWithTouchDispatcher(void);//多触点的委托注册放在onEnter的生命函数中会造成程序异常退出,默认都重写此函数,将多触点委托注册放在此函数中进行注册 virtual void ccTouchesBegan(CCSet *pTouches, CCEvent *pEvent); virtual void ccTouchesMoved(CCSet *pTouches, CCEvent *pEvent); virtual void ccTouchesEnded(CCSet *pTouches, CCEvent *pEvent); virtual void onExit();//在这里移除注册
注意:CCSet*对象,是触摸点的集合。它存储着所有的触摸信息。即:遍历它,可以处理所有的触摸点响应。其他的用法同单点触摸一样。
接下来就是接收触摸后的处理:
常用触摸点坐标转换方式:
CCPoint point=pTouch->getLocation(); //通过pTouch得到用户的触摸点,getLocation函数会将触摸坐标转换为GL坐标,因此拿到的坐标可以直接使用 CCPoint ptNode = convertTouchToNodeSpace(pTouch);//将视图坐标转化为节点坐标(节点坐标等同于绘图坐标,即GL坐标),原点在本节点的左下角 CCPoint ptNode = convertTouchToNodeSpaceAR(pTouch);//转换为以锚点为原点的坐标系中的坐标
多点触摸使用实例:
//注册多触点的委托监听 void HelloWorld::registerWithTouchDispatcher(void){ CCDirector::sharedDirector()->getTouchDispatcher()->addStandardDelegate(this, 0); } //用户手指第一次触碰 void HelloWorld::ccTouchesBegan(CCSet *pTouches, CCEvent *pEvent){ CCSetIterator iter = pTouches->begin();//CCSetIterator:迭代器,用于遍历CCSet集合对象 for (; iter != pTouches->end(); iter++) { CCTouch* pTouch = (CCTouch*)(*iter); CCPoint location = pTouch->getLocation(); if(pTouch->getID()==0){//第一个触点 CCSprite * sp1 = (CCSprite*)this->getChildByTag(91); sp1->setPosition(location); }else if(pTouch->getID()==1){//第二个触点 CCSprite * sp2= (CCSprite*)this->getChildByTag(92); sp2->setPosition(location); } } } //用户手指进行移动或者拖拽 void HelloWorld::ccTouchesMoved(CCSet *pTouches, CCEvent *pEvent){ CCSetIterator iter = pTouches->begin(); for (; iter != pTouches->end(); iter++) { CCTouch* pTouch = (CCTouch*)(*iter); CCPoint location = pTouch->getLocation(); if(pTouch->getID()==0){//第一个触点 CCSprite * sp1 = (CCSprite*)this->getChildByTag(91); sp1->setPosition(location); }else if(pTouch->getID()==1){//第二个触点 CCSprite * sp2= (CCSprite*)this->getChildByTag(92); sp2->setPosition(location); } } } //用户手指抬起 void HelloWorld::ccTouchesEnded(CCSet *pTouches, CCEvent *pEvent){ CCSetIterator iter = pTouches->begin(); for (; iter != pTouches->end(); iter++) { CCTouch* pTouch = (CCTouch*)(*iter); CCPoint location = pTouch->getLocation(); CCLOG("pTouch 触摸点 %i 的坐标: x:%f,y:%f",pTouch->getID(),location.x,location.y); } } //删除多触点的委托监听 void HelloWorld::onExit(){ CCDirector::sharedDirector()->getTouchDispatcher()->removeDelegate(this); CCLayer::onExit(); }