新手入门,Cocos2d-x

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

原创: 任珊

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

上一章我们讲述了场景层中触摸响应的原理和过程,紧接着下面我们来创建一个炮塔选择面板层,该层是触摸场景层后的产物。在该层中玩家可以选择添加不同类型的炮塔,抛开其他层不看,这一层其实就是如下所示的一层:

选择面板的响应

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

// 定义炮塔类型
typedef enum
{
    ARROW_TOWER = 0,
    ATTACK_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("multiDirTower.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();
    // 设置是否向下传递触摸,true表示不向下触摸。
    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);

这里要注意:当我们再次使用事件监听器的时候,需要使用 clone() 方法重新克隆一个,因为每个监听器在添加到事件调度器中时,都会为其添加一个已注册的标记,这就使得它不能够被添加多次。

实现触摸回调函数

绑定好触摸事件后,接下来仍是实现具体的触摸回调。在该层回调函数中我们要做的有:当玩家触碰到某选项时,重新设置其透明度,向玩家表明该项是被选中的;当玩家松开手时,确定其选择的炮塔类型,方便我们后面的获取和使用。代码如下:

bool TowerPanleLayer::onTouchBegan(Touch *touch, Event *event)
{
    // 1
    auto target = static_cast<Sprite*>(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<Sprite*>(event->getCurrentTarget());
    // 5
    if (target == sprite1)
    {
        chooseTowerType = ARROW_TOWER;
    }
    else if(target == sprite2)
    {
        chooseTowerType = ATTACK_TOWER;
    }
    else if(target == sprite3)
    {
        chooseTowerType = MULTIDIR_TOWER;
    }
    else{
        chooseTowerType = ANOTHER;
    }
}
  1. 返回触摸事件当前作用的目标节点。
  2. 把touch对象中保存的屏幕坐标转换到GL坐标,再转换到目标节点的本地坐标下。

    在Node对象中有几个函数可以做坐标转换。convertToNodeSpace方法可以把世界坐标转换到当前node的本地坐标系中;convertToWorldSpace方法可以把基于当前node的本地坐标系下的坐标转换到世界坐标系中;convertTouchToNodeSpace这个函数可以把屏幕坐标系转换到GL坐标系,再转换到父节点的本地坐标下。
  3. 计算目标节点的矩形区域。
  4. 判断触碰点在不在目标节点的矩形区域内,即判断是否被选中。
  5. 根据选择的目标确定炮塔的类型。

判定玩家是否触摸到某炮塔选项的过程可用如下所示的原理图来解释:



如图:在一个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)。显然地,我们就可以判断点(50,50)在这个大小为(100,100)的矩形中了。

到此为止,我们已经得到了玩家选择的炮塔类型,接下来就可以回到场景层添加炮塔了。

添加炮塔

在场景层中,我们把添加炮塔的方法放在update函数体内,这样程序就会逐帧检测是否添加炮塔,其方法如下:

void PlayLayer::addTower()
{
    if(chooseTowerpanle != NULL  )
    {
        auto type = chooseTowerpanle->getChooseTowerType();
        if(type == TowerType::ANOTHER)
        {
            return;
        }
        Point matrixCoord = convertToMatrixCoord(towerPos);
        int MatrixIndex = static_cast( matrixCoord.y * MAP_WIDTH + matrixCoord.x );
        bool noMoneyTips = false;
        TowerBase* tower = NULL;
        if( type == TowerType::ARROW_TOWER )
        {
            if( money >= 200 )
            {
                tower = ArrowTower::create();
                money -= 200;
            }
            else
                noMoneyTips = true;
        }
        else if( type == TowerType::ATTACK_TOWER )
        {
            if( money >= 150 )
            {
                tower = AttackTower::create();
                money -= 150;
            }
            else
                noMoneyTips = true;
        }
        else if( type == TowerType::MULTIDIR_TOWER )
        {
            if( money >= 200 )
            {
                tower = MultiDirTower::create();
                money -= 200;
            }else
                noMoneyTips = true;
        }
        if(tower != NULL)
        {
            tower->setPosition(towerPos);
            tower->runAction(Sequence::create(FadeIn::create(1.0f),NULL));
            this->addChild(tower);
            instance->towerVector.pushBack(tower);
            towerMatrix[MatrixIndex] =  tower;
        }
        type =  TowerType::ANOTHER;
        chooseTowerpanle->setChooseTowerType(type);
        this->removeChild(chooseTowerpanle);
        chooseTowerpanle = NULL;        
        if( noMoneyTips == true )
        {
            auto tips = Sprite::createWithSpriteFrameName("nomoney_mark.png");
            tips->setPosition(towerPos);
            this->addChild(tips);
            tips->runAction(Sequence::create(DelayTime::create(0.5f),
                                             CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, tips)),
                                             NULL));
        }
    }
}

