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

原创: @涵紫任珊

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

本章让我们回顾一下上一章所做的事情,有点忘了!呵呵,对了,就是下图的效果:

result

上一章,我们通过Cocos Studio编辑器简单的制作了一个能够自动适应多分辨率的UI,而在本章,我们将教会大家如何拖动滚动层中的商品选项到地图中,效果如下图所示:

result1

这里,我们第一步需要做的事是,把滚动层中的物品换成我们需要的商品,然后再完成拖动操作(把选中的商品拖动到TMX地图中空白的(没有其他障碍瓦片的)位置处)。下面就一起开始跟着做吧。

设置商品项

在Cocos Studio中,每个商品的信息都如下左图所示,包含了选项背景、商品描述、价格等等一些相同的属性。

item

所以在程序中,我们可以通过遍历滚动层中所有的子项来获得各属性的值,并重新设置它。这样一来,我们先在程序中定义如下的一些变量:

// 商品图样
const char* shop_textures[8] =
{
    "shopItem/Item1.png", "shopItem/Item2.png", "shopItem/Item3.png", "shopItem/Item4.png", "shopItem/Item5.png", "shopItem/Item6.png", "shopItem/Item1.png", "shopItem/Item2.png"
};

// 拖动过程中,选中项正常的纹理图,这个正常是能被添加在拖动位置
const char* move_textures[8] =
{
    "shopItem/moveItem1.png", "shopItem/moveItem2.png", "shopItem/moveItem3.png", "shopItem/moveItem4.png", "shopItem/moveItem5.png", "shopItem/moveItem6.png", "shopItem/moveItem1.png", "shopItem/moveItem2.png"
};

// 拖动过程中,选中项不正常的纹理图
const char* move_textures_en[8] =
{
    "shopItem_en/Item1.png", "shopItem_en/Item2.png", "shopItem_en/Item3.png", "shopItem_en/Item4.png", "shopItem_en/Item5.png", "shopItem_en/Item6.png", "shopItem_en/Item1.png", "shopItem_en/Item2.png"
};

// 商品描述
const char* shop_info[8] =
{
    "土地", "破树", "烂树", "烂草", "破屋", "破地", "婆婆", "反正破",
};

// 所值价格
const int shop_money[8] =
{
    20, 40, 60, 100, 120, 99, 50, 200,
};

再在initUI函数中添加如下的一段代码:

auto shop_scrollView = dynamic_cast(panel_shop->getChildByName("scrollview_shop"));

for (int i = 0; i < shop_scrollView->getChildren().size(); ++i)
    {
        Layout* shop_layout = static_cast(shop_scrollView->getChildren().at(i));
        shop_layout->setTag(SHOP_ITEM_LAYOUT_TAG + i);

        ImageView* buy_Sprite = static_cast(shop_layout->getChildByName("shopitem"));
        buy_Sprite->loadTexture(shop_textures[i]);

        TextField* info = static_cast(shop_layout->getChildByName("info"));
        info->setText(shop_info[i]);

        TextField* money = static_cast(shop_layout->getChildByName("money_image")->getChildByName("money"));
        money->setText(std::to_string(shop_money[i]));
    }

这样我们就可以设置好每个商品项了。再次运行程序,就会是下面这样的效果:

setItem

是不是很简单,哈哈,接下来继续看看怎样拖到商品项里的商品。

拖到操作

在Cocos Studio中,每个商品项中的“shopitem”是一个可交互的ImageView(前面我们自己设置的),我们可以通过给它绑定一个TouchEvent回调,让它像按钮一样可以处理更多不同的响应事件。

所以,回到initUI函数,为buy_Sprite绑定一个回调函数:

buy_Sprite->addTouchEventListener(CC_CALLBACK_2(GameScene::SpriteCallback, this));

其中,SpriteCallback回调方法和之前的menuShopCallback方法类似,需要根据TouchEvent事件的类型(按下、移动、抬起、取消),进行相应的逻辑处理。

拖动操作大概的过程是,按住某个商品,把它往游戏场景中拖,如果拖到的地方,该处的某个地图层上又没有其他的障碍物“瓦片”(我们把tmx地图的每一块图块都叫做瓦片),那我们放手时就在这里生成一个对应的瓦片商品。

具体的实现如下所示:

