Cocos2d-x游戏开发实战精解
上QQ阅读APP看书,第一时间看更新

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体中引用,除非其位于捕获列表中。而将用到的外部对象名加入到方括号中就会使其位于捕获列表中,从而使之合法。