在这一过程中,我们会根据所选择的炮塔类型来创建炮塔,另外,还会根据当前玩家所持金币数,判断玩家是否有能力购买炮塔。如果玩家金币不足,则会提示玩家。提示方式与前面我们提示不可添加炮塔的方式一样。



对于金币,玩家必须为了它不停的奋斗,因为只有有了足够的金币才能购买到炮塔。当然,进入游戏一般都会有一笔小额的初始使用经费,它能满足玩家刚进入游戏时最基本的要求。在此后的过程,玩家就只有通过努力干掉小偷,从它们身上得到不同经额的回报了。这种游戏机制是不是很熟悉?呵呵,一般的塔防游戏都是这样设计的,我们这也叫遵守传统吧!

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 寻路算法之一 距离优先


原文地址:http://www.waitingfy.com/archives/820
文章作者:瓦力冫

1.效果图

寻路这块在游戏中一直很重要,花了点时间研究了下这个问题,主要参考的是《Data Structures For Game Programmers》,其他的算法用普通Console演示就行了,寻路算法还是用一个界面比较好,最近在学Cocos2d-x,就用它了。用到Cocos2d-x中的基本画线段,画矩形就行了,还有简单的sprite拖动。这demo建了一个线条类,继承CCNode,重写draw方法就行了。在draw方法中简单地调用ccDrawColor4F函数来设置颜色,ccDrawLine来画线条,非常容易,Cocos2d-x这些函数封装了opengles中的原始函数,使用非常简单。sprite拖动可以参考这篇文章《Cocos2d-x Touch 事件应用的一个例子 》

p1

1.小人和红色X都可以用鼠标移动,移到上面的地图上,表示寻路起点和终点。

2.Distance, Simple Heuristic, Complex Heuristic, A Star分别是4种寻路算法,点击程序就会开始演示寻路过程。

3.地图的格子点击会加深颜色,总共4个等级,白,灰,深灰,黑,表示该格子的通过难度,白色是1,灰是2,深灰是3,黑色是不可通过区域。

4.”+++”表示加快演示速度,”—”表示降低演示速度。

2. Breadth – First Search算法

顾名思义,有点像呼吸,一层层地扩展开来,这个时候队列(Queue),stl中的deque就派上用场了。deque不懂可以参考这篇文章《C++ Queue Example Rearranging RailRoad Cars》

p2

起点在中心,会先访问它的第一个外圈,再是第二个。现在我觉得它更像一颗石头扔在水面上的效果。

下面是伪代码:

BreadthFirst( Node )
Queue.Enqueue( Node )Mark( Node )
While( Queue.IsNotEmpty )
Process( Queue.Front )
For Each Child of Queue.Front
if NotMarked( Child )
Queue.Enqueue( Child )
Mark( Child )
end if
end For
Queue.Dequeue()
End While
End Function

遍历一个树或者图都可以用这个方法。在我们这里遇到了点麻烦,因为我们都知道斜角的距离是根号2的倍数,要比上下左右方向远,因为寻路,很重要的因素的距离的长短。我们需要考虑距离这个因素了。

3.Distance – First Search

p3

起点还在中心,这张图显示了每一格到中心的估算距离。如果依靠距离优先的算法,下图是寻路次序:

p4

所以我们定义了一个方向数组:

const int DIRECTION[8][2]={
{0,1},  //north
{1,0},  //east
{0,-1},  //south
{-1,0},  //west
{1,1},  //northeast
{1,-1},  //southeast
{-1,-1},  //southwest
{-1,1}  //northwes
};

这样通过一个for循环,就可以访问它周围一圈的格子了,而且是按照距离优先了,上下左右优先,斜角次些。

