九、关卡数据

转载至:http://cn.cocos2d-x.org/tutorial/show?id=1300

读取游戏关卡数据

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

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

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

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

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

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

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

p1

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

p2

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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文件中解析的关卡数据的。

1
2
3
4
5
6
7
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的实现方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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对象中保存。

p3

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

判断游戏是否结束

判断是否过关

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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()方法中加上如下的判断。

1
CC_SYNTHESIZE( bool , enemySuccessful, EnemySuccessful);
1
2
3
4
5
6
7
8
9
10
11
12
13
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函数体中,这样程序会逐帧检测游戏是否失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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);
             }
         }
     }
}

添加工具栏

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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);
}
分享到:


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值