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

原创: @涵紫任珊

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

本章我们将继续学习SLG游戏的制作,本章教程将是该系列游戏的最后一章。在开始之前,我先要对各位翘首以盼等候很久的童鞋们说声抱歉,由于此前事情太多,所以这一系列文章一拖再拖,迟迟没有更新,还望见谅啦!

下面我们回到正题,在上一篇文章中,我们已经讲到了各种操作界面的创建,接下来我们来创建其中相比下较难一点的计时面板,也可以说是计时层。最终我们要实现的游戏效果如下图所示:

png.jpg

计时面板类

这个计时面板是在播种的时候同未成熟幼苗一起创建的,每当播洒一颗种子时,该处坐标上就会创建一个隐藏的计时面板;当该处坐标再次处于选中状态,那么隐藏的计时面板就会显示出来;直到时间耗尽时,该计时面板会从场景中移除。

定义计时面板类

对于计时面板来说,需要注意以下几点:

  • 首先,这里计时面板是一个用进度条表示时间进度的控件项;
  • 其次,对于不同的农作物而言,它们的成熟时间,标题标示都是不相同的;
  • 在一个计时面板的生命周期中,我们需要不停的更新面板状态:进度条不断的减短,时间不断的减少;
  • 最后在时间耗尽时,移除该面板。

效果如下:
result
根据以上分析结果,计时面板的定义如下:

class TimingLayer: public Layer
{
public:
    virtual bool init() override;
    static TimingLayer* create(Vec2 pos, CropsType type);
    virtual void updateProgressBar(float dt);// 更新

    CC_SYNTHESIZE(bool, timeOut, TimeOut);    // 记录时间是否有用
    CC_SYNTHESIZE(Vec2, timingLayerPos, TimingLayerPos);// 标示TimingLayer的位置
    void setParam(CropsType type);            // 设置TimingLayer属性
private:
    ProgressTimer* progressBar;             // 进度条
    Label* nameLabel;                        // 显示标示了计时面板所属类型的文本项
    Label* timeLabel;                        // 显示剩余时间的文本项

    int counter;                            // 计时项
    int growUpTime;                            // 农作物成熟所需时间,也就是计时面板的生命周期
    float percent;                            // 进度条的百分比
};

我们通过create(Vec2 pos, CropsType type)两个属性来创建计时面板,其一是坐标项,用它来标示计时面板的“位置”,每个地图位置上至多只能有一个计时面板;其二是农作物类型,通过它可以确定计时面板的时间和标题项。

计时面板的实现

首先我们先来看看TimingLayer的初始化,如下:

bool TimingLayer::init()
{
   if (!Layer::init())
    {
        return false;
    }
    counter= 1;
    percent = 100;
    // 1
    auto progressBarBg = Sprite::create("progressBg.png");
    progressBarBg->setAnchorPoint(Vec2(0, 0));
    progressBarBg->setPosition(Vec2(0,  0 ));
    addChild(progressBarBg);
    // 2
    progressBar = ProgressTimer::create(Sprite::create("progressBar.png"));
    progressBar->setType(ProgressTimer::Type::BAR);
    progressBar->setMidpoint(Point(0, 0.5f));
    progressBar->setBarChangeRate(Point(1, 0));
    progressBar->setPercentage(percent);
    progressBar->setAnchorPoint(Point(0.5f, 0.5f));
    progressBar->setPosition(Point(progressBarBg->getContentSize().width / 2,  progressBarBg->getContentSize().height /2 ));
    progressBarBg->addChild(progressBar);
    // 3
    nameLabel = Label::createWithBMFont("fonts/Font30.fnt"," ");
    nameLabel->setPosition(Vec2(progressBarBg->getContentSize().width / 2,  progressBarBg->getContentSize().height ));
    nameLabel->setAnchorPoint(Vec2(0.5f, 0.0f));
    progressBarBg->addChild(nameLabel);
    // 4
    timeLabel = Label::createWithBMFont("fonts/Font30.fnt","");
    timeLabel->setPosition(Vec2(progressBarBg->getContentSize().width / 2,  - progressBarBg->getContentSize().height ));
    timeLabel->setAnchorPoint(Vec2(0.5f, 0.0f));
    progressBarBg->addChild(timeLabel);
    // 5
    this->schedule(schedule_selector(TimingLayer::updateProgressBar), 1.0f);
    return true;
}
  1. 创建并添加进度条的背景图片,这样进度条看着才更明显。
  2. 创建进度条并把它添加到背景图片上,这里ProgressTimer是Cocos2d-x自带的进度条类。
    • ProgressTimer有两种类型:一种是环形,一种是条形,使用ProgressTimer时我们需要通过setType方法指明它所属类型。
    • 另外,setMidpoint方法设置进度条的起始点,(0,y)表示最左边,(1,y)表示最右边,(x,1)表示最上面,(x,0)表示最下面。
    • setBarChangeRate方法用来设置进度条变化方向的,如果不用变化的方向,则设置该方向为0,否则设置为1。所以(1,0)表示横方向,(0,1)表示纵方向。
    • ProgressTimer有一个非常最要的percentage属性。它代表了当前进度条的进度值,这也是为什么我们在类中定义了percent属性的原因。如果要让一个进度条正常的显示出来,那么percentage的值必须大于0。setPercentage方法能设置ProgressTimer的percentage值。
  3. 创建标示了计时面板所属类型的文本项,也就是用它来显示计时面板的标题。这里通过Label的createWithBMFont函数来创建,其中Font30.fnt字体是我通过Glyph Designer(一款Mac下的字库图集制作工具,Windows下可使用Hiero和BMFont)生成的位图字体,里面包含了本游戏所需的全部字符。 createFont.png
  4. 创建并添加显示剩余时间的文本项。
  5. 每秒更新一次updateProgressBar函数。

