Cocos2d-x塔防游戏_贼来了5——触摸响应

原创: 任珊


Cocos2d-x塔防游戏_贼来了1——基础知识储备
Cocos2d-x塔防游戏_贼来了2——地图的创建加载
Cocos2d-x塔防游戏_贼来了3——进攻的敌人
Cocos2d-x塔防游戏_贼来了4——创建炮塔
Cocos2d-x塔防游戏_贼来了6——触摸响应2
Cocos2d-x塔防游戏_贼来了7——数据管理与碰撞检测
Cocos2d-x塔防游戏_贼来了8——批量添加敌人
Cocos2d-x塔防游戏_贼来了9——关卡数据
Cocos2d-x塔防游戏_贼来了10——选择关卡

引言

上章我们构建了炮塔的基类,并且创建好了三种不同的炮塔,这章我们就来把它添加到场景。

添加炮塔的方式多种多样,你可以拖动某个区域的炮塔到场景,也可以选中某个炮塔后把它插入到特定的场景区域中。但这里我们将模仿保卫萝卜中添加炮塔的方式(PS:触摸屏幕,弹出一个炮塔选择面板,选中面板中相应的炮塔后,在最初触碰到的位置上创建一个炮塔)。其过程如下图所示:

由此可见,上述添加炮塔的过程需要经过两次触碰操作才能实现,即触碰场景层(也就是PlayLayer)和触碰选择面板层(目前还未创建)。具体流程如下:

  1. 触摸场景层的某个位置,如果该处不是路面,且没有其他炮塔和障碍物,那么则在该处“瓦片”(我们把tmx地图的每一块图块都叫做瓦片)上生成一个炮塔选择面板。上图蓝色选择框的位置与触摸的图块重合。
  2. 选择炮塔面板内的炮塔。如果选中了则在第一次触摸屏幕的“瓦片”上,也就是再蓝色选择框的位置处再创建一个相应类型的炮塔,同时移除选择炮塔面板。

对于一款游戏而言,不管你的动画做得多么生动,特效做得多么炫,算法设计的多么牛逼,对它而言最重要的特性还是与玩家在游戏中的实时交互。

《辞海》中这样定义游戏:以直接获得快感为主要目的,且必须有主体参与互动的活动。由此可见,玩家与游戏的互动对于一款游戏是的多么的重要。在移动平台类游戏中,主要的互动动作基本上都是通过触摸屏幕、重力感应等方式体现的。所以接下来,我们将实现触摸功能。

场景层的响应

触摸响应是玩家在移动平台类游戏中交互体验最直接和普遍的方式。在Cocos2d-x 3.0 中,实现触摸响应的一般流程是:

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

下面就先来看看在PlayerLayer中怎样实现第一步触摸的响应吧。

重载触摸回调函数

打开PlayerLayer.h文件,添加以下成员函数。

bool onTouchBegan(Touch *touch, Event *event) override;

因为在场景层我们只需要触摸屏幕生成选择面板,而它的逻辑并不算复杂,所以,我们只需要重载onTouchBegan函数来响应触摸点击开始事件就足够了。onTouchMoved,onTouchEnded和onTouchCancelled都不用重写,反正本游戏用不着。

创建绑定触摸事件

接下来,在PlayerLayer的init方法中添加代码,创建绑定触摸事件。

    auto touchListener = EventListenerTouchOneByOne::create();
    touchListener->onTouchBegan = CC_CALLBACK_2(PlayLayer::onTouchBegan, this);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, this);

一般在使用触摸事件时,我们第一步都是创建一个事件监听器,EventListenerTouchOneByOne 表示单点触摸,与之相对的还有表示多点触摸的 EventListenerTouchAllAtOnce。

接下来我们让监听器绑定事件处理函数。

监听器创建完成后我们把它绑定给eventDispatcher事件分发器,eventDispatcher 是 Node 的属性,通过它我们可以统一管理当前节点(如:场景、层、精灵等)的所有事件分发情况。它本身是一个单例模式值的引用,有了这个属性我们能更为方便的调用。

我们将事件监听器 touchListener 添加到事件调度器_eventDispatcher中。其中使用 addEventListenerWithSceneGraphPriority 方法添加的事件监听器优先级固定为0。本部分内容可参考使用Cocos2d-x制作三消类游戏Sushi Crush——第三部分中触摸事件的响应部分。

实现触摸回调函数

绑定好触摸事件后,接着就该实现具体的触摸回调函数了,代码如下:

bool PlayLayer::onTouchBegan(Touch *touch, Event *event)
{
    if(chooseTowerpanle != NULL)
    {
        this->removeChild(chooseTowerpanle);
    }
    auto location = touch->getLocation();    
    checkAndAddTowerPanle(location);
    return true;
}

一旦玩家开始触碰屏幕,我们的程序就会开始调用onTouchBegan方法,移除已有炮塔选择界面,检测触摸处是否可以创建一个炮塔选择界面,并扶正坐标值,使炮塔(框选项)能刚好添加到地图的“瓦片”上。

