Cocos2d-x塔防游戏_贼来了9——关卡数据

原创: 任珊

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

读取游戏关卡数据

关卡设计对于塔防游戏是必须的,通过关卡设计玩家可以尝试各种不同风格和难度的游戏。它是游戏的重要组成部分,游戏的节奏、难度等级等方面很大程度上要依靠关卡来控制。而在各关中,敌人波数、敌人个数、地图、金币等等信息都是不同的。如果每个关卡都循规蹈矩的重构代码,那它的重用率将会很高,程序的藕合度也将很低,因此,这里我们很有必要把这些数据信息收集起来统一管理。这样,在游戏场景中我们就可以在不同的关卡中重用相同的一套逻辑了。

但是,现在另一个问题又出现了,我们该如何储存和处理这些数据啦?

我们可以把游戏中的数据分为静态数据和动态数据两种:

  • 动态数据是指游戏运行和运营过程中不断变动的数据,这些数据会随着玩家在游戏世界中执行各种行为的不同而发生改变,如本游戏中的分数。一般简单的数据可以使用Cocos2d-x中的UserDefault来进行动态数据的存储,大型的数据则会更倾向于用SQLite来进行存储。所以,在开发过程中应该根据需求来选择数据存储方案。
  • 静态数据则是程序中的只读数据,如资源名,敌人起始血量,起始金币数等等。然而,为了达到最佳的游戏效果或方便测试,这些数据在开发过程中可能是经常变动的。所以为了便于修改,一般会把这些数据放到外部文件中进行保存,杜绝硬编码。

现在回到游戏,我们第一步需要做的是抽象出一组有关关卡信息的静态数据,然后把它们写到文件中,便于读取。

如果是简单数据的读取,我们除了使用常用的格式之外,我们还可以用Cocos2d-x最常用的plist来读取。plist是基于XML的纯文本格式,随便找个文本编辑器就可以编辑。当然,如果你使用的是OS X系统,那在XCode中可以直接创建和编辑plist文件。下面我们就来和大家共同学习一下plist。

根据本游戏关卡的特征,我们抽象出了包括如下所示的一系列数据:



把这些关卡数据写入plist文件后,第二步就可以设计一个类来解析读取数据了。要解析plist文件可以参考Cocos2d-x类库中的SpriteFrameCache类和ParticleSystem类,它们使用ValueMap类来对plist文件进行操作。下图是创建好的plist文件:



说了那么多,接下来我们还是来看看代码吧。如下所示:

class LoadLevelinfo: public Ref
{
public:    
    ~LoadLevelinfo();
    static LoadLevelinfo * createLoadLevelinfo(const std::string& plistpath);

    bool initPlist(const std::string& plistpath);
    void readLevelInfo();
    void clearAll();   
private:
    ValueMap resources;
    ValueMap levelInfo;
};

变量resources是关卡待加载的资源数据,levelInfo是关卡信息数据。initPlist方法根据plist文件路径加载并读取游戏相关数据,readLevelInfo则是读取并保存plist文件中所有属性的值。而这些值都被保存在GameManger中,如下就是GameManger中增加的属性,它们基本上都是用来存储从plist文件中解析的关卡数据的。

    CC_SYNTHESIZE(int, money, Money);
    CC_SYNTHESIZE(int, groupNum, GroupNum);
    CC_SYNTHESIZE(std::string, curMapName, CurMapName);
    CC_SYNTHESIZE(std::string, currLevelFile, CurrLevelFile);
    CC_SYNTHESIZE(std::string, nextLevelFile, NextLevelFile);
    CC_SYNTHESIZE(bool, isFinishedAddGroup, IsFinishedAddGroup);
    CC_SYNTHESIZE(std::string, curBgName, CurBgName);

接下来是initPlist和readLevelInfo的实现方法,如下所示:

bool LoadLevelinfo::initPlist(const std::string& plistpath)
{ 
    bool bRet = false;
    do
    {
        // 1
        std::string fullPath = FileUtils::getInstance()->fullPathForFilename(plistpath);
        ValueMap dict = FileUtils::getInstance()->getValueMapFromFile(fullPath);  
        // 2    
        resources = dict["resources"].asValueMap();
        levelInfo = dict["levelInfo"].asValueMap();   
        bRet = true;
    }
    while (0);
    return bRet;
}