初始化TimingLayer的后,接下来我们来创建TimingLayer:

TimingLayer* TimingLayer::create(Vec2 pos, CropsType type)
{
    TimingLayer *pRet = new TimingLayer();
    if (pRet && pRet->init())
    {
        pRet->setTimingLayerPos(pos);
        pRet->setParam(type);      
        pRet->autorelease();
        return pRet;
    }
    else
    {
        delete pRet;
        pRet = NULL;
        return NULL;
    }
}

其中setTimingLayerPos方法设置TimingLayer所处的位置,setParam方法根据传入的CropsType类型设置TimingLayer的相关属性,如下代码所示:

void TimingLayer::setParam(CropsType type)
{
    switch (type) {
        case WHEAT:
            nameLabel->setString("小麦");// 标题标示
            growUpTime = 60;            // 成熟时间
            break;
        case CORN:
            nameLabel->setString("玉米");
            growUpTime = 120;
            break;
        case CARROT:
            nameLabel->setString("胡萝卜");
            growUpTime = 180;
            break;          
        default:
            break;
    }
}

最后,我们来看看如何更新TimingLayer的进度条等状态,代码如下:

void TimingLayer::updateProgressBar(float dt)
{
    counter++;
    percent = 100 - float(counter) / float(growUpTime ) * 100; // 1
    if(percent <= 100 && percent > 0 ) // 2
    {
        progressBar->setPercentage(percent);
        timeLabel->setString(std::to_string(growUpTime - counter) + " 秒");
    }
    else // 3
    {
        this->setTimeOut( true);
        this->unschedule(schedule_selector(TimingLayer::updateProgressBar));
        this->removeFromParent();
    }
}
  1. 注意计算百分比时,计数器/成熟时间的值应该是一个“小数”类型,所以要将它们转换成float类型再进行计算。
  2. 当percent的值在0到100的范围内时,更新进度条的百分比,同时更新显示的剩余时间。
  3. 当percent超出范围时,设置timeOut属性为true,停止更新updateProgressBar函数,并且移除TimingLayer。

现在我们的计时面板就算创建好了,接下来我们就可以在播种时创建一个这样的计时器面板了。

添加计时面板

回到主场景,我们在GameScene中定义一个向量来保存游戏中所有的TimingLayer,这样遍历该向量可以控制所有计时面板的显示和隐藏。

Vector timingVector;

在播种的时候,计时面板会同农作物幼苗一起“创建”,所以下面我们在播种的地方创建相应的计时面板,即像下面一样修改update函数:

        switch (type)
        {
            case WHEAT:
            {
                map->getLayer("goodsLayer")->setTileGID(18, touchObjectPos);
                createTimingLayer(WHEAT);
                this->removeChild(seedPanel);
            }
                break;
            case CORN:
            {
                map->getLayer("goodsLayer")->setTileGID(20, touchObjectPos);
                createTimingLayer(CORN);
                this->removeChild(seedPanel);
            }
                break;
            case CARROT:
            {
                map->getLayer("goodsLayer")->setTileGID(22, touchObjectPos);
                createTimingLayer(CARROT);
                this->removeChild(seedPanel);
            }
                break;
            default:
                break;
        }