因为是地图,我们这里简单定义了一个2维数组,非常简单用一个vector就可以模拟了,假定读者熟悉stl中的vector和C++中的template,不熟悉可以参考这篇文章《STL Vector》《C++ 基础之 “模版函数”,”类模版”》

#ifndef ARRAY2D_H
#define ARRAY2D_H
 
#include 
 
using namespace std;
 
template 
class Array2D{
public:
    Array2D(int p_width, int p_height):m_array(p_width * p_height),
        m_width(p_width),m_height(p_height){
    }
    Datatype* Get(int p_x, int p_y)const{
        return m_array[p_y * m_width + p_x];
    }
    void Set(int p_x, int p_y, Datatype* data){
        m_array[p_y * m_width + p_x] = data;
    }
    int Size() const{
        return m_width * m_height;
    }
    int Width() const{
        return m_width;
    }
    int Height()const{
        return m_height;
    }
private:
    vector m_array;
    int m_width;
    int m_height;
 
};
 
#endif

我们还定义了一个Cell类表示每一个格子:它有很多属性,像位置,最短距离到这个Cell的Cell的位置,是否已经处理过,到起点的距离,是否可以通过,还有就是这个Cell的权重,表示经过难度。我们这里使用了一个从cocos2d-x中拷来的宏,这样get和set方法就不用手写了。

#ifndef _CELL_H
#define _CELL_H
 
#define SYNTHESIZE(varType, varName, funName)\
protected: varType varName;\
public: virtual varType get##funName(void) const { return varName; }\
public: virtual void set##funName(varType var){ varName = var; }
 
class Cell{
public:
    Cell():_marked(false),_distance(0),_lastX(-1),_lastY(-1),
        _x(-1),_y(-1),_passable(true),_weight(1),_drawProgress(false){
    }
 
    SYNTHESIZE(int, _x, X);                       //start at left bottom
    SYNTHESIZE(int, _y, Y);                       //start at left bottom
    SYNTHESIZE(int, _lastX, LastX);               //store the nearest cell's location related this cell
    SYNTHESIZE(int, _lastY, LastY);               //store the nearest cell's location related this cell
    SYNTHESIZE(bool, _marked, Marked);            //whether this cell process or not
    SYNTHESIZE(float, _distance, Distance);       //distance between this cell and start
    SYNTHESIZE(bool, _passable, Passable);        //whether this call can pass
    SYNTHESIZE(int, _drawProgress, DrawProgress); //just for draw the path finding progress
    inline void setWeight(int weight){
        if(weight > 4){
            _weight = 1;
        }else{
            _weight = weight;
            setPassable(weight == 4 ? false : true);
        }
 
    }
    inline int getWeight()const{ return _weight;}
private:
    int _weight;              //default is 1, 4 means this cell is impassable.
                               //distance have relationship with weight
};
 
#endif

核心算法如下:事先需要了解的知识:因为我们需要按照最短距离优先寻路,所以一个优先队列就需要了,这里简单地使用了heap,对heap不了解的可以看下这篇文章《HeapSort(堆排序 C++) 》,下面还用上了C++中的函数指针,可以参考这篇文章《C++ 函数指针 函数名作为参数 》,为什么要用函数指针呢?看完整个寻路算法系列你就知道了。

语言解释:

先把起点Cell加入到heap中,对这个Cell的周围8个Cell进行处理,主要是更新他们到起点的距离和记录最短距离到这个Cell的Cell的位置。每次找到一个新的Cell:
1.如果还没处理过,标上处理过标志,更新他们到起点的距离和记录最短距离到这个Cell的Cell的位置,再把这个Cell加入到堆中,重新形成一个堆,这样开始很容易得到离起点最近的点。

2.如果处理过,看下新的距离是不是比老的距离短,如果短,更新上面的提到的两点。

不断处理,直到访问了所有的点或者找到终点了。

下面是代码,整个寻路算法的核心代码:

typedef bool (*compareTwoCells)(Cell *c1, Cell *c2);
bool compareTwoCellsByDistance(Cell *c1, Cell *c2){
    if(c1->getDistance() <= c2->getDistance()){
        return false;
    }else{
        return true;
    }
}
void HelloWorld::startPathFinding(compareTwoCells compareMethod, int startX,int startY,int goalX,int goalY){
    Cell *startCell = _m_Map.Get(startX, startY);
    vector vecCells;
    vecCells.push_back(startCell);
    make_heap(vecCells.begin(),vecCells.end(),compareMethod);
    startCell->setMarked(true);
    Cell *nowProcessCell;
 
    while(vecCells.size() != 0){
        pop_heap(vecCells.begin(),vecCells.end(),compareMethod);
        nowProcessCell = vecCells.back();
        vecCells.pop_back();
 
        if(nowProcessCell->getX() == _goalX && nowProcessCell->getY() == _goalY){//the goal is reach
            return;
        }
 
        for(int i = 0; i < 8; ++i){ //check eight direction
 
            int indexX = nowProcessCell->getX() + DIRECTION[i][0];
            int indexY = nowProcessCell->getY() + DIRECTION[i][1];
 
            if(indexX >= 0 && indexX < xLineCount && indexY >= 0 && indexY < yLineCount
                && _m_Map.Get(indexX,indexY)->getPassable() == true){//check is a OK cell or not
                    Cell *cell = _m_Map.Get(indexX,indexY);
                    float beforeDistance = DISTANCE[i] * cell->getWeight() + _m_Map.Get(nowProcessCell->getX(),
                        nowProcessCell->getY())->getDistance();//calculate the distance
                    if(cell->getMarked() == false){ 
                        cell->setMarked(true);
                        cell->setLastX(nowProcessCell->getX());
                        cell->setLastY(nowProcessCell->getY());
                        cell->setDistance(beforeDistance);
                        vecCells.push_back(cell);//only push the unmarked cell into the vector
                        push_heap(vecCells.begin(),vecCells.end(),compareMethod);
                    }else{// if find a lower distance, update it 
                        if(beforeDistance < cell->getDistance()){
                            cell->setDistance(beforeDistance);
                            cell->setLastX(nowProcessCell->getX());
                            cell->setLastY(nowProcessCell->getY());
                            make_heap(vecCells.begin(),vecCells.end(),compareMethod);//distance change,so make heap again
                        }
                    }
            }
 
        }
    }
}
startPathFinding(compareTwoCellsByDistance,_playerX,_playerY,_goalX,_goalY);//demo

4.寻路动态图

p5

我只是简单地在起点和终点间加入了一个不可通过的墙,通过查看蓝色的区域会发现这个算法很慢。目标在右边,这个算法上下左右都找,虽然找到了也太浪费资源了吧?下篇我们来看看其他的寻路算法。

源代码下载

【官方教程】Cocos2d-x xml解析

原创: zeroyang

Cocos2d-x 已经加入了tinyxml2用于xml的解析。3.0版本位于external/tinyxml2下。2.x版本位于cocos2dx/support/tinyxml2下。

tinyxml2 Github地址:https://github.com/leethomason/tinyxml2

帮助文档地址:http://grinninglizard.com/tinyxml2docs/index.html

生成xml文档

  1. 引入头文件

    #include "tinyxml2/tinyxml2.h"
    using namespace tinyxml2;
    
  2. xml文档生成

    void  HelloWorld::makeXML(const char *fileName)
    {
    std::string filePath = FileUtils::getInstance()->getWritablePath() + fileName;
    XMLDocument *pDoc = new XMLDocument();
    //xml 声明(参数可选)
    XMLDeclaration *pDel = pDoc->NewDeclaration("xml version=\"1.0\" encoding=\"UTF-8\"");
    pDoc->LinkEndChild(pDel);
    //添加plist节点
    XMLElement *plistElement = pDoc->NewElement("plist");
    plistElement->SetAttribute("version", "1.0");
    pDoc->LinkEndChild(plistElement);
    XMLComment *commentElement = pDoc->NewComment("this is xml comment");
    plistElement->LinkEndChild(commentElement);
    //添加dic节点
    XMLElement *dicElement = pDoc->NewElement("dic");
    plistElement->LinkEndChild(dicElement);
    //添加key节点
    XMLElement *keyElement = pDoc->NewElement("key");
    keyElement->LinkEndChild(pDoc->NewText("Text"));
    dicElement->LinkEndChild(keyElement);
    XMLElement *arrayElement = pDoc->NewElement("array");
    dicElement->LinkEndChild(arrayElement);
    for (int i = 0; i<3; i++) {
        XMLElement *elm = pDoc->NewElement("name");
        elm->LinkEndChild(pDoc->NewText("Cocos2d-x"));
        arrayElement->LinkEndChild(elm);
    }
    pDoc->SaveFile(filePath.c_str());
    pDoc->Print();
    delete pDoc;
    }
    
  3. 打印结果

    
    
    
    
        Text
        
            Cocos2d-x
            Cocos2d-x
            Cocos2d-x
        
    
    
    

上面代码使用tinyxml简单生成了一个xml文档。

解析xml

下面我们就来解析上面创建的xml文档

  1. 引入头文件

    #include "tinyxml2/tinyxml2.h"
    using namespace tinyxml2;
    
  2. xml解析

    void HelloWorld::parseXML(const char *fileName)
    {
    std::string filePath = FileUtils::getInstance()->getWritablePath() + fileName;
    XMLDocument *pDoc = new XMLDocument();
    XMLError errorId = pDoc->LoadFile(filePath.c_str());
    if (errorId != 0) {
        //xml格式错误
        return;
    }
    XMLElement *rootEle = pDoc->RootElement();
    //获取第一个节点属性
    const XMLAttribute *attribute = rootEle->FirstAttribute();
    //打印节点属性名和值
    log("attributename = %s,attributevalue = %s", attribute->Name(), attribute->Value());

    XMLElement *dicEle = rootEle->FirstChildElement("dic"); XMLElement *keyEle = dicEle->FirstChildElement("key"); if (keyEle) { log("keyEle Text= %s", keyEle->GetText()); } XMLElement *arrayEle = keyEle->NextSiblingElement(); XMLElement *childEle = arrayEle->FirstChildElement(); while ( childEle ) { log("childEle Text= %s", childEle->GetText()); childEle = childEle->NextSiblingElement(); } delete pDoc; }

    在节点解析过程中,注意对获取到的节点进行判空处理。

  3. 解析结果打印

    cocos2d: attribute_name = version,attribute_value = 1.0
    cocos2d: keyEle Text= Text
    cocos2d: childEle Text= Cocos2d-x
    cocos2d: childEle Text= Cocos2d-x
    cocos2d: childEle Text= Cocos2d-x

小结

上面的简单示例,演示了如何使用tinyxml进行xml文档生成和解析。更多详细的帮助请参考 tinyxml帮助文档http://grinninglizard.com/tinyxml2docs/index.html

Cocos2d-x深入研究系列

Cocos2d-x深入研究系列

Cocos2d-x基础

Cocos2d-x基础

[Cocos2d-x v3.x官方文档]瓦片地图


glory原创,首发于泰然,转载请注明出处
https://github.com/chukong/cocos-docs/blob/master/manual/framework/native/v3/tiled-map/zh.md
欢迎大家斧正错误,提交PR。

Cocos2d-x官方中文文档 v3.x

在游戏开发过程中,我们会遇到超过屏幕大小的地图,例如即时战略游戏,使得玩家可以在地图中滚动游戏画面。这类游戏通常会有丰富的背景元素,如果直接使用背景图切换的方式,需要为每个不同的场景准备一张背景图,而且每个背景图都不小,这样会造成资源浪费。

瓦片地图就是为了解决这问题而产生的。一张大的世界地图或者背景图可以由几种地形来表示,每种地形对应一张小的的图片,我们称这些小的地形图片为瓦片。把这些瓦片拼接在一起,一个完整的地图就组合出来了,这就是瓦片地图的原理。

TileMap方案

在Cocos2d-x中,瓦片地图实现的是TileMap方案,TileMap要求每个瓦片占据地图上一个四边形或六边形的区域。把不同的瓦片拼接在一起,就可以组成完整的地图了。我们需要很多较小的纹理来创建瓦片。通常我们会将这些较小的纹理放图一张图片中,这样做会提高绘图性能。

瓦片地图编辑器

Cocos2d-x支持由瓦片地图编辑器Tiled Map Editor制作并保存为TMX格式的地图。Tiled Map Editor是一个开源项目,支持Windows、Linux及Mac OS X多个操作系统,我们可以从官网下载到编辑器的Java和QT版本。