void LoadLevelinfo::readLevelInfo()
{
    GameManager *instance = GameManager::getInstance();
    // 3
    auto money =   levelInfo["money"].asFloat();
    instance->setMoney(money);
    auto currlevel =   levelInfo["currlevel"].asString();
    instance->setCurrLevelFile(currlevel);
    auto nextlevel =   levelInfo["nextlevel"].asString();
    instance->setNextLevelFile(nextlevel);

    ValueMap& groupDict = levelInfo["group"].asValueMap();
    auto groupTotle = groupDict.size();
    instance->setGroupNum(groupTotle);

    for (auto iter = groupDict.begin(); iter != groupDict.end(); ++iter)
    {
        ValueMap& group = iter->second.asValueMap();
        std::string spriteFrameName = iter->first;
        auto type1Num = group["type1Num"].asInt();
        auto type2Num = group["type2Num"].asInt();
        auto type3Num = group["type3Num"].asInt();
        auto type1Hp = group["type1Hp"].asInt();
        auto type2Hp = group["type2Hp"].asInt();
        auto type3Hp = group["type3Hp"].asInt();

        GroupEnemy* groupEnemy = GroupEnemy::create()->initGroupEnemy(type1Num, type1Hp, type2Num, type2Hp, type3Num, type3Hp);
        instance->groupVector.pushBack(groupEnemy);
    }

    auto curMapName =   resources["map"].asString();
    instance->setCurMapName(curMapName);
    auto curBgName =   resources["image"].asString();
    instance->setCurBgName(curBgName);
}
  1. plistpath是.plist文件的相对路径,这里通过FileUtils类获得给定文件名的完整路径,再把该文件中的内容(类型为Dictionary)加载到ValueMap的对象中保存。
  2. 放到Map中即可用Map的方法读取键为”id"的值是多少,分别读取dict对象中键为”resources",”levelInfo"的值,它们的类型依旧是Dictionary,所以依旧将其内容转换到ValueMap对象中保存。

  3. 根据plist文件的属性和层次特征,一层一层的遍历获得相应类型的键值,再把它们存储到GameManager中。
  4. 根据从plist文件中获得的敌人信息创建一波敌人(groupEnemy),并把它插入groupVector向量统一管理。

判断游戏是否结束

判断是否过关

当最后一波敌人添加完后,变量isSuccessful将被置为true。这也说明了玩家游戏已经顺利过关,该跳转到下一个界面了。

在update函数体中添加如下的代码段,实现最终分数的评比和场景跳转。

    if(isSuccessful)
    {
        isSuccessful = false;
        auto star = 0;
        auto playHp = this->getPlayHpPercentage();

        if( playHp > 0 && playHp <= 30){ star = 1;}         else if(playHp > 30 && playHp <= 60 ){ star = 2;}         else if(playHp > 60 && playHp <= 100 ){ star = 3;}         if( star > UserDefault::getInstance()->getIntegerForKey(instance->getCurrLevelFile().c_str()))
        {
            UserDefault::getInstance()->setIntegerForKey(instance->getCurrLevelFile().c_str(), star);
        }

        instance->clear();
        // 应该跳转到成功界面,这里暂时显示如下文字
        Size winSize = Director::getInstance()->getWinSize();
        auto putOutLabel = Label::createWithBMFont("fonts/boundsTestFont.fnt", "Congratulations!");
        putOutLabel->setPosition(Point(winSize.width / 2, winSize.height / 2 ));
        putOutLabel->setScale(4);
        this->addChild(putOutLabel);
    }

该段代码将根据玩家剩余血量来评定分数,当血量在60到100之间时,玩家将得到三颗星;当在30到60之间时,则为两颗星;在0到30之间时就只会得到一颗星了。

正如前面所说,像游戏分数这样简单的动态数据我们使用UserDefault来进行存储即可,所以,我们把过关后的分数用UserDefault存储起来。它的键名是从plist中读取的,为了能清楚地分便,所以设为了该关数据的文件名。

判断是否失败

当敌人移动到最后一个路径点的时候,这也意味着该敌人成功的攻克了玩家的防守,它取得了胜利。所以我们需要为每个敌人都添加一条是否成功进入玩家阵地的属性,并在敌人的nextPoint()方法中加上如下的判断。

CC_SYNTHESIZE(bool, enemySuccessful, EnemySuccessful);
Node* EnemyBase::nextPoint()
{
    int maxCount = this->pointsVector.size();
    pointCounter++;
    if (pointCounter < maxCount  ){         auto node =this->pointsVector.at(pointCounter);
        return node;
    }
    else{
        setEnemySuccessful(true);
    }
    return NULL;
}