其中createTimingLayer函数是用于创建计时面板的方法,其代码段如下所示:

void GameScene::createTimingLayer(CropsType type)
{
    auto timingLayer = TimingLayer::create(touchObjectPos, type); // 1
    auto screenPos = this->convertToScreenCoord(touchObjectPos); // 2
    timingLayer->setPosition(screenPos);
    timingLayer->setVisible(false);  // 3
    bgSprite->addChild(timingLayer, 10); // 4

    this->timingVector.pushBack(timingLayer); // 5 
}
  1. 在touchObjectPos位置上创建一个type类型的计时面板。
  2. 将touchObjectPos转换成场景坐标,并设置计时面板的位置到该处。
  3. 创建的计时面板初始状态下是不可以见的,所以要把它隐藏起来。
  4. convertToScreenCoord方法转换的场景坐标是相对于整个地图的,所以我们把这个计时面板添加到地图的背景上。
  5. 把创建的计时面板压入向量中,便于统一管理。

显示隐藏计时面板

现在当我们“播种”的时候就可以为每一个幼苗一同创建对应的计时器了。这些计时面板都是隐藏的,那么在此之后,我们需要在再次选中幼苗时,让计时面板能显示出来。所以要在以下两个地方做些调整。
1、在触摸开始时,即onTouchesBegan函数中确定所有的计时面板是隐藏的:

    for (TimingLayer* timingLayerTemp : timingVector)
    {
        timingLayerTemp->setVisible(false);
    }

2、在触摸结束时,即onTouchesEnded函数中加上以下判断:

    else if(tileType == GROUD_CROP)
    {
        for (int i = 0; i < timingVector.size(); i++)
       {
           auto temp = timingVector.at(i);
           auto pos = temp->getTimingLayerPos();
           if( pos == touchObjectPos)
           {        
               temp->setVisible(true);
           }
        }
    }

更新成熟的农作物

到现在为止,运行游戏你会发现一切都差不多趋于完整,接下来当时间耗尽时,我们就该收获庄稼了。所以我们在Update函数中加入以下函来更新农作物的状态,便于后面可以收割。

void GameScene::updateRipeCrop()
{
    for (int i = 0; i < timingVector.size(); i++)
    {
        auto temp = timingVector.at(i);
        if( temp->getTimeOut())  
        {
            auto pos = temp->getTimingLayerPos(); 
            auto gid = map->getLayer("goodsLayer")->getTileGIDAt( pos); 
            switch (gid) 
            {
                case 18:
                {
                    map->getLayer("goodsLayer")->setTileGID(19, pos);
                    tileType = TileType::CROP_HARVEST;
                }
                    break;
                case 20:
                {
                    map->getLayer("goodsLayer")->setTileGID(21, pos);
                    tileType = TileType::CROP_HARVEST;
                }
                    break;
                case 22:
                    map->getLayer("goodsLayer")->setTileGID(23, pos);
                    tileType = TileType::CROP_HARVEST;
                    break;
                default:
                    break;
            }
            timingVector.eraseObject(temp); 
        }
    }
}

updateRipeCrop函数遍历了计时面板向量timingVector,它通过得到每个计时面板的timeOut属性来判断农作物是否成熟。如果成熟则通过得到相应计时面板的位置来改变该位置处的瓦片块状态,从而实现将幼苗换成成熟的状态,这里18,20,22分别表示幼苗时期的农作物,19,21,23是成熟的农作物,这种写法当初没想太多,但请各位小伙们不要跟我学,:]嘻嘻,这样太不规范,久了会忘的,最好还是用标记标示,这里我动动嘴皮子就不改了。

收获农作物

最后,经过一番辛勤的劳作之后,是时候收获成果了。

所以此时我们就可以在检查到type等于CROP_HARVEST的时候时,创建收获界面了。这里收获农作物同移除瓦片(即RemoveLayer)的功能超级相似,相信大家依葫芦画瓢就可以实现,相关的代码也已上传,所以也就不在过多赘述了。

最终我们已经实现了之前所承诺的《全面农场》的功能,如下图所示: res3.jpg 播种、计时、收获整个操作的效果图如下:
result1

总结

目前,整个游戏已经有了大致的雏形,接下来需要做的逻辑原理大同小异,所以我就不再废话。我想对于一个游戏Demo来说,该SLG游戏教学的目的已经差不多达到了吧,感兴趣的同学请自行扩展,本游戏的源码我已上传到Github仓库,点击可以下载,同时也欢迎大家克隆、斧正、提交pr。

标签: none

?>