在游戏开发中,坐标系是一个非常重要的概念,在调用任何函数设置或得到对象的位置时,都必须要明确这个函数使用的哪个坐标系,各种坐标系之间又怎样转换。如果这部分弄混淆的话,那开发中就可能遇到各种各样的问题。所以现在依旧糊涂的看官们可以参考Cocos2d-x 3.0坐标系详解 一文。

处理触摸事件时touch对象中保存的坐标是屏幕坐标,我们在使用时必须要将它转换到Cocos2d坐标系。getLocation方法可以完成这一转换。

checkAndTowerPanle函数是用于检测和创建炮塔选择界面的方法。检测玩家触摸位置是否可以创建选择面板的条件有两个:第一,该处是空地(根据tmx文件属性判定);第二,该处没有其他炮塔。

  • 判定是草地的方法:为地图中是草地的瓦片添加属性canTouch,并把它标记为1,在程序中读它的值就可以实现了。
  • 判定有无其他炮塔的方法:根据地图的瓦片块数,把地图划分为一个MAP_WIDTH * MAP_HEIGHT的炮塔矩阵,使用TowerBase **towerMatrix数组来存储这个矩阵数据,如果该数组的某个位置为NULL,则该处尚且为空,可以创建一个炮塔。

下面先看看它怎么实现。

void PlayLayer::checkAndAddTowerPanle(Point position)
{
    // 1
    Point towerCoord = convertTotileCoord(position);
    Point matrixCoord = convertToMatrixCoord(position);
    // 2
    int gid = bgLayer->getTileGIDAt(towerCoord);
    auto tileTemp = map->getPropertiesForGID(gid).asValueMap();
    // 3
    int TouchVaule;
    int MatrixIndex = static_cast( matrixCoord.y * MAP_WIDTH + matrixCoord.x );    if (tileTemp.empty())
    {
        TouchVaule = 0;
    }else
    {
        TouchVaule = tileTemp.at("canTouch").asInt();
    }
    // 4
    auto tileWidth = map->getContentSize().width / map->getMapSize().width;
    auto tileHeight = map->getContentSize().height / map->getMapSize().height;
    towerPos = Point((towerCoord.x * tileWidth) + tileWidth/2 -offX, map->getContentSize().height - (towerCoord.y * tileHeight) - tileHeight/2);
    // 5
    if (1 == TouchVaule && towerMatrix[MatrixIndex]==NULL)
    {
        addTowerChoosePanle(towerPos);
    }
    else{
        auto tips = Sprite::createWithSpriteFrameName("no.png");
        tips->setPosition(towerPos);
        this->addChild(tips);
        tips->runAction(Sequence::create(DelayTime::create(0.8f),
                                         CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, tips)),
                                         NULL));
    }
}
  1. 把传入的Cocos2d坐标系分别转换为tiledMap坐标和数组坐标。tiledMap坐标的(0, 0) 坐标在左上角,而数组坐标的(0 , 0角。
  2. 分别求瓦片的全局标和数组下标。对于tiledMap的每一个瓦片来说,它都有一个全局标识量,瓦片的GID范围从正整数1开始到瓦片地图中tile的总量。得到了瓦片的GID就可以获取该块瓦片的全值在左。
  3. 获取瓦片信息。在此之前,我们需要重新编辑一下tmx文件,如下图所示,我们把它的空白处改为用一透明的瓦片来填充(因为这样的话,我们就可以在地图的下层贴如意的背景图了,同时省去了编辑的麻烦),该瓦片有一个canTouch属性,其值为1,这也表明了此瓦片上可以创建炮塔。
  4. 修正炮塔面板坐标。其实这就是个把地图坐标转换为屏幕坐标的过程,需要注意的是,计算坐标值时我们应该减去之前修正误差的那部分距离,这样才能确保准确。
  5. 如果满足该处是空地且无其他炮塔的条件,那么则在该处创建炮塔选择界面;否则在该处添加一个提示错误的图片,不时这个图片会被移除。

下面是转换坐标的两个函数,同样地,在转换中我们需要加上修正tiledMap数值的那段距离offX,既把裁剪掉的那部分地图补会来,否则又会造成错位。

// 把本地坐标(OpenGL坐标)转换为地图坐标
Point PlayLayer::convertTotileCoord(Point position)
{
    int x = (position.x + offX)/ map->getContentSize().width * map->getMapSize().width;
    int y =map->getMapSize().height- position.y / map->getContentSize().height * map->getMapSize().height;

    return Point(x, y);
}
// 把本地坐标(OpenGL坐标)转换为数组坐标
Point PlayLayer::convertToMatrixCoord(Point position)
{
    int x = (position.x + offX)/ map->getContentSize().width * map->getMapSize().width;
    int y = position.y / map->getContentSize().height * map->getMapSize().height;
    t)reurn Point(x, y);
}

选择面板的响应

现在我们来创建选择面板层。

新建一个TowerPanleLayer类,继承于Layer,其定义如下所示:

typedef enum
{
    ARROW_TOWER = 0,
    DECELERATE_TOWER ,
    MULTIDIR_TOWER,
    ANOTHER
} TowerType;
class TowerPanleLayer: public Layer
{
public:
    virtual bool init() override;
    CREATE_FUNC(TowerPanleLayer);