每当有敌人攻克防守时,玩家的血量就会相应的减少,当玩家血量减少到0时,游戏失败,跳转到下一个界面。实现该方法的enemyIntoHouse函数我们依旧把它放在update函数体中,这样程序会逐帧检测游戏是否失败。

void PlayLayer::enemyIntoHouse()
{
    auto enemyVector = instance->enemyVector;
    for (int i = 0; i < enemyVector.size(); i++)     {         auto enemy = enemyVector.at(i);         if( enemy->getEnemySuccessful())
        {
            instance->enemyVector.eraseObject(enemy);
            enemy->removeFromParent();
            auto playHp = getPlayHpPercentage() - 10;
            if(playHp > 0){
                setPlayHpPercentage(playHp);
                playHpBar->setPercentage(playHp);
            }
            else{
                instance->clear();
                // 应该跳转到失败界面
                this->removeAllChildren();
                Size winSize = Director::getInstance()->getWinSize();
                auto putOutLabel = Label::createWithBMFont("fonts/boundsTestFont.fnt", "Game Over");
                putOutLabel->setPosition(Point(winSize.width / 2, winSize.height / 2 ));
                putOutLabel->setScale(4);
                this->addChild(putOutLabel);
            }
        }
    }
}

添加工具栏

这里工具栏指游戏场景上方的图形化信息提示栏。为游戏添加工具栏可以更直观的观察到游戏的信息动态,如游戏金币数、当前波数、总波数等等信息。所以,它是很有必要的。

void PlayLayer::initToolLayer()
{
    auto size = Director::getInstance()->getWinSize();
    toolLayer = Layer::create();
    addChild(toolLayer);
    // 工具栏背景图片
    auto spritetool = Sprite::createWithSpriteFrameName("toolbg.png");
    spritetool->setAnchorPoint(Point(0.5f, 1));
    spritetool->setPosition (Point(size.width / 2, size.height));
    toolLayer->addChild(spritetool);   
    // 金币数
    money = instance->getMoney();
    moneyLabel = Label::createWithBMFont("fonts/bitmapFontChinese.fnt", " ");
    moneyLabel->setPosition(Point(spritetool->getContentSize().width / 8, spritetool->getContentSize().height / 2));
    moneyLabel->setAnchorPoint(Point(0, 0.5f));
    auto moneyText = std::to_string(money);
    moneyLabel->setString(moneyText);
    spritetool->addChild(moneyLabel);   
    // 玩家血量条
    playHpBar = ProgressTimer::create(Sprite::createWithSpriteFrameName("playhp.png"));
    playHpBar->setType(ProgressTimer::Type::BAR);
    playHpBar->setMidpoint(Point(0, 0.4f));
    playHpBar->setBarChangeRate(Point(1, 0));
    playHpBar->setPercentage(playHpPercentage);
    playHpBar->setPosition(Point(spritetool->getContentSize().width / 5 *4  , spritetool->getContentSize().height / 2));
    spritetool->addChild(playHpBar);
    // 玩家得分标尺  
    auto star = Sprite::createWithSpriteFrameName("playstar.png");
    star->setPosition(Point(spritetool->getContentSize().width / 5 *4 , spritetool->getContentSize().height / 2));
    spritetool->addChild(star); 
    // 当前波数
    int groupTotal = instance->getGroupNum();
    groupLabel = Label::createWithBMFont("fonts/bitmapFontChinese.fnt", " ");
    groupLabel->setPosition(Point(spritetool->getContentSize().width / 8 * 3, spritetool->getContentSize().height / 2 ));
    groupLabel->setAnchorPoint(Point(0.5f , 0.5f));
    auto groupInfoText = std::to_string(groupCounter + 1);
    groupLabel->setString(groupInfoText);
    spritetool->addChild(groupLabel);
    // 总波数
    groupTotalLabel = Label::createWithBMFont("fonts/bitmapFontChinese.fnt", " ");
    groupTotalLabel->setPosition(Point(spritetool->getContentSize().width / 2 , spritetool->getContentSize().height / 2 ));
    groupTotalLabel->setAnchorPoint(Point(0.5f , 0.5f));
    auto groupTotalText = std::to_string(groupTotal);
    groupTotalLabel->setString(groupTotalText);
    spritetool->addChild(groupTotalLabel);
}

标签: cocos2d-x教程

?>