上面3張圖是完成後的效果圖
遊戲已完成,除了英雄外,基本還原了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<std::vector<Vector<GroupMonster*>>> waveVector;
//存儲敵人路線
std::vector<std::vector<std::vector<Point>>> path;
//下一波敵人提示(玩過遊戲的人知道就是那個一閃一閃的骷髏頭)
Vector<WaveFlag*> waveFlags;
void addWaveProgressBars(std::vector<Point> 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格式,用文件的形式來保存每一關的信息
<dict>
<key>data</key>
<array>
<dict>
<key>gold</key>
<string>500</string>
<key>life</key>
<string>20</string>
<key>wave</key>
<string>3</string>
</dict>
</array>
<key>monsters</key>
<array>
<array>
<array>
<dict>
<key>path</key>
<string>0</string>
<key>road</key>
<string>0</string>
<key>type</key>
<string>0</string>
</dict>
<dict>
<key>path</key>
<string>2</string>
<key>road</key>
<string>1</string>
<key>type</key>
<string>0</string>
</dict>
<dict>
<key>path</key>
<string>0</string>
<key>road</key>
<string>2</string>
<key>type</key>
<string>-1</string>
</dict>
<dict>
<key>path</key>
<string>1</string>
<key>road</key>
<string>3</string>
<key>type</key>
<string>-1</string>
</dict>
</array>
<span style="white-space:pre"> </span></array>
<span style="white-space:pre"> </span></array>
第一個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::loadAndSetLevelData()
{
//加載初始血量金錢等
auto dataDic = Dictionary::createWithContentsOfFile(String::createWithFormat("level%d_%d_monsters.plist", getLevel(),difficulty)->getCString());
auto data_array = dynamic_cast<__Array*>(dataDic->objectForKey("data"));
auto data_tempDic = dynamic_cast<__Dictionary*>(data_array->getObjectAtIndex(0));
startGold = dynamic_cast<__String*>(data_tempDic->objectForKey("gold"))->intValue();
maxLife = dynamic_cast<__String*>(data_tempDic->objectForKey("life"))->intValue();
maxWave = dynamic_cast<__String*>(data_tempDic->objectForKey("wave"))->intValue();
//加載怪物數據
auto monsters_array = dynamic_cast<__Array*>(dataDic->objectForKey("monsters"));
for(int i =0 ;i < monsters_array->count();i++)
{
auto monster_array = dynamic_cast<__Array*>(monsters_array->getObjectAtIndex(i));
std::vector<Vector<GroupMonster*>> thisTimeMonster;
for(int j =0;j<monster_array->count();j++)
{
auto tempArray = dynamic_cast<__Array*>(monster_array->getObjectAtIndex(j));
Vector<GroupMonster*> monsterVector;
for(int k =0;k<tempArray->count();k++)
{
auto tempDic = dynamic_cast<__Dictionary*>(tempArray->getObjectAtIndex(k));
monsterVector.pushBack(GroupMonster::initGroupEnemy(
dynamic_cast<__String*>(tempDic->objectForKey("type"))->intValue(),
dynamic_cast<__String*>(tempDic->objectForKey("road"))->intValue(),
dynamic_cast<__String*>(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<__Array*>(plistDic->objectForKey("paths"));
for(int i = 0;i<path_array->count();i++)
{
std::vector<std::vector<Point>> tempPathVector;
auto path_array2 = dynamic_cast<__Array*>(path_array->getObjectAtIndex(i));
for(int j = 0;j<path_array2->count();j++)
{
std::vector<Point> tempRandomPathVector;
auto path_array3 = dynamic_cast<__Array*>(path_array2->getObjectAtIndex(j));
for(int k =0;k<path_array3->count();k++)
{
auto tempDic = dynamic_cast<__Dictionary*>(path_array3->getObjectAtIndex(k));
Point tempPath = Point();
tempPath.x = dynamic_cast<__String*>(tempDic->objectForKey("x"))->floatValue()*1.15;
tempPath.y = dynamic_cast<__String*>(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<std::vector<std::vector<Point>>> 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;i<waveFlags.size();i++){
if(waveFlags.at(i)->getWavePercentage() == 100.0f){
if(wave<maxWave)
{
isStart = true;
SoundManager::playIncomingWave();
wave ++;
for(int i = 0;i<waveFlags.size();i++){
waveFlags.at(i)->setWavePercentage(0.0f);
}
playerState->setWave(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 ;i<waveVector.at(wave).at(time).size();i++)
{
auto monsterData = waveVector.at(wave).at(time).at(i);
switch (monsterData->getType())
{
case(0):{
auto thug = Thug::createMonster(path.at(monsterData->getRoad()).at(monsterData->getPath()));
addChild(thug);
GameManager::getInstance()->monsterVector.pushBack(thug);}
break;
<span style="white-space:pre"> </span>default:
break;
<span style="white-space:pre"> </span> <span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>}
<span style="white-space:pre"> </span>}
}
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即還不是最後一波時
延遲15S將waveFlag執行上述restart,重新開始計時
若是最後一波,將IsEnd標記置爲true,等待結束畫面
主要流程就是
1根據自己的格式讀取關卡信息,包括路線,怪物數量,類型等
2讀取地圖
3計時刷新
因爲需要仿照原版遊戲,我採用的是progressTimer來計時,通過讀取他的百分比來判斷是否開始新的一波
4添加怪物
根據自己設計的邏輯添加怪物即可
地圖類第一張就差不多了,下一章將講解觸摸層