怎样制作基于Cocos2d-x的SLG游戏-第2章

原创: @涵紫任珊

怎样制作基于Cocos2d-x的SLG游戏-第1章
怎样制作基于Cocos2d-x的SLG游戏-第2章
怎样制作基于Cocos2d-x的SLG游戏-第3章
怎样制作基于Cocos2d-x的SLG游戏-第4章
怎样制作基于Cocos2d-x的SLG游戏-第5章
怎样制作基于Cocos2d-x的SLG游戏-第6章
怎样制作基于Cocos2d-x的SLG游戏-第7章

上一章中,我们使用TiledMap制作了一张简单的地图,并把它加入到了程序中,紧接着本章将实现地图的双指缩放和单指移动功能。

是不是觉得这一功能很接地气,呵呵,其实在很多类似的大地图背景游戏中这是非常常见和必要的一项功能,玩家可以通过滑动屏幕实现地图的滚动预览,同时可以通过两个手指的拉伸和聚拢实现地图背景的放大缩小。下面,就跟着我们一起来实现吧。

p1

双指缩放,单指拖动的实现

Cocos2d-x中有自己的一套事件分发机制,如果你还不是很清楚,可先阅读Cocos2d-x事件分发机制一文。

在Cocos2d-x 3.x 中,实现触摸响应的一般流程如下:

  1. 重载触摸回调函数
  2. 创建并绑定触摸事件
  3. 实现触摸回调函数

具体实现如下:

1、首先,在GameScene.h文件中声明成员函数。

virtual void onTouchesBegan(const std::vector &touches, cocos2d::Event *event);
virtual void onTouchesMoved(const std::vector &touches, cocos2d::Event *event);

2、在GameScene.cpp文件的init函数中创建并绑定触摸事件。

// 1 创建一个事件监听器
auto listener = EventListenerTouchAllAtOnce::create();
// 2 绑定触摸事件
listener->onTouchesBegan = CC_CALLBACK_2(GameScene::onTouchesBegan, this);// 触摸开始时触发
listener->onTouchesMoved = CC_CALLBACK_2(GameScene::onTouchesMoved, this);// 触摸移动时触发
// 3 添加监听器
_eventDispatcher->addEventListenerWithSceneGraphPriority(listener, bgSprite);
  1. 在使用触摸事件时,我们首先需要创建一个事件监听器,事件监听器包含了触摸事件、键盘响应事件、加速记录事件、鼠标响应事件和自定义事件。其中的触摸监听类型触摸事件又分为 EventListenerTouchOneByOne(单点触摸) 和 EventListenerTouchAllAtOnce(多点触摸) 两种。
  2. 让监听器绑定事件处理函数。上面绑定的onTouchesBegan和onTouchesMoved分别响应的是触摸点击开始事件和移动事件。与之相关的还有onTouchEnded和onTouchCancelled两个事件处理函数,但目前我们的游戏还不需要(也有可能不会用到),所以这里就不用实现了。
  3. 监听器创建完成后需要把它绑定给_eventDispatcher事件分发器,_eventDispatcher 是 Node 的属性,通过它我们可以统一管理当前节点(如:场景、层、精灵等)的所有事件分发情况。
    将事件监听器 listener 添加到事件调度器_eventDispatcher中有两种方法,即如下的两个函数:
    void addEventListenerWithSceneGraphPriority(EventListener* listener, Node* node);              
    void addEventListenerWithFixedPriority(EventListener* listener, int fixedPriority);
    

    两者的主要区别在于它们加入到事件分发器中的优先级的差异。其中的使用 addEventListenerWithSceneGraphPriority 方法添加的事件监听器优先级固定为0;而使用 addEventListenerWithFixedPriority 方法添加的事件监听器的优先级则可以自己设置,但不可以设置为 0,因为这个是保留给 SceneGraphPriority使用的。

3、最后在GameScene.cpp文件中实现触摸回调函数
一旦玩家开始触碰屏幕,我们的程序就会开始调用相应的触摸事件处理函数来处理相应的逻辑,所以现在我们就可以来完成这部分的逻辑了。

实现中有以下几个需要注意的问题:

  • 需要判断触碰是单点还是多点,如果是多点,那么就缩放;是单点,就拖动。
  • 节点缩放的参考点默认是其锚点。显然,对于一个大地图背景来说,如果不实时改变它的锚点位置和本身位置,那它的缩放必然不会按照选取的区域进行缩放,必然会出现类似下图的情况。