    bool onTouchBegan(Touch *touch, Event *event);
    void onTouchEnded(Touch* touch, Event* event);
    CC_SYNTHESIZE(TowerType, chooseTowerType, ChooseTowerType);

private:    
    Sprite* sprite1;
    Sprite* sprite2;
    Sprite* sprite3;
};

选择面板的触摸响应过程与场景层的响应的过程是一样的,只是具体细节有所不同,这里我们将调用触摸点击开始事件onTouchBegan和触摸结束事件onTouchEnded的回调。

类似地,我们先在TowerPanleLayer头文件中声明相应的事件回调,接下来在init方法中创建绑定触摸事件。

在选择面板中,我们设置了三个炮塔选项供玩家选择,每一个选项都可以处理触摸响应事件。所以,绑定需要绑定到每个选项上,下面就来看看怎么实现吧。

首先,头文件中定义的三个精灵分别表示我们的三个选项,在init方法中添加如下代码初始化它们:

    auto sprite = Sprite::createWithSpriteFrameName("towerPos.png");
    sprite->setPosition(Point(0, 0));
    this->addChild(sprite);

    sprite2 = Sprite::createWithSpriteFrameName("mftower.png");
    sprite2->setAnchorPoint( Point(0.5f, 0));
    sprite2->setPosition(Point(0, sprite2->getContentSize().height/2));
    this->addChild(sprite2);

    sprite1 = Sprite::createWithSpriteFrameName("arrowTower.png");
    sprite1->setAnchorPoint( Point(0.5f, 0));
    sprite1->setPosition(Point(-sprite2->getContentSize().width, sprite2->getContentSize().height/2));
    this->addChild(sprite1);

    sprite3 = Sprite::createWithSpriteFrameName("mftower.png");
    sprite3->setAnchorPoint( Point(0.5f, 0));
    sprite3->setPosition(Point(sprite2->getContentSize().width, sprite2->getContentSize().height/2));
    this->addChild(sprite3);

其中sprite表示选择的区域,它所在地位置也是炮塔将要生成的位置所在地。

之后,我们为每个选项绑定触摸事件。同样是在init方法中:

    auto touchListener = EventListenerTouchOneByOne::create();
    touchListener->setSwallowTouches(true);
    touchListener->onTouchBegan = CC_CALLBACK_2(TowerPanleLayer::onTouchBegan, this);
    touchListener->onTouchEnded = CC_CALLBACK_2(TowerPanleLayer::onTouchEnded, this);

    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, sprite1);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener->clone(), sprite2);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener->clone(), sprite3);

实现触摸回调函数

bool TowerPanleLayer::onTouchBegan(Touch *touch, Event *event)
{
    // 1
    auto target = static_cast(event->getCurrentTarget());
    // 2
    Point locationInNode = target->convertTouchToNodeSpace(touch);
    // 3
    Size size = target->getContentSize();
    Rect rect = Rect(0, 0, size.width, size.height);
    // 4
    if (rect.containsPoint(locationInNode))
    {
        target->setOpacity(180);
        return true;
    }
    return false;
}
void TowerPanleLayer::onTouchEnded(Touch* touch, Event* event)
{
    auto target = static_cast(event->getCurrentTarget());
    // 5
    if (target == sprite1)
    {
        chooseTowerType = ARROW_TOWER;
    }
    else if(target == sprite2)
    {
        chooseTowerType = DECELERATE_TOWER;
    }
    else if(target == sprite3)
    {
        chooseTowerType = MULTIDIR_TOWER;
    }
    else{
        chooseTowerType = ANOTHER;
    }
}
  1. 返回触摸事件当前作用的目标节点。
  2. 把touch对象中保存的屏幕坐标系转换到GL坐标系,再转换到目标节点的本地坐标下。
  3. 计算目标节点的矩形区域。
  4. 判断触碰点在不在目标节点的矩形区域内,即是否被选中。
  5. 根据选择的目标确定炮塔的类型。
  6. 然后我们可以通过containsPoint方法来检测触碰点在不在该目标节点的矩形区域内。

判定玩家是否触摸到了某选项的过程如图所示:


在一个240*360的屏幕内,当玩家触摸屏幕时,触摸对象中将保存下此时的屏幕坐标值(150, 210)。但由于Cocos2d-x中处理坐标一般都使用的是OpenGL坐标,所以这就需要把它转换为(150, 150)的Cocos2d-x坐标值。当我们判定触摸点在不在蓝色的Node内时,可以先计算出这个Node的矩形区域(0, 0, Node宽, Node高),再把触摸点(150, 150)转换到它本地坐标系下。这样就可以直接通过containsPoint方法来判断一个点在不在一个矩形区内了。事例中Node的坐标为(100, 100),所以转换后,触碰点的坐标就变成了(50, 50)。

小结

触摸屏幕实现炮塔的添加是本游戏的一个重要功能,在此过程中,我们需要特别注意坐标的转换和添加炮塔条件的检测。

下节内容将详细讲解如何实现回调函数和添加塔防。

标签: cocos2d-x 3.0教程

?>