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

原创: @涵紫任珊

怎样制作基于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章

上一章中,我们实现了商品的拖动操作,把商店的商品拖动到了地图上,但本章将实现拖动商品的校对检测,同时将实现瓦片的触摸控制(移动、销毁),播种、除草、收获。

前面我们拖动商品到什么地方,它就跟着移动到什么地方,如下图网格里的示意图所示。但一般情况下,为了更加形象的体现出瓦片地图这一特性,一般都不能让待入列的瓦片那些肆掠的移动,所以接下来我们将实现拖动商品项的细节调整。

p1

优化拖动操作

首先,先在头文件中设置两个变量用于记录鼠标/手指当前和在此之前的移动坐标(这个坐标指地图坐标)。

    Vec2 currPos;
    Vec2 perPos;

接着修改moveCheck函数,如下代码所示:

void GameScene::moveCheck(Vec2 position, int tag)
{
    auto mapSize = map->getMapSize();
    auto tilePos = this->convertTotileCoord(position);    
    canBliud = false; 
    // 1
    perPos = currPos;

    if( tilePos.x >= 0 && tilePos.x <= mapSize.width - 1 && tilePos.y >= 0 && tilePos.y <= mapSize.height - 1)
    {
        currPos = tilePos;
        int gid = map->getLayer("2")->getTileGIDAt(tilePos);
        if (gid == 0){
            buyTarget->setTexture(move_textures[tag]);           
            canBliud = true;
        }
        else
        {
            buyTarget->setTexture(move_textures_en[tag]);
            canBliud = false;
        }
        // 2
        auto screenPos = this->convertToScreenCoord(tilePos);
        buyTarget->setPosition(screenPos);     
        // 3
        if(perPos != currPos){
            map->getLayer("3")->removeTileAt(perPos);
            map->getLayer("3")->setTileGID(17, currPos);
        }
    }
    // 4
    else{
        buyTarget->setPosition(position);
        buyTarget->setTexture(move_textures_en[tag]);
        map->getLayer("3")->removeTileAt(perPos);
        canBliud = false;
    }   
}
  1. 给之前坐标赋值,让它等于当前坐标.
  2. 把当前地图坐标转换为屏幕坐标,同时设置商品项的位置到这个屏幕坐标上。这里convertToScreenCoord函数将完成这一转换。这样一来,screenPos坐标就将会是固定的一些值。注意:不要忘了删掉SpriteCallback函数中设置商品项位置的函数段。
  3. 鼠标/手指移动到哪里,就在那里的瓦片上设置一个可以标识它的记号,同时当移动到别的地方,要删除之前位置上的记号。
  4. 当tilePos超出地图范围时,让商品项跟着鼠标/手指的移动而移动,同时移除perPos位置上的记号。

convertToScreenCoord函数中的数学公式其实就是convertTotileCoord函数中数学原理的一个反推公式,其代码如下:

Vec2 GameScene::convertToScreenCoord(Vec2 position)
{
    auto mapSize = map->getMapSize();
    auto tileSize = map->getTileSize();
    auto tileWidth = map->getBoundingBox().size.width / map->getMapSize().width;
    auto tileHeight = map->getBoundingBox().size.height / map->getMapSize().height;

    auto variable1 = (position.x + mapSize.width / 2 - mapSize.height) * tileWidth * tileHeight ;
    auto variable2 = (-position.y + mapSize.width / 2 + mapSize.height) * tileWidth * tileHeight ;

    int posx = (variable1 + variable2) / 2 / tileHeight;
    int posy = (variable2 - variable1) / 2 / tileWidth;

    return Point(posx, posy);
}

最后还缺什么啦,我们来用手指想一下。对的,一般当拖动商品项到一个“不空”的瓦片上或地图之外的区域时,将会提示此处不可放。所以,接下来我们的工作来加这段提示的代码吧。

在SpriteCallback方法的if( canBliud == true ){}函数段后加上如下代码:

    else{
        // 得到放手时鼠标/手指的屏幕坐标,这个坐标是相对于地图的。所以计算它时应该要考虑到地图的移动和缩放。
        auto endPos =Vec2((widget->getTouchEndPos().x - bgOrigin.x)/bgSprite->getScale(), (widget->getTouchEndPos().y - bgOrigin.y)/bgSprite->getScale());
        // 把上面得到的屏幕坐标转换围地图坐标
        auto coord = convertTotileCoord( endPos);
        // 再把地图坐标转换为固定的一些屏幕坐标
        auto screenPos = this->convertToScreenCoord(coord);
        // 创建提醒项,把它设置在screenPos处
        auto tips = Sprite::create("tip.png");
        tips->setPosition(screenPos);
        bgSprite->addChild(tips);
        // 让提醒项出现一段时间后移除它
        tips->runAction(Sequence::create(DelayTime::create(0.1f),                                                CallFunc::create(CC_CALLBACK_0(Sprite::removeFromParent, tips)),                                                NULL));
    }