p2

(上图中,背景图片的锚点在蓝点的位置,当我们想放大红圈所圈的那棵树时,如果只是简单的改变背景的放大倍率,那一定会出现上图的第二种情况(目标会向右上角偏移);但如果我们把背景的锚点和位置都设置到目标处,那就会像第三中情况一样,得到一个比较好的放大效果。)

  • 当缩放到一定程度,如缩小到与可视区域一样时,为了避免出现空白的区域,我们需要做一些处理。同时地图不能无止境的放大或缩小,需要有一定的范围来约束。比如,放大到它本身的4倍时,应该停止放大。
  • 拖动地图移动时,地图不能移出可视区域,这里需要做边界控制。

掌握了这些注意事项以后,现在我们就可以开始具体的行动了。

首先,在GameScene.h中定义如下的变量:

Sprite* bgSprite;
Vec2 bgOrigin;

bgSprite是地图背景,需要缩放和移动的对象都是其子节点,这样我们就可以通过操作它来实现缩放和移动了。bgOrigin用于记录bgSprite的初始原点位置。

接着,我们跳转到GameScene.cpp的init()方法,修改之前添加地图背景的方法,同时初始化bgOrigin。如下代码所示:

mapLayer = Layer::create();
this->addChild(mapLayer,-1);
 
bgSprite = Sprite::create("2.jpg");
bgSprite->setAnchorPoint(Vec2::ZERO);
bgSprite->setPosition(Vec2::ZERO),
bgOrigin = Vec2(Vec2::ZERO);
mapLayer->addChild(bgSprite);
 
auto treeSprite = Sprite::create("1.png");
treeSprite->setAnchorPoint(Vec2::ZERO);
treeSprite->setPosition(Vec2::ZERO),
treeSprite->setScale(2);
bgSprite->addChild(treeSprite, 2);
 
auto map = TMXTiledMap::create("mymap8.tmx");
map->setAnchorPoint(Vec2::ZERO);
map->setPosition(Vec2::ZERO),
bgSprite->addChild(map, 1);

因为对层而言,它相比于其他的节点来说,其锚点、位置、大小都不好控制,所以我们需要通过另外的节点(比如这里的bgSprite)来执行后面的缩放和移动等动作。

最后压轴来了,实现触摸事件的处理函数如下:

void GameScene::onTouchesMoved(const std::vector&touches, Event  *event)
{
    auto winSize = Director::getInstance()->getWinSize();
    if(touches.size() > 1)        // 多点进行缩放
    {
        // 得到当前两触摸点
        auto point1 = touches[0]->getLocation();
        auto point2 = touches[1]->getLocation();
        // 计算两点之间得距离
        auto currDistance = point1.distance(point2);
        // 计算两触摸点上一时刻之间得距离
        auto prevDistance = touches[0]->getPreviousLocation().distance(touches[1]->getPreviousLocation());
        // 两触摸点与原点的差向量,pointVec1和pointVec2是相对于bgSprite的位置
        auto pointVec1 = point1  - bgOrigin;
        auto pointVec2 = point2  - bgOrigin;
        // 两触摸点的相对中点
        auto relMidx = (pointVec1.x + pointVec2.x) / 2 ;
        auto relMidy = (pointVec1.y + pointVec2.y) / 2 ;
        // 计算bgSprite的锚点
        auto anchorX = relMidx / bgSprite->getBoundingBox().size.width;
        auto anchorY = relMidy / bgSprite->getBoundingBox().size.height;
        // 相对屏幕的中点
        auto absMidx = (point2.x + point1.x) / 2 ;
        auto absMidy = (point2.y + point1.y) / 2 ;
 
        // 缩放时,为了避免出现空白的区域,需要做以下的边界处理。        
        // 当bgSprite快要进入到屏幕时,修改bgSprite的位置(既absMidx和absMidy)。
        if(  bgOrigin.x > 0)
        {
            absMidx -= bgOrigin.x;
        }
        if( bgOrigin.x < -bgSprite->getBoundingBox().size.width + winSize.width )
        {
            absMidx +=  -bgSprite->getBoundingBox().size.width + winSize.width - bgOrigin.x;
        }
        if( bgOrigin.y > 0 )
        {
            absMidy -= bgOrigin.y;
        }
        if( bgOrigin.y < -bgSprite->getBoundingBox().size.height + winSize.height )
        {
            absMidy +=  -bgSprite->getBoundingBox().size.height + winSize.height - bgOrigin.y;
        }
        // 重设bgSprite锚点和位置
        bgSprite->setAnchorPoint(Vec2(anchorX, anchorY));
        bgSprite->setPosition(Vec2(absMidx, absMidy));
        // 根据两触摸点前后的距离计算缩放倍率
        auto scale = bgSprite->getScale() * ( currDistance / prevDistance);
        // 控制缩放倍率在1~4倍之间,最小倍率不能太小,不让背景将不能填充满整个屏幕。
        scale = MIN(4,MAX(1, scale));
        bgSprite->setScale(scale);
        // 更新原点位置
        bgOrigin = Vec2(absMidx, absMidy) - Vec2(bgSprite->getBoundingBox().size.width * anchorX, bgSprite->getBoundingBox().size.height * anchorY) ;
    }
    else if(touches.size() == 1)        // 单点进行移动
    {
        // 单点时,touches中只有一个Touch对象,所以通过touches[0]就可以得到触摸对象
        auto touch = touches[0];
        // 计算滑动过程中的滑动增量
        auto diff = touch->getDelta();       
        // 得到当前bgSprite的位置
        auto currentPos = bgSprite->getPosition();
        // 得到滑动后bgSprite应该所在的位置
        auto pos = currentPos + diff;
        // 得到此刻bgSprite的尺寸
        auto bgSpriteCurrSize = bgSprite->getBoundingBox().size;
 
        //边界控制,约束pos的位置
        pos.x = MIN(pos.x, bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x);
        pos.x = MAX(pos.x, -bgSpriteCurrSize.width + winSize.width + bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x);
        pos.y = MIN(pos.y, bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y);
        pos.y = MAX(pos.y, -bgSpriteCurrSize.height + winSize.height + bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y);
        // 重设bgSprite位置
        bgSprite->setPosition(pos);
 
        // 更新原点位置
        if( pos.x >= bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x
           || pos.x <= -bgSpriteCurrSize.width + winSize.width + bgSpriteCurrSize.width * bgSprite->getAnchorPoint().x)
        {
            diff.x = 0;
        }
        if( pos.y >= bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y
           || pos.y <= -bgSpriteCurrSize.height + winSize.height + bgSpriteCurrSize.height * bgSprite->getAnchorPoint().y)
        {
            diff.y = 0;
        }
        bgOrigin += diff;
    }
}

以上就是onTouchesMoved函数的实现方法了,原理已在注释中解释清楚,所以我想理解起来已经不会很难。下面给出一张示意图帮助大家理解:

p3

下图是缩放过程中刚好出现空白的区域时的图形示意图:

p4

此时空白的区域的宽等于 -bgSprite->getBoundingBox().size.width + winSize.width - bgOrigin.x,所以我们把背景的位置向右移动-bgSprite->getBoundingBox().size.width + winSize.width - bgOrigin.x个单位就可以避免这种情况的出现。

代码中有一点需要注意的是,在缩放过程中,bgSprite的尺寸不断变化的,所以计算起锚点或进行边界处理时,一定要用它缩放后的尺寸宽高来计算,而不能是它本身的宽高。 所以代码中计算bgSprite的尺寸我们用getBoundingBox函数来获得经过缩放和旋转之后的外框盒大小,而不用getContentSize函数来获得节点原始的大小。

iOS端多点触碰默认是关闭的,所以需要在AppController.mm 程序启动回调中启用多点触摸才可以,具体方法是在以下的函数段后加入[eaglView setMultipleTouchEnabled:YES];

如下所示:

CCEAGLView *eaglView = [CCEAGLView viewWithFrame: [window bounds]
                                 pixelFormat: kEAGLColorFormatRGBA8
                                 depthFormat: GL_DEPTH24_STENCIL8_OES
                          preserveBackbuffer: NO
                                  sharegroup: nil
                               multiSampling: NO
                             numberOfSamples: 0];
 
[eaglView  setMultipleTouchEnabled:YES];

总的来说,要想很好的实现这一功能不是容易的,以上就是我们实现了的一种方法,虽然细节上还有一些问题,也未在真机上测试,但还是希望能对大家的学习有所帮助。如果你有更好的方法实现,也可以提出来,大家一起进步学习。

下章将讲解如何利用CocoStudio来制作一个支持多分辨率的UI系统。

标签: none

?>