void GameScene::SpriteCallback(cocos2d::Ref* pSender, Widget::TouchEventType type)
{
    Size winSize = Director::getInstance()->getWinSize();
    // 获得所选择的Widget,和它的父Widget(也就是商品项)   
    Widget* widget = static_cast(pSender);
    Widget* parent = static_cast(widget->getParent());
    // 得到商品项的标记
    int tag = parent->getTag();

    // 根据TouchEventType类型进行逻辑处理
    switch (type)
    {
        // 按下手指/鼠标时,放大选中得商品
        case Widget::TouchEventType::BEGAN:
            widget->runAction( EaseElasticInOut::create( ScaleTo::create(0.1f, 1.5), 0.2f));
            break;

        case Widget::TouchEventType::MOVED:
            // 滑动手指/鼠标时,如果购物滚动面板是弹出状态,就先把它收起来 
            if(comeOut == true)
            {
                panel_shop->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(-panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                shop_btn->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(- panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                comeOut = false;
            }
            // 如果buyTarget为空,就先创建它
            if( buyTarget== NULL ){                
                buyTarget = Sprite::create(move_textures[tag - SHOP_ITEM_LAYOUT_TAG]);
                buyTarget->setAnchorPoint(Vec2(0.5f, 0));
                 // 把buyTarget添加到bgSprite上,这样buyTarget的位置就是相对于bgSprite了。
                bgSprite->addChild(buyTarget, 10);
            }
            // 移动buyTarget 
            else{                          
                Vec2 pos;
                // 因为buyTarget的位置是相对于地图的,所以我们需要考虑到它的移动和缩放。
                pos.x = (widget->getTouchMovePos().x - bgOrigin.x)/bgSprite->getScale();
                pos.y = (widget->getTouchMovePos().y - bgOrigin.y)/bgSprite->getScale();

                buyTarget->setPosition(pos);
                // 检测是否可以创建商品,这个后面会讲解。
                moveCheck(pos, tag - SHOP_ITEM_LAYOUT_TAG);
            }            
            break;
        // 抬起  
        case Widget::TouchEventType::ENDED:
            // 还原放大的widget
            widget->runAction( EaseElasticInOut::create(ScaleTo::create(0.1f, 1), 0.2f));
            // 移除buyTarget
            if(buyTarget != NULL)
            {
                buyTarget->removeFromParent();
                buyTarget= NULL;
            }            
            canBliud = false;
            break;
        // 取消触摸
        case Widget::TouchEventType::CANCELED:
            // 还原放大的widget
            widget->runAction( EaseElasticInOut::create(ScaleTo::create(0.1f, 1), 0.2f));
            // 生成瓦片
            if( canBliud == true )
            {
                // 得到放手时位置
                auto endPos =Vec2((widget->getTouchEndPos().x - bgOrigin.x)/bgSprite->getScale(), (widget->getTouchEndPos().y - bgOrigin.y)/bgSprite->getScale());
                // 在convertTotileCoord( endPos)位置上设置(生成)一个GID为9 + tag - SHOP_ITEM_LAYOUT_TAG的瓦片,这对应着滚动层中的商品,如下图所示。
                map->getLayer("2")->setTileGID(9 + tag - SHOP_ITEM_LAYOUT_TAG, convertTotileCoord( endPos));
                canBliud = false;
            }
            // 移除buyTarget
            if(buyTarget != NULL)
            {
                buyTarget->removeFromParent();
                buyTarget= NULL;
            }  
            // 弹出购物滚动面板          
            if(comeOut == false)
            {
                panel_shop->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                shop_btn->runAction( EaseElasticOut::create(MoveBy::create(1, Vec2(panel_shop->getContentSize().width / 3 * 2, 0)), 0.5f));
                comeOut = true;
            }
            break;
        default:
            break;
    }
}

以上代码在触摸到可交互的商品时,先获取到它父节点的标记,这样我们就知道它是哪种商品了。再根据事件类型,进行以下的逻辑处理:

  • 触摸开始时,为所选择的商品(widget)添加一个放大的缓动效果,证明它是被选中了的。当然如果触摸开始到结束的时间太短,是不会有这个效果的。
  • 拖到widget时,先像在menuShopCallback中一样,收起弹出的购物滚动面板;然后创建一个临时的购买目标项(buyTarget),让它跟着手指的移动而移动,在此同时,检测它目前所在的地图位置上是否可以创建一个商品,关于这一点,后面会更详细的说明。
  • 正常完成触摸后,还原放大的widget,同时移除buyTarget。
  • 非正常取消触摸时,同样先还原放大的widget,移除buyTarget,弹出购物滚动面板。然后最重要的一点:在地图上“创建”选中的商品。这点我们也留到后面说明。

这里TouchEventType::ENDED在正常触摸完后,手指离开屏幕得时候触发,而TouchEventType::CANCELED则是在非正常触摸完结束时触发,比如手指没正常离开时候来了个电话,或者滑动过程中滑出了Widget范围抬起手指。

另外,地图图块已更新为:

iso-test-128

不用在意最后两个瓦片和前面的相同,这是本人的原因,因为实在是找不到资源了,做一点算一点吧,有机会再改。

上述检测以及在地图上生成选中商品的过程,都需要我们把Cocos2d-x坐标转化为瓦片地图对应的地图坐标。因为只要这样我们才能获得TMX地图层中每块瓦片的信息。关于瓦片地图的详细介绍请参考瓦片地图一文。

下面是把屏幕坐标转换为地图坐标的一段函数的实现:

Vec2 GameScene::convertTotileCoord(Vec2 position)
{
    // 得到瓦片地图的瓦片尺寸,对于本游戏是(30 * 30)
    auto mapSize = map->getMapSize();
    // 计算当前缩放下,每块瓦片的长宽
    auto tileWidth = map->getBoundingBox().size.width / map->getMapSize().width;
    auto tileHeight = map->getBoundingBox().size.height / map->getMapSize().height;
    // 把position转换为瓦片坐标,确保得到的是整数
    int posx = mapSize.height - position.y / tileHeight + position.x / tileWidth - mapSize.width / 2;
    int posy = mapSize.height - position.y / tileHeight - position.x / tileWidth + mapSize.width / 2;

    return Point(posx, posy);
}

convertTotileCoord方法将返回一个45度地图的坐标(这个坐标可能是超出正常取值范围的,它未经约束),其中参数position必须是一个相对于bgSprite(也就是瓦片地图 map)的位置值,因为地图的位置可能会有滚动和缩放的变化;参数tag是传入的标记值。

convertTotileCoord方法的注释已给出,其中最难理解的应该是将屏幕坐标转换为瓦片地图坐标的数学公式,其数学原理请阅读:www.gandraxa.com/isometric_projection.aspx

检测(moveCheck)的实现:

void GameScene::moveCheck(Vec2 position, int tag)
{
    auto mapSize = map->getMapSize();
    // 将position转化为地图坐标
    auto tilePos = this->convertTotileCoord(position);

    // canBliud是用于判断是否可生成瓦片的变量
    canBliud = false;

    // 约束tilePos的范围。如果tilePos在正确取值范围内(菱形内)
    if( tilePos.x >= 0 && tilePos.x <= mapSize.width - 1 && tilePos.y >= 0 && tilePos.y<= mapSize.height - 1)
    {
        // 前半段map->getLayer("2")是取得地图中名称为“2”的图层,而getTileGIDAt(tilePos)则是得到tilePos坐标上瓦片的GID标示。(GID标示为0时,表示该处没有任何的瓦片。)
        int gid = map->getLayer("2")->getTileGIDAt(tilePos);
        // 该处没有其他障碍瓦片时
        if (gid == 0) 
        {    // 设置拖动过程中正常的buyTarget纹理
            buyTarget->setTexture(move_textures[tag]);
            canBliud = true;
        }
        // 该处有障碍瓦片时,把buyTarget设置为move_textures_en中颜色偏红的纹理
        else{
            buyTarget->setTexture(move_textures_en[tag]);
            canBliud = false;
        }
    }
    // 如果位置在地图以外(四个角),同样把buyTarget设置为move_textures_en中颜色偏红的纹理
    else{
        buyTarget->setTexture(move_textures_en[tag]);
        canBliud = false;
    }
}

如果一切正常,此时我们运行游戏已经可以把购物面板中的商品无付费的拖动到场景中了,当然细节上还没考虑太多,后面再慢慢考虑。

但这儿还有一个问题,就是随着地图的移动和缩放,你会发现有时手指移动的位置和商品的位置不一致,就像下图这样:

err

找了半天,发现产生这个Bug的原因还是bgOrigin的问题。所以,果断回到onTouchesMoved函数,把下面一段代码:

    // 更新原点位置
    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;

修改为了:

    Vec2 off = pos - currentPos;        
    bgOrigin += off;

这样代码逻辑才更加严谨,是之前疏忽了。
好的,这章就算完成了,下面给一张清晰的游戏效果图。

result

点击这里下载资源。

标签: none

?>