4.1 单点触摸的纸牌游戏
任何一款游戏都少不了对用户操作的处理,比如获取用户点击屏幕的坐标位置,或者确认是否用户单击了游戏中的某个NPC角色,因此,在游戏开发中对用户输入的处理是必不可少的。
在移动游戏中,由于硬件的限制(目前大多数手机都配置了较大的触摸屏且没有太多的按键),游戏与用户的交互主要通过对屏幕的单击来实现。范例4-1就是在Cocos2d-x中实现游戏与用户交互的一个例子。
【范例4-1】用单点触摸实现简单的交互。
01 Size visibleSize = Director::getInstance()->getVisibleSize(); 02 Vec2 origin = Director::getInstance()->getVisibleOrigin(); 03 //加入背景 04 auto* background = Sprite::create("background.png"); 05 background->setPosition(visibleSize.width/2,visibleSize.height/2); 06 this->addChild(background); 07 //加入卡牌精灵 08 for (int i = 0; i < 5; i++) 09 { 10 char imageName[15] = { 0 }; 11 sprintf(imageName, "mycard0%d.png", i); 12 myCard[i] = Sprite::create(imageName); 13 myCard[i]->setScale(0.26f); 14 myCard[i]->setPosition(visibleSize.width*(i+1)/6, visibleSize. height/2); 15 this->addChild(myCard[i]); 16 } 17 //建立事件监听器 18 auto myListener = EventListenerTouchOneByOne::create(); 19 myListener->setSwallowTouches(true); 20 myListener->onTouchBegan = [=](Touch* touch, Event* event) 21 { 22 //获取事件所绑定的target 23 auto target = static_cast<Sprite*>(event->getCurrentTarget()); 24 //获取当前单击点所在相对按钮的位置坐标 25 Point locationInNode = target->convertToNodeSpace(touch-> getLocation()); 26 Size s = target->getContentSize(); 27 Rect rect = Rect(0, 0, s.width, s.height); 28 //单击范围判断检测 29 if (rect.containsPoint(locationInNode)) 30 { 31 //显示当前卡牌精灵的坐标位置 32 log("sprite began... x = %f, y = %f", locationInNode.x, locationInNode.y); 33 target->setOpacity(180); 34 return true; 35 } 36 return false; 37 }; 38 myListener->onTouchMoved = [](Touch* touch, Event* event) 39 { 40 auto target = static_cast<Sprite*>(event->getCurrentTarget()); 41 //显示当前卡牌精灵的坐标位置 42 log("sprite move... x = %f, y = %f", touch->getLocation().x, touch->getLocation().y); 43 //移动当前卡牌精灵的坐标位置 44 target->setPosition(target->getPosition() + touch->getDelta()); 45 }; 46 myListener->onTouchEnded = [](Touch* touch, Event* event) 47 { 48 auto target = static_cast<Sprite*>(event->getCurrentTarget()); 49 target->setOpacity(255); 50 }; 51 //为事件监听器绑定事件 52 _eventDispatcher->addEventListenerWithSceneGraphPriority (myListener, myCard[0]); 53 //再次绑定时要使用clone方法 54 _eventDispatcher->addEventListenerWithSceneGraphPriority (myListener->clone(), myCard[1]); 55 _eventDispatcher->addEventListenerWithSceneGraphPriority (myListener->clone(), myCard[2]); 56 _eventDispatcher->addEventListenerWithSceneGraphPriority (myListener->clone(), myCard[3]); 57 _eventDispatcher->addEventListenerWithSceneGraphPriority (myListener->clone(), myCard[4]);
整体代码可查看源文件当前目录下的ChapterFour001项目,该项目运行后将在屏幕中显示5张卡牌,结果如图4-1所示。可以通过触摸拖动这5张卡牌改变卡牌所在的位置,如图4-2中就是笔者随意拖动之后得到的结果。
图4-1 项目运行后将显示5张卡牌
图4-2 拖动卡牌可以改变卡牌的位置
下面来介绍这样的功能是怎样实现的。首先是在场景中加入了背景,如范例第04~06行所示,然后再在背景层上方加入5张卡片,卡片精灵是在HelloWorldScene.h文件中已经被定义的一个精灵类的数组,如范例4-2第04行所示,然后在范例的第08~16行,将卡片加入到场景中。
【范例4-2】HelloWorldScene.h中对卡片精灵的定义。
01 class HelloWorld : public Cocos2d::Layer 02 { 03 public: 04 Cocos2d::Sprite *myCard[5]; 05 int selectedId; 06 static Cocos2d::Scene* createScene(); 07 virtual bool init(); 08 CREATE_FUNC(HelloWorld); 09 };
范例4-1第18行中创建了一个EventListenerTouchOneByOne类型的对象myListener,接下来就将通过它来接收用户对屏幕做出的操作。从这个类的名称上就不难理解它的作用,EventListener就是事件监听器的意思,而TouchOneByOne显然就是单点触摸的意思。
在2.x版本的Cocos2d中,对事件的响应是通过onTouchBegan等方法来进行的,而在3.x版本中,这些方法被封装到了事件监听器类中,在本节中使用到的EventListenerTouchOneByOne类就是一个用来对单点触摸进行处理的事件监听器类。在该类中分别封装了方法onTouchBegan、onTouchMoved、onTouchEnded和onTouchCancelled来对用户的单击行为进行处理,如判断用户所单击的元素、记录单击轨迹等。在范例4-1的第20~50行是对这些方法的使用。
提示:在Cocos2d 3.x版本中,提供的触发器类型包括了EventListenerTouch(触摸事件)、EventListenerKeyboard(键盘响应事件)、EventListenerMouse(鼠标响应事件)、EventListenerAcceleration(加速记录事件)和EventListenerCustom(自定义事件)5种,其中EventListenerTouchOneByOne是触摸事件的一个子类。
在范例4-1的第20~37行是对onTouchBegan操作的处理,即当用户触摸到屏幕时程序该进行怎样的响应。首先第23行是获取事件所绑定的target,即用户的滑动、拖曳行为是针对哪一个“物体”进行的。如在本节的范例中用户的拖曳操作一定是针对卡牌的。
在第25行获取了用户所单击的点的坐标,从这句代码中不难看出,用户所单击的坐标实际上是通过第20行的参数touch被传递的。再看第29行if语句中的判断,实际上就是通过判断touch所传递的坐标是否包含于被单击的“物体”中,来确定是否下一步的操作。这里就要提到一个问题,那就是在onTouchBegan方法中是有返回值的,类型为bool。从范例中可以看出,当用户单击在对应的“物体”范围中时,则认为单击是有效的,那么将返回true,否则返回false。当onTouchBegan的返回值为false时,将不会执行接下来的onTouchMoved操作,响应被截断。因此,只有在onTouchBegan的返回值为true时,onTouchMoved方法才能发挥作用。
第38~42行就是对onTouchMoved方法的重写,作用是当用户在“物体”上拖动时,根据用户的手势的位置变化移动“物体”。第42行的作用是输出当前卡牌的坐标。
有心的读者也许会想到一个问题,那就是当两张卡牌有重叠在一起的部分时,用户单击它们重叠的部分进行拖动会发生什么,如图4-3所示。毫无疑问,一般来说应当选择相对上层的卡牌进行操作而忽略底层的卡牌,经过实验也确实如此。这时修改第19行中的参数为false后再进行该操作,会发现底层的卡牌也跟着上层的卡牌一起被拖动了。由此可知,第19行的作用就是设置事件监听器是否允许触摸事件向下传递。
图4-3 卡牌重叠部分
至于第46~50行的onTouchEnded,相信读者已经不难猜出这是对触摸事件结束的响应了。下面再来思考一个比较有深度的问题,既然在onTouchBegan方法中要判断用户是否单击在了相应的“物体”上,比如在范例中的卡牌上,那么Cocos2d是怎样知道哪个“物体”是应该被响应的呢?
这就看第52~57行的代码了,使用addEventListenerWithSceneGraphPriority方法将事件监听器与“物体(也就是范例中的卡牌)”绑定在了一起,这样就可以知道应当对哪个“物体”的单击进行响应了。
提示:触发器在多次与不同对象进行绑定时,要对触发器对象使用clone方法来保证程序的正常运行。另外在此处代码中所使用的_eventDispatcher是Node对象中自带的属性,用于统一管理各个节点的分发情况。
这样,在Cocos2d-x中对触摸的响应就算是实现了,在卡牌游戏中可以使用这样的方法来实现对卡牌的拖曳、出牌等操作。还有一些RPG游戏中用来实现对人物行走的控制,以及在类似2048这样的游戏中判断用户的手势等。那么现在还有一个问题,就是在本节提供的例子中,要怎样判断是哪张卡牌被拖动了呢?比如在5张卡牌都被绑定了事件监听器的情况下,怎样能够保证仅有第1张卡牌能被拖动呢?很简单,只需要将第29行中if语句中的判断条件修改为rect.containsPoint(locationInNode)&&target==myCard[0]就可以了,即直接来判断target是否为目标对象。
提示:为了能够在onTouchBegan方法中引用来自外部的对象,需要将第20行的方括号中加入一个等号或者直接引入要引用的对象名,否则就会报出错误:封闭函数局部变量不能在lambda体中引用,除非其位于捕获列表中。而将用到的外部对象名加入到方括号中就会使其位于捕获列表中,从而使之合法。