如何使用Tiled工具建立地图可以参考以下文章:

如何使用cocos2dx3.0制作基于tilemap的游戏

地图方向

Tiled地图支持直角鸟瞰地图(90°地图)、等距斜视地图(斜45°地图)和六边形地图,不支持左右或上下边界的六边形地图。

地图资源

  • 建议瓦片地图素材大小为32*32的倍数
  • 瓦片素材组与其他图片不能混合使用
  • 只有瓦片素材图能被导入TMX文件
  • 每个Layer最多支持1套瓦片素材组。

瓦片层

  • TMX文件中瓦片层的数量没有上限
  • 每一个瓦片层只能由一种瓦片素材组成
  • 每一个瓦片层可以被TMXLayer类表示-为SpriteSheet的子类
  • 每一个单一的瓦片被Sprite表示-父节点为TMXLayer

对象层

  • 瓦片地图支持对象组
  • 用来添加除背景以外的游戏元素-道具、障碍物等
  • 对象组中的对象在TMX文件中以键值对形式存在,因此可以直接在TMX文件中对他进行修改

瓦片地图坐标系

对于一个16*16的瓦片地图文件的坐标系统为

  • (0, 0): 左上角
  • (15, 15): 右下角

tiledmap

在Cocos2d-x中使用TMX

创建TMX节点

TMXTiledMap *map = TMXTiledMap::create("bg.tmx");
addChild(map, 0);

遍历子节点

Vector pChildrenArray = map->getChildren();

SpriteBatchNode* child = NULL;

Ref* pObject = NULL;

for (Vector::iterator it = pChildrenArray.begin(); it != pChildrenArray.end(); it++) {
    pObject = *it;
    child = (SpriteBatchNode*)pObject;

}

获取/删除一个瓦片

TMXLayer* layer = map->getLayer("layer0");
Sprite* tile0 = layer->getTileAt(Point(1, 15));
layer->removeTileAt(Point(1, 15));

遍历对象层中对象

TMXObjectGroup* objectGroup = map->getObjectGroup("center");
ValueVector object = objectGroup->getObjects();

for (ValueVector::iterator it = object.begin(); it != object.end(); it++) {
    Value obj = *it;
    ValueMap map = obj.asValueMap();
    log("x = %d y = %d", map.at("x").asInt(), map.at("y").asInt());
}

[Cocos2d-x v3.x官方文档]骨骼动画详解-Spine


任珊原创,首发于泰然,转载请注明出处
https://github.com/chukong/cocos-docs/blob/master/manual/framework/native/v3/spine/zh.md
欢迎大家斧正错误,提交PR。

Cocos2d-x官方中文文档 v3.x

游戏中人物的走动,跑动,攻击等动作是必不可少,实现它们的方法一般采用帧动画或者骨骼动画。

帧动画与骨骼动画的区别在于:帧动画的每一帧都是角色特定姿势的一个快照,动画的流畅性和平滑效果都取决于帧数的多少。而骨骼动画则是把角色的各部分身体部件图片绑定到一根根互相作用连接的“骨头”上,通过控制这些骨骼的位置、旋转方向和放大缩小而生成的动画。

它们需要的图片资源各不相同,如下分别是帧动画和骨骼动画所需的资源图:

骨骼动画比传统的逐帧动画要求更高的处理器性能,但同时它也具有更多的优势,比如:

  • 更少的美术资源: 骨骼动画的资源是一块块小的角色部件(比如:头、手、胳膊、腰等等),美术再也不用提供每一帧完整的图片了,这无疑节省了资源大小,能为您节省出更多的人力物力更好的投入到游戏开发中去。
  • 更小的体积: 帧动画需要提供每一帧图片。而骨骼动画只需要少量的图片资源,并把骨骼的动画数据保存在一个 json 文件里面(后文会提到),它所占用的空间非常小,并能为你的游戏提供独一无二的动画。
  • 更好的流畅性: 骨骼动画使用差值算法计算中间帧,这能让你的动画总是保持流畅的效果。
  • 装备附件: 图片绑定在骨骼上来实现动画。如果你需要可以方便的更换角色的装备满足不同的需求。甚至改变角色的样貌来达到动画重用的效果。
  • 不同动画可混合使用: 不同的骨骼动画可以被结合到一起。比如一个角色可以转动头部、射击并且同时也在走路。
  • 程序动画: 可以通过代码控制骨骼,比如可以实现跟随鼠标的射击,注视敌人,或者上坡时的身体前倾等效果。

骨骼动画编辑器——Spine

Spine是一款针对游戏的2D骨骼动画编辑工具,它具有良好的UI设计和完整的功能,是一个比较成熟的骨骼动画编辑器。Spine旨在提供更高效和简洁的工作流程,以创建游戏所需的动画。

使用Spine创建骨骼动画分两大步骤:

  1. 在SETUP模式下,组装角色部件,为其绑定骨骼;
  2. 在ANIMATE模式下,基于绑定好的骨骼创建动画。

下面简单介绍下具体步骤,更多详细内容请查看官方网站教程:Spine快速入门教程

1)在SETUP模式下,选中Images属性,导入所需图片资源所在文件夹,其中路径名和资源名中不能出现中文,否则解析不了;
2)拖动Images下的图片到场景,对角色进行组装(把各个身体部位拼在一起),可通过Draw Order属性调整图片所在层的顺序;
3)创建骨骼,并绑定图片到骨骼上,要注意各骨骼的父子关系。
4)切换到ANIMATE模式,选中要“动”的骨骼,对其进行旋转、移动、缩放等操作,每次改动后要记得打关键帧。
5)在菜单栏找到Texture Packer项,对角色纹理进行打包,资源文件后缀为atlas(而非Cocos2d-x常用的plist)。打包后将生成两个文件,即:png 和 atlas。

6)导出动画文件Json。

Spine动画的使用

Cocos2d-x程序中,使用Spine动画首先需要包含spine的相关头文件。

include 
include "spine/spine.h"
using namespace spine;

其常用方法如下:

创建一个Spine动画对象,将动画文件和资源文件导入。

auto skeletonNode = new SkeletonAnimation("enemy.json", "enemy.atlas");

骨骼动画往往是不止一个动画的,例如:当人物需要行走时,就设置播放动画为行走;当要发动攻击时,就设置播放动画为攻击。下面方法可以设置当前播放动画,其中参数false表示不循环播放,true表示循环播放。

skeletonNode->setAnimation(0, "walk", true);

setAnimation方法只能播放一种动画,所以当要连续播放不同的动画时,需要使用addAnimation方法来实现,它可以一条一条的播放不同的动画。

skeletonNode->addAnimation(0, "walk", true);
skeletonNode->addAnimation(0, "attack", false);

对于一般情况下,动画的切换要求两个动画完全能衔接上,不然会出现跳跃感,这个对于美术来说要求很高,而Spine加了个动画混合的功能来解决这个问题。使得不要求两个动画能完全的衔接上,比如上面的walk和attack动画, 就是衔接不上的,直接按上面的办法播放,会出现跳跃,但是加了混合后,看起来就很自然了。哪怕放慢10倍速度观察,也完美无缺。这个功能在序列帧动画时是无法实现的,也是最体现Spine价值的一个功能。

skeletonNode->setMix("walk", "attack", 0.2f);
skeletonNode->setMix("attack", "walk", 0.4f);

设置动画的播放快慢可通过设置它的timeScale值来实现。

skeletonNode->timeScale = 0.6f;

设置是否显示骨骼通过设置debugBones,true表示显示,false表示隐藏。

skeletonNode->debugBones = true;

例子:创建一个player行走和攻击的动画, 并且循环播放。

auto skeletonNode = new SkeletonAnimation("enemy.json", "enemy.atlas");
skeletonNode->setMix("walk", "attack", 0.2f);
skeletonNode->setMix("attack", "walk", 0.4f);

skeletonNode->setAnimation(0, "walk", false);
skeletonNode->setAnimation(0, "attact", false);
skeletonNode->addAnimation(0, "walk", false);
skeletonNode->addAnimation(0, "attact", true);

skeletonNode->debugBones = true;

Size windowSize = Director::getInstance()->getWinSize();
skeletonNode->setPosition(Point(windowSize.width / 2, windowSize.height / 2));
addChild(skeletonNode);

效果图:

?>