运行你的游戏,你会发现它的效果如下图所示:

p2

对单独的瓦片进行操作

商品项的拖动效果已经近乎完美,接下来是该对单独的瓦片进行操作了。经营模式游戏中,当玩家触碰到地图瓦片时,将可以进行相应的操作,比如:触碰到树木将可以砍掉它;触碰到土地将可以播撒小麦、土豆、玉米等农作物,如果上成熟状态可以收获它们;触碰到建筑物时将可以修整,拆除它等等。如果玩家长按某个瓦片,我们还能移动它。

反正在这类游戏中对瓦片的操作是非常繁琐的,接下来我们就来一步一步的攻克它吧。最终的目标是做一个类似下图(《全面农场》)的效果。

p3 p4 p5

接下来我们需要先判断玩家的触碰是长按还是短按,然后在考虑其他的问题。

判断短按长按

Cocos2d-x中是没有长按事件的触发机制的,所以如果想实现长按的检测,需要自己实现。本教程中通过schedule来实现长按操作,其原理如下:

当玩家触碰到屏幕上的瓦片(除了草地,既瓦片地图的最底层)时,开始计时。如果在计时过程中发生了触摸移动或者异常中断了触摸响应,那么就取消本次计时。在计时过程中,如果达到预定时间,那么则执行相应的函数。最后在结束本次触摸时(抬起鼠标/手指),结束计时。

根据以上阐述的原理,我们需要一个变量来判断是否继续计时,所以请先在头文件中定义如下的变量:

bool press;
Vec2 touchObjectPos;

其中touchObjectPos由于记录触摸到的瓦片的地图坐标。

接下来找到onTouchesBegan函数,在函数中添加如下的一段代码:

    if(touches.size() == 1)
    {
        auto touch = touches[0];
        auto screenPos = touch->getLocation();       
        auto mapSize = map->getMapSize();       
        Vec2 pos;
        pos.x = (screenPos.x - bgOrigin.x)/bgSprite->getScale();
        pos.y = (screenPos.y - bgOrigin.y)/bgSprite->getScale();
        auto tilePos = this->convertTotileCoord(pos);        
        if( tilePos.x >= 0 && tilePos.x <= mapSize.width - 1 && tilePos.y >= 0 && tilePos.y <= mapSize.height - 1){
            int gid = map->getLayer("2")->getTileGIDAt(tilePos);
            // 如果瓦片地图的"2"层上tilePos处存在其他瓦片,则执行以下代码。
            if (gid != 0)
            {
                touchObjectPos = tilePos;
                map->getLayer("3")->setTileGID(17, tilePos);
                this->schedule(schedule_selector(GameScene::updatePress), 2);
                // longPress在开始按下的时候为true,如果移动,取消,则为false,在抬起的时候如果变量为true,那么执行schedule中的updatePress函数
                press = true;
            }
        }
    }

schedule的原理我就不废话了,不清楚的同学可以查看官方文档调度器(scheduler)一文。updatePress函数如下:

void GameScene::updatePress(float t)
{
    // 取消计时
    this->unschedule(schedule_selector(GameScene::updatePress));
    if(press)
    {
        log("是长按");
        map->getLayer("3")->removeTileAt(touchObjectPos);
        press = false;
    }
}

接下来在onTouchesMoved函数中添加如下代码:

    press = false;
    map->getLayer("3")->removeTileAt(touchObjectPos);
    this->unschedule(schedule_selector(GameScene::updatePress));

最后添加一个用于出来结束触摸响应的onTouchesEnded事件函数,其代码如下所示:

void GameScene::onTouchesEnded(const std::vector&touches, Event  *event)
{
    this->unschedule(schedule_selector(GameScene::updatePress));

    Size winSize = Director::getInstance()->getWinSize();
    if(touches.size() == 1)
    {
        auto touch = touches[0];
        auto screenPos = touch->getLocation();
        if(press)
        {
            log("是短按。此处应该创建相应的操作面板了");

            map->getLayer("3")->removeTileAt(touchObjectPos);
            press = false;
        }
    }
}

创建操作面板

对于一个模拟经营类游戏来说,操作面板之多,所以本教程就不大动干戈挨个的创建了,象征性的例举几个例子就行,毕竟这只是一篇教程,而不是商业项目,所以感兴趣的同学请跟着教程思路自己去拓展一下。

下面以播种面板为例,创建一个播种面板SeedChooseLayer,它继承于Layer,其定义如下所示:

// 定义农作物类型
typedef enum
{
    WHEAT = 0,
    CORN = 1,
    CARROT,
    NOTHING
} CropsType;

class SeedChooseLayer: public Layer
{
public:
    virtual bool init() override; 
    // 重载触摸回调函数
    bool onTouchBegan(Touch *touch, Event *event);
    CC_SYNTHESIZE(CropsType, currType, CurrType);// 选中的作物类型
    CREATE_FUNC(SeedChooseLayer);

private:
    Sprite* wheat;
    Sprite* corn;
    Sprite* carrot;
};

对去播种面板层的触摸响应,这里和地图的响应略有不同,它是单点触摸事件,所以以上重载的是用于出来单点事件的onTouchBegan函数(触摸点击开始事件)。

接下来,我们来实现SeedChooseLayer的各个方法。如下是init的实现。

bool SeedChooseLayer::init()
{
    if (!Layer::init())
    {
        return false;
    }
    currType = CropsType::NOTHING;

    auto bgSprite = Sprite::create("chooseBg.png");
    bgSprite->setAnchorPoint(Vec2(1, 0));
    this->addChild(bgSprite);

    wheat = Sprite::create("corn.png");
    wheat->setAnchorPoint( Point(0, 0));
    wheat->setPosition(Point(0, 0));
    bgSprite->addChild(wheat);

    corn = Sprite::create("wheat.png");
    corn->etPosition(Point(bgSprite->getContentSize().width/2, bgSprite->getContentSize().height/2));
    bgSprite->addChild(corn);

    carrot = Sprite::create("carrot.png");
    carrot->setAnchorPoint( Point(1, 1));
    carrot->setPosition(Point(bgSprite->getContentSize().width, bgSprite->getContentSize().height));
    bgSprite->addChild(carrot);

    // 创建事件监听器,OneByOne表示单点
    auto touchListener = EventListenerTouchOneByOne::create();
    // 设置是否向下传递触摸,true表示不向下触摸。
    touchListener->setSwallowTouches(true);
    touchListener->onTouchBegan = CC_CALLBACK_2(SeedChooseLayer::onTouchBegan, this);

    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener, wheat);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener->clone(), corn);
    _eventDispatcher->addEventListenerWithSceneGraphPriority(touchListener->clone(), carrot);
    return true;
}

在init函数中,我们初始化了播种面板中种子的布局,并且创建绑定了触摸事件。这里要注意:在面板中,我们设置了三个农作物选项供玩家选择,每一个选项都要处理触摸响应事件。所以,绑定需要绑定到每个选项上。然而当我们再次使用事件监听器的时候,需要使用 clone() 方法重新克隆一个,因为每个监听器在添加到事件调度器中时,都会为其添加一个已注册的标记,这就使得它不能够被添加多次。

绑定好触摸事件后,接下来需要实现具体的触摸回调。在onTouchBegan函数中我们要做的是:当玩家触碰到某选项时,重新设置其透明度,向玩家表明该项是被选中的;并且确定其选择的作物类型,方便我们后面代码的获取和使用。其代码如下图所示:

bool SeedChooseLayer::onTouchBegan(Touch *touch, Event *event)
{
    auto target = static_cast(event->getCurrentTarget());   // 1
    Point locationInNode = target->convertTouchToNodeSpace(touch);   // 2
    // 3
    Size size = target->getContentSize();
    Rect rect = Rect(0, 0, size.width, size.height);   
    // 4
    if (rect.containsPoint(locationInNode))
    {
        target->setOpacity(180);
        // 5
        if (target == wheat)
        {
            currType = CropsType::WHEAT;
        }else if(target == corn)
        {
            currType = CropsType::CORN;
        }else if(target == carrot)
        {
            currType = CropsType::CARROT;
        }else{
            currType = CropsType::NOTHING;
        }
        return true;
    }
    return false;
}
  1. 返回触摸事件当前作用的目标节点。
  2. 把touch对象中保存的屏幕坐标转换到GL坐标,再转换到目标节点的本地坐标下。
    在Node对象中有几个函数可以做坐标转换。convertToNodeSpace方法可以把世界坐标转换到当前node的本地坐标系中;convertToWorldSpace方法可以把基于当前node的本地坐标系下的坐标转换到世界坐标系中;convertTouchToNodeSpace这个函数可以把屏幕坐标系转换到GL坐标系,再转换到父节点的本地坐标下。
  3. 计算目标节点的矩形区域。
  4. 判断触碰点在不在目标节点的矩形区域内,即判断是否被选中。
  5. 根据选择的目标确定作物的类型。

以上我们就已经实现了播种界面的创建了,最后在onTouchesEnded函数中判断为短按的地方加上如下代码就可以实现播种界面的显示了。

panel = SeedChooseLayer::create();
panel->setPosition(screenPos);
this->addChild(panel, 10, SEEDPANEL_TAG);

效果如下图所示:

p6

本章资源已上传,点此可进行下载,文章中的源代码将在本系列教程结束时上传,敬请期待。

标签: none

?>