教你如何使用Cocos2D-X开发塔防游戏(二)

    作者:课课家教育更新于: 2019-03-27 09:01:06

      上篇我们讲解了制作塔防游戏的准备工作,本篇教程将教你如何使用Cocos2d-x开发塔防游戏的地图。

      以下3张图是完成后的效果图

    教你如何使用Cocos2D-X开发塔防游戏(二)_Cocos2D-X教程_开发Cocos2D-X_游戏开发_课课家

      游戏已完成,除了英雄外,基本还原了90%的游戏内容,一共13关,20种防御塔,30+种敌人,如上图,以假乱真吧。

      下面从地图模块起介绍我的方法,如有更好的方法,请留言一起讨论,游戏资源下载原版游戏数据包,解压即可。

      推荐一款软件,TextureUnpackerRelease1.04可以分割plist形式的图片,提高效率。

      经过一个月学习也发现之前有很多化简为繁的错误。

      正文:

      地图模块我采用3层结构,从下到上分别为地图层,触摸层,按键层。

      地图层负责地图的绘制,添加防御塔、敌人等,触摸层负责拦截一些触摸事件,添加技能/商店技能触摸响应,同时兼顾防御塔升级菜单弹出层,按键层则负责技能和商店按键、玩家生命金钱状态,暂停按键,暂停菜单等。

      首先是地图层,新建一个基类Basemap,基层与Layer

      class BaseMap : public Layer

      {

      public:

      CREATE_FUNC(BaseMap);

      //当前关卡

      CC_SYNTHESIZE(int, level, Level);

      //绑定按键层

      void bindPlayerStateMenu(PlayerStateMenu* playerState);

      Sprite* mapsprite;

      //触摸层

      TouchLayer* mTouchLayer;

      protected:

      //存储每一波敌人信息容器

      std::vector waveVector;

      //存储敌人路线

      std::vector path;

      //下一波敌人提示(玩过游戏的人知道就是那个一闪一闪的骷髅头)

      Vector waveFlags;

      void addWaveProgressBars(std::vector waveFlagLocations);

      void showWaveProgressBars(float dt);

      virtual void addWaves(float dt);

      //添加怪物

      virtual void addMonsters(float dt);

      //初始化地图

      void initMap();

      //添加不同地图装饰物

      virtual void addOrnament(){};

      //添加建塔点

      virtual void addTerrains(){};

      //退出

      virtual void onExitTransitionDidStart();

      virtual void onExit(){};

      //其他

      };

      下面是我采用的plist格式,用文件的形式来保存每一关的信息

      第一个key date保存关卡的信息包括初始金钱,初始生命与一关的敌人波数

      第二个Key monster保存这一关的的怪物信息

      其中分为3个array

      第一层array保存本关所有敌人

      第二层array保存本波敌人,比如有6波敌人,就有6个array,addwave(float dt)将读取这层

      第三层array保存本波敌人这一帧(我设置1.0秒,即1.0秒刷新一次)刷新出的敌人,addmonsters(float dt)将读取这层信息

      玩过KingdomRush的知道这个游戏一般有2-5条道路,每条道路有3个分支,path保存的就是分支,Road保存就是道路,type保存怪物类型,-1表示这一个1.0s不刷新敌人。

      读取如下:

      void BaseMap::loadAndSrtlevelData()

      {

      //加载初始血量金钱等

      auto dataDic = Dictionary::createWithContentsOfFile(String::createWithFormat("level%d_%d_monsters.plist", getLevel(),difficulty)->getCString());

      auto data_array = dynamic_cast(dataDic->objectForKey("data"));

      auto data_tempDic = dynamic_cast(data_array->getObjectAtIndex(0));

      startGold = dynamic_cast(data_tempDic->objectForKey("gold"))->intValue();

      maxLife = dynamic_cast(data_tempDic->objectForKey("life"))->intValue();

      maxWave = dynamic_cast(data_tempDic->objectForKey("wave"))->intValue();

      //加载怪物数据

      auto monsters_array = dynamic_cast(dataDic->objectForKey("monsters"));

      for(int i =0 ;i < monsters_array->count();i++)

      {

      auto monster_array = dynamic_cast(monsters_array->getObjectAtIndex(i));

      std::vector thisTimeMonster;

      for(int j =0;jcount();j++)

      {

      auto tempArray = dynamic_cast(monster_array->getObjectAtIndex(j));

      Vector monsterVector;

      for(int k =0;kcount();k++)

      {

      auto tempDic = dynamic_cast(tempArray->getObjectAtIndex(k));

      monsterVector.pushBack(GroupMonster::initGroupEnemy(

      dynamic_cast(tempDic->objectForKey("type"))->intValue(),

      dynamic_cast(tempDic->objectForKey("road"))->intValue(),

      dynamic_cast(tempDic->objectForKey("path"))->intValue()));

      }

      thisTimeMonster.push_back(monsterVector);

      monsterVector.clear();

      }

      waveVector.push_back(thisTimeMonster);

      thisTimeMonster.clear();

      }

      }

      GroupMonster是用于保存敌人信息的类:

      class GroupMonster: public Node

      {

      public:

      // virtual bool init();

      static GroupMonster* initGroupEnemy(int type, int road, int path);

      CREATE_FUNC(GroupMonster);

      //保存怪物类型

      CC_SYNTHESIZE(int, type, Type);

      //不同出口

      CC_SYNTHESIZE(int, road, Road);

      //不同路线

      CC_SYNTHESIZE(int, path, Path);

      };

      #endif

      另外还需要读取本关路线,这个是原版数据包解压后就有的,例如Levelx.plist文件

      void BaseMap::loadPathFromPlist()

      {

      winSize = Director::getInstance()->getWinSize();

      auto plistDic = Dictionary::createWithContentsOfFile(String::createWithFormat("level%d_paths.plist",getLevel())->getCString());

      auto path_array = dynamic_cast(plistDic->objectForKey("paths"));

      for(int i = 0;icount();i++)

      {

      std::vector tempPathVector;

      auto path_array2 = dynamic_cast(path_array->getObjectAtIndex(i));

      for(int j = 0;jcount();j++)

      {

      std::vector tempRandomPathVector;

      auto path_array3 = dynamic_cast(path_array2->getObjectAtIndex(j));

      for(int k =0;kcount();k++)

      {

      auto tempDic = dynamic_cast(path_array3->getObjectAtIndex(k));

      Point tempPath = Point();

      tempPath.x = dynamic_cast(tempDic->objectForKey("x"))->floatValue()*1.15;

      tempPath.y = dynamic_cast(tempDic->objectForKey("y"))->floatValue()*1.20+50;

      tempRandomPathVector.push_back(tempPath);

      }

      tempPathVector.push_back(tempRandomPathVector);

      }

      path.push_back(tempPathVector);

      }

      }

      因为原版游戏针对低分辨率和高分辨率,使用的是同一个plist文件,所以我猜还需要对路线进行不不同分辨率的修正,将X轴乘以1.15,Y轴乘以1.2加上50敌人就正好可以在高清版上正确行走了(高分辨率对应xxx-hd.png,低分辨率对应xxx.png,我只做了高清的)

      其中:

      std::vector path;

      path.at(x).at(x).at(x)即为敌人的路线。

      自定义一个精灵类来实现倒计时的图标,里面是一个ProgressTimer,用一个定时器来实现ProgressTimer的更新同时实现图标放大->缩小->放大这种呼吸灯的效果

      #ifndef _WAVE_FLAG_H_

      #define _WAVE_FLAG_H_

      #include "cocos2d.h"

      USING_NS_CC;

      class WaveFlag : public Sprite

      {

      public:

      virtual bool init();

      CREATE_FUNC(WaveFlag);

      //获得progressTimer百分比

      float getWavePercentage();

      //重新开始计时

      void restartWaveFlag();

      //停止计时

      void stopRespiration();

      //设置百分比,用于双击立即开始

      void setWavePercentage(float per);

      ProgressTimer* waveProgressTimer;

      //点击后发光效果

      Sprite* selected;

      bool isshown;

      void setSelected();

      private:

      float percentage;

      //呼吸&计时效果定时器

      void startRespiration(float dt);

      };

      #endif

      接下来是Init()初始化这个图标

      bool WaveFlag::Init()

      {

      if (!Sprite::init())

      {

      return false;

      }

      waveProgressTimer = ProgressTimer::create(Sprite::createWithSpriteFrameName("waveFlag_0003.png"));

      waveProgressTimer->setType(ProgressTimer::Type::RADIAL);

      auto flag = Sprite::createWithSpriteFrameName("waveFlag_0001.png");

      flag->setPosition(Point(waveProgressTimer->getContentSize().width/2,waveProgressTimer->getContentSize().height/2));

      selected = Sprite::createWithSpriteFrameName("waveFlag_selected.png");

      selected->setPosition(Point(waveProgressTimer->getContentSize().width/2,waveProgressTimer->getContentSize().height/2));

      waveProgressTimer->addChild(flag);

      waveProgressTimer->addChild(selected);

      selected->setVisible(false);

      addChild(waveProgressTimer);

      setScale(0.8f);

      setVisible(false);

      isShown = false;

      return true;

      }

      对应图片可以数据包中找到

      void WaveFlag::stopRespiration()

      {

      waveProgressTimer->setPercentage(100);

      isShown = false;

      setVisible(false);

      unschedule(schedule_selector(WaveFlag::startRespiration));

      }

      void WaveFlag::restartWaveFlag()

      {

      isShown = true;

      setVisible(true);

      waveProgressTimer->setPercentage(0);

      percentage = 0;

      schedule(schedule_selector(WaveFlag::startRespiration),0.5f);

      }

      void WaveFlag::startRespiration(float dt)

      {

      waveProgressTimer->setPercentage(percentage);

      runAction(Sequence::create(ScaleTo::create(0.25f,0.6f,0.6f),ScaleTo::create(0.25f,0.8f,0.8f),NULL));

      percentage = percentage + 2.5f;

      if(percentage >100){

      isShown = false;

      setVisible(false);

      unschedule(schedule_selector(WaveFlag::startRespiration));

      }

      }

      初始化地图后调用restartWaveFlag()来开始计时,调用startRespiration(float dt)来实现动态效果并且更新ProgressTimer()。

      下面是BaseMap的addWave函数

      void BaseMap::addWaves(float dt)

      {

      for(int i = 0;igetWavePercentage() == 100.0f){

      if(wavesetWave(wave+1,maxWave);

      waveEvent();

      }

      break;

      }

      }

      }

      当上面的waveFlag的ProgressTimer到达100%后,则开始刷新这一波的怪物,初始wave = -1,更新按键层玩家信息后,进入waveEvent()

      void BaseMap::waveEvent()

      {

      schedule(schedule_selector(BaseMap::addMonsters), 1.0f, waveVector.at(wave).size(), 0);

      }

      waveEvent()在父类中只是开始addMonsters(float dt)的功能,没1.0f刷新一次怪(刷新最里层的一个array),刷新的怪物个数为读取文件的该层dict的个数。

      在子类中,需要添加特殊的时间,比如上图猩猩BOSS这波怪,可以在此添加一些新时间,将最后的0S改成需要延时的时间,进行其他时间后再刷新这波敌人(比如酷炫的BOSS入场动画)。

      void BaseMap::addMonsters(float dt)

      {

      //waveVector.size()为波数

      //waveVector.at()保存该wave怪物,size为怪物个数

      //waveVector.at().at()保存该0.5s内需要创建的怪物,.size为怪物个数

      if( time < waveVector.at(wave).size())

      {

      for(int i=0 ;igetType())

      {

      case(0):{

      auto thug = Thug::createMonster(path.at(monsterData->getRoad()).at(monsterData->getPath()));

      addChild(thug);

      GameManager::getInstance()->monsterVector.pushBack(thug);}

      break;

      default:

      break;

      }

      }

      }

      }

      time ++;

      }else{

      time = 0;

      if(wave!=maxWave-1)

      //15秒后显示WaveProgressBar

      {

      SoundManager::playNextWaveReady();

      scheduleOnce(schedule_selector(BaseMap::showWaveProgressBars),15.0f);

      }else{

      isEnd = true;

      }

      }

      }

      将路线赋给怪物,让怪物根据路线在地图上移动,即可实现怪物行走,怪物类将在下面章节中讲解。

      当这一层array刷新完毕,并且wave!=maxWave-1即还不是最后一波时,延迟15 s将waveFlag执行上述restart,重新开始计时;若是最后一波,将IsEnd标记置为true,等待结束画面。

      主要流程就是:

      1)根据自己的格式读取关卡信息,包括路线,怪物数量,类型等。

      2)读取地图。

      3)计时刷新:因为需要仿照原版游戏,我采用的是progressTimer来计时,通过读取他的百分比来判断是否开始新的一波。

      4)添加怪物:根据自己设计的逻辑添加怪物即可。

课课家教育

未登录