本文实践自 Pablo Ruiz 的文章《》,文中使用Cocos2D,我在这里使用Cocos2D-x 2.0.4进行学习和移植。在这篇文章,将会学习到如何制作一个塔防游戏。在这当中,学习如何在设定的时间内出现一波波的敌人,使这些敌人沿着指定的路点前进,如何在地图上指定的位置创建炮塔,如何使炮塔射击敌人,如何可视化调试路点和炮塔的***范围。

步骤如下:

1.新建Cocos2d-win32工程,工程名为"TowerDefense",去除"Box2D"选项,勾选"Simple Audio Engine in Cocos Denshion"选项;
2.下载本游戏所需的资源,将资源放置"Resources"目录下;
1363254115_7963.png
3.为场景添加背景图片。打开HelloWorldScene.cpp文件,修改init函数,如下:

bool HelloWorld::init(){bool bRet = false;do    {        CC_BREAK_IF(! CCLayer::init());        this->setTouchEnabled(true);        CCSize wins = CCDirector::sharedDirector()->getWinSize();        CCSprite * CCSprite::create("Bg.png");          this->addChild(background);         background->setPosition(ccp(wins.width / 2, wins.height / 2));        bRet = true;    } while (0);return bRet;}

通过放置的背景图片,可以直观的看出哪些地方允许玩家放置炮塔。编译运行,如下图所示:

1363254129_2901.png
4.接着,需要沿路设置一些点,在这些点上能够让玩家触摸和建立炮塔。为了方便管理,使用.plist文件来存储炮塔的放置点,这样就可以很容易的改变它们。TowersPosition.plist已经在资源文件夹中,其中已经有了一些炮塔的位置。查看这个文件,可以看到一个字典数组,字典只包含两个键"x"和"y"。每个字典条目代表一个炮塔位置的x和y坐标。现在需要读取这个文件,并且放置塔基到地图上。打开HelloWorldScene.h文件,添加以下变量:

1
cocos2d::CCArray* towerBases;

打开HelloWorldScene.cpp文件,添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void HelloWorld::loadTowerPositions()
{
   CCArray* towerPositions = CCArray::createWithContentsOfFile(
"TowersPosition.plist");
   towerBases = CCArray::createWithCapacity(
10);
   towerBases->retain();
   CCObject *pObject =
NULL;
   CCARRAY_FOREACH(towerPositions, pObject)
   {
       CCDictionary* towerPos = (CCDictionary*)pObject;
       CCSprite* towerBase = CCSprite::create(
"open_spot.png");
this->addChild(towerBase);
       towerBase->setPosition(ccp(((CCString*)towerPos->objectForKey(
"x"))->intValue(),
           ((CCString*)towerPos->objectForKey(
"y"))->intValue()));
       towerBases->addObject(towerBase);
   }
}

init函数里面,添加背景图片代码之后,添加如下代码:

1
this->loadTowerPositions();

在析构函数里面,添加如下代码:

1
towerBases->release();

编译运行,就可以看到道路两侧的方块,这些是做为玩家炮塔的基座。如下图所示:
1363254144_8551.png
5.开始建立炮塔。打开HelloWorldScene.h文件,添加如下代码:

1
CC_SYNTHESIZE_RETAIN(cocos2d::CCArray*, _towers, Towers);

添加Tower类,派生自CCNode类,Tower.h文件代码如下:

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
#ifndef __TOWER_H__
#define __TOWER_H__
#include
"cocos2d.h"
#include
"HelloWorldScene.h"
#define kTOWER_COST
300
class Tower :
public cocos2d::CCNode
{
public:
   Tower(
void);
   ~Tower(
void);
static Tower* nodeWithTheGame(HelloWorld* game, cocos2d::CCPoint location);
bool initWithTheGame(HelloWorld* game, cocos2d::CCPoint location);
void update(
float dt);
void draw(
void);
   CC_SYNTHESIZE(HelloWorld*, _theGame, TheGame);
   CC_SYNTHESIZE(cocos2d::CCSprite*, _mySprite, MySprite);
private:
int attackRange;
int damage;
float fireRate;
};
#endif
// __TOWER_H__

打开Tower.cpp文件,代码如下:

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
53
54
55
56
57
58
59
60
61
62
#include
"Tower.h"
using
namespace cocos2d;
Tower::Tower(
void)
{
}
Tower::~Tower(
void)
{
}
Tower* Tower::nodeWithTheGame(HelloWorld* game, CCPoint location)
{
   Tower *pRet =
new Tower();
if (pRet && pRet->initWithTheGame(game, location))
   {
return pRet;
   }
else
   {
delete pRet;
       pRet =
NULL;
return
NULL;
   }
}
bool Tower::initWithTheGame(HelloWorld* game, CCPoint location)
{
bool bRet =
false;
do
   {
       attackRange =
70;
       damage =
10;
       fireRate =
1;
       _mySprite = CCSprite::create(
"tower.png");
this->addChild(_mySprite);
       _mySprite->setPosition(location);
       _theGame = game;
       _theGame->addChild(
this);
this->scheduleUpdate();
       bRet =
true;
   }
while (
0);
return bRet;
}
void Tower::update(
float dt)
{
}
void Tower::draw(
void)
{
#ifdef COCOS2D_DEBUG
   ccDrawColor4F(
255,
255,
255,
255);
   ccDrawCircle(_mySprite->getPosition(), attackRange,
360,
30,
false);
#endif
   CCNode::draw();
}

这个Tower类包含几个属性:一个精灵对象,这是炮塔的可视化表现;一个父层的引用,方便访问父层;还有三个变量:

  • attackRange: 炮塔可以***敌人的距离。

  • damage: 炮塔对敌人造成的伤害值。

  • fireRate: 炮塔再次***敌人的时间间隔。

有了这三个变量,就可以创建各种不同***属性的炮塔,比如需要很长时间来重新加载的远程重击,或者范围有限的快速***。最后,代码中的draw方法,用于在炮塔周围绘制一个圆,以显示出它的***范围,这将方便调试。
6.让玩家添加炮塔。打开HelloWorldScene.cpp文件,加入以下头文件声明:

1
#include
"Tower.h"

在析构函数中添加如下代码:

1
_towers->release();

init函数,添加如下代码:

1
2
_towers = CCArray::create();
_towers->retain();

添加如下两个方法,代码如下:

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
bool HelloWorld::canBuyTower()
{
return
true;
}
void HelloWorld::ccTouchesBegan(CCSet *pTouches, CCEvent *pEvent)
{
   CCSetIterator iter = pTouches->begin();
for (; iter != pTouches->end(); iter++)
   {
       CCTouch* pTouch = (CCTouch*)(*iter);
       CCPoint location = pTouch->getLocation();
       CCObject *pObject =
NULL;
       CCARRAY_FOREACH(towerBases, pObject)
       {
           CCSprite *tb = (CCSprite*)pObject;
if (
this->canBuyTower() && tb->boundingBox().containsPoint(location) && !tb->getUserData())
           {
//We will spend our gold later.
               Tower* tower = Tower::nodeWithTheGame(
this, tb->getPosition());
               _towers->addObject(tower);
               tb->setUserData(tower);
           }          
       }
   }
}

方法ccTouchesBegan检测当用户触摸屏幕上任何点时,遍历towerBases数组,检查触摸点是否包含在任何一个塔基上。不过在创建炮塔前,还有两件事需要检查:

①玩家是否买得起炮塔?canBuyTower方法用来检查玩家是否有足够的金币来购买炮塔。在这里先假设玩家有很多金币,方法返回true。
②玩家是否违法了建筑规则?如果tb的UserData已经设置了,那么这个塔基已经有了炮塔,不能再添加一个新的了。
如果一切检查都通过,那么就创建一个新的炮塔,放置在塔基上,并将它添加到炮塔数组中。编译运行,触摸塔基,就可以看到炮塔放置上去了,并且它的周围还有白色的圆圈显示***范围,如下图所示:
1363254160_6894.png
7.添加路点。敌人将会沿着一系列的路点前进,这些简单相互连接的点构成了一条路径,敌人在这条路径上进行行走。敌人会出现在第一个路点,搜寻列表中的下一个路点,移动到那个位置,重复这个过程,直到他们到达列表中的最后一个路点——玩家基地。如果被敌人到达基地,那么玩家就会受到损害。添加Waypoint类,派生自CCNode类,Waypoint.h文件代码如下:

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
#ifndef __WAYPOINT_H__
#define __WAYPOINT_H__
#include
"cocos2d.h"
#include
"HelloWorldScene.h"
class Waypoint :
public cocos2d::CCNode
{
public:
   Waypoint(
void);
   ~Waypoint(
void);
static Waypoint* nodeWithTheGame(HelloWorld* game, cocos2d::CCPoint location);
bool initWithTheGame(HelloWorld* game, cocos2d::CCPoint location);
void draw(
void);
   CC_SYNTHESIZE(cocos2d::CCPoint, _myPosition, MyPosition);
   CC_SYNTHESIZE(Waypoint*, _nextWaypoint, NextWaypoint);
private:
   HelloWorld* theGame;
};
#endif
// __WAYPOINT_H__

打开Waypoint.cpp文件,代码如下:

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
53
54
55
56
57
58
59
#include
"Waypoint.h"
using
namespace cocos2d;
Waypoint::Waypoint(
void)
{
   _nextWaypoint =
NULL;
}
Waypoint::~Waypoint(
void)
{
}
Waypoint* Waypoint::nodeWithTheGame(HelloWorld* game, CCPoint location)
{
   Waypoint *pRet =
new Waypoint();
if (pRet && pRet->initWithTheGame(game, location))
   {
return pRet;
   }
else
   {
delete pRet;
       pRet =
NULL;
return
NULL;
   }
}
bool Waypoint::initWithTheGame(HelloWorld* game, CCPoint location)
{
bool bRet =
false;
do
   {
       theGame = game;
       _myPosition = location;
this->setPosition(CCPointZero);
       theGame->addChild(
this);
       bRet =
true;
   }
while (
0);
return bRet;
}
void Waypoint::draw(
void)
{
#ifdef COCOS2D_DEBUG
   ccDrawColor4F(
0,
255,
0,
255);
   ccDrawCircle(_myPosition,
6,
360,
30,
false);
   ccDrawCircle(_myPosition,
2,
360,
30,
false);
if (_nextWaypoint)
   {
       ccDrawLine(_myPosition, _nextWaypoint->_myPosition);
   }
#endif
   CCNode::draw();
}

首先,通过传入的HelloWorld对象引用和路点位置坐标,进行初始化一个waypoint对象。每个路点都包含下一个路点的引用,这将会创建一个路点链接列表。每个路点知道列表中的下一个路点。通过这种方式,可以引导敌人沿着链表上的路点到达他们的最终目的地。最后,draw方法绘制显示路点的位置,并且绘制一条直线将其与下一个路点进行连接,这仅仅用于调试目的。

8.创建路点列表。打开HelloWorldScene.h文件,添加以下代码:

1
CC_SYNTHESIZE_RETAIN(cocos2d::CCArray*, _waypoints, Waypoints);

打开HelloWorldScene.cpp文件,加入以下头文件声明:

1
#include
"Waypoint.h"

在析构函数中添加如下代码:

1
_waypoints->release();

添加以下方法:

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 HelloWorld::addWaypoints()
{
   _waypoints = CCArray::create();
   _waypoints->retain();
   Waypoint *waypoint1 = Waypoint::nodeWithTheGame(
this, ccp(
420,
35));
   _waypoints->addObject(waypoint1);
   Waypoint *waypoint2 = Waypoint::nodeWithTheGame(
this, ccp(
35,
35));
   _waypoints->addObject(waypoint2);
   waypoint2->setNextWaypoint(waypoint1);
   Waypoint *waypoint3 = Waypoint::nodeWithTheGame(
this, ccp(
35,
130));
   _waypoints->addObject(waypoint3);
   waypoint3->setNextWaypoint(waypoint2);
   Waypoint *waypoint4 = Waypoint::nodeWithTheGame(
this, ccp(
445,
130));
   _waypoints->addObject(waypoint4);
   waypoint4->setNextWaypoint(waypoint3);
   Waypoint *waypoint5 = Waypoint::nodeWithTheGame(
this, ccp(
445,
220));
   _waypoints->addObject(waypoint5);
   waypoint5->setNextWaypoint(waypoint4);
   Waypoint *waypoint6 = Waypoint::nodeWithTheGame(
this, ccp(-
40,
220));
   _waypoints->addObject(waypoint6);
   waypoint6->setNextWaypoint(waypoint5);
}

init函数,添加如下代码:

1
this->addWaypoints();

编译运行,效果如下图所示:
1363254180_6128.png

截止到目前的"塔防游戏.zip"  

在地图上有6个路点,这是敌人的行走路线。在让敌人出现在游戏中前,还需要添加一个辅助方法。打开HelloWorldScene.cpp文件,添加方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
bool HelloWorld::collisionWithCircle(CCPoint circlePoint,
float radius, CCPoint circlePointTwo,
float radiusTwo)
{
float xdif = circlePoint.x - circlePointTwo.x;
float ydif = circlePoint.y - circlePointTwo.y;
float distance = sqrt(xdif * xdif + ydif * ydif);
if(distance <= radius + radiusTwo)
   {
return
true;
   }
return
false;
}

方法collisionWithCircle用于判断两个圆是否碰撞或者相交。这将用于判断敌人是否到达一个路点,同时也可以检测敌人是否在炮塔的***范围之内。

9.添加敌人。打开HelloWorldScene.h文件,添加以下代码:

1
2
3
4
CC_SYNTHESIZE_RETAIN(cocos2d::CCArray*, _enemies, Enemies);
int wave;
cocos2d::CCLabelBMFont* ui_wave_lbl;

打开HelloWorldScene.cpp文件,在析构函数里,添加如下代码:

1
_enemies->release();

添加Enemy类,派生自CCNode类,Enemy.h文件代码如下:

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
#ifndef __ENEMY_H__
#define __ENEMY_H__
#include
"cocos2d.h"
#include
"HelloWorldScene.h"
#include
"Waypoint.h"
class Enemy :
public cocos2d::CCNode
{
public:
   Enemy(
void);
   ~Enemy(
void);
static Enemy* nodeWithTheGame(HelloWorld* game);
bool initWithTheGame(HelloWorld* game);
void doActivate(
float dt);
void getRemoved();
void update(
float dt);
void draw(
void);
   CC_SYNTHESIZE(HelloWorld*, _theGame, TheGame);
   CC_SYNTHESIZE(cocos2d::CCSprite*, _mySprite, MySprite);
private:
   cocos2d::CCPoint myPosition;
int maxHp;
int currentHp;
float walkingSpeed;
   Waypoint *destinationWaypoint;
bool active;
};
#endif
// __ENEMY_H__

打开Enemy.cpp文件,代码如下:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include
"Enemy.h"
using
namespace cocos2d;
#define HEALTH_BAR_WIDTH
20
#define HEALTH_BAR_ORIGIN -
10
Enemy::Enemy(
void)
{
}
Enemy::~Enemy(
void)
{
}
Enemy* Enemy::nodeWithTheGame(HelloWorld* game)
{
   Enemy *pRet =
new Enemy();
if (pRet && pRet->initWithTheGame(game))
   {
return pRet;
   }
else
   {
delete pRet;
       pRet =
NULL;
return
NULL;
   }
}
bool Enemy::initWithTheGame(HelloWorld* game)
{
bool bRet =
false;
do
   {
       maxHp =
40;
       currentHp = maxHp;
       active =
false;
       walkingSpeed =
0.
5;
       _theGame = game;
       _mySprite = CCSprite::create(
"enemy.png");
this->addChild(_mySprite);
       Waypoint *waypoint = (Waypoint*)_theGame->getWaypoints()->objectAtIndex(_theGame->getWaypoints()->count() -
1);
       destinationWaypoint = waypoint->getNextWaypoint();
       CCPoint pos = waypoint->getMyPosition();
       myPosition = pos;
       _mySprite->setPosition(pos);
       _theGame->addChild(
this);
this->scheduleUpdate();
       bRet =
true;
   }
while (
0);
return bRet;
}
void Enemy::doActivate(
float dt)
{
   active =
true;
}
void Enemy::getRemoved()
{
this->getParent()->removeChild(
this,
true);
   _theGame->getEnemies()->removeObject(
this);
//Notify the game that we killed an enemy so we can check if we can send another wave
   _theGame->enemyGotKilled();
}
void Enemy::update(
float dt)
{
if (!active)
   {
return;
   }
if (_theGame->collisionWithCircle(myPosition,
1, destinationWaypoint->getMyPosition(),
1))
   {
if (destinationWaypoint->getNextWaypoint())
       {
           destinationWaypoint = destinationWaypoint->getNextWaypoint();
       }
else
       {
//Reached the end of the road. Damage the player
           _theGame->getHpDamage();
this->getRemoved();
       }
   }
   CCPoint targetPoint =  destinationWaypoint->getMyPosition();
float movementSpeed = walkingSpeed;
   CCPoint normalized = ccpNormalize(ccp(targetPoint.x - myPosition.x, targetPoint.y - myPosition.y));
   _mySprite->setRotation(CC_RADIANS_TO_DEGREES(atan2(normalized.y, - normalized.x)));
   myPosition = ccp(myPosition.x + normalized.x * movementSpeed, myPosition.y + normalized.y * movementSpeed);
   _mySprite->setPosition(myPosition);
}
void Enemy::draw(
void)
{
   CCPoint healthBarBack[] = {
       ccp(_mySprite->getPosition().x -
10, _mySprite->getPosition().y +
16),
       ccp(_mySprite->getPosition().x +
10, _mySprite->getPosition().y +
16),
       ccp(_mySprite->getPosition().x +
10, _mySprite->getPosition().y +
14),
       ccp(_mySprite->getPosition().x -
10, _mySprite->getPosition().y +
14)
   };
   ccDrawSolidPoly(healthBarBack,
4, ccc4f(
255,
0,
0,
255));
   CCPoint healthBar[] = {
       ccp(_mySprite->getPosition().x + HEALTH_BAR_ORIGIN, _mySprite->getPosition().y +
16),
       ccp(_mySprite->getPosition().x + HEALTH_BAR_ORIGIN + (
float)(currentHp * HEALTH_BAR_WIDTH) / maxHp, _mySprite->getPosition().y +
16),
       ccp(_mySprite->getPosition().x + HEALTH_BAR_ORIGIN + (
float)(currentHp * HEALTH_BAR_WIDTH) / maxHp, _mySprite->getPosition().y +
14),

       ccp(_mySprite->getPosition().x + HEALTH_BAR_ORIGIN

, _mySprite->getPosition().y + 14)

   };
   ccDrawSolidPoly(healthBar,
4, ccc4f(
0,
255,
0,
255));
   CCNode::draw();
}

首先,通过传递一个HelloWorld对象引用进行初始化。在初始化函数里面,对一些重要的变量进行设置:

  • maxHP: 敌人的生命值。

  • walkingSpeed: 敌人的移动速度。

  • mySprite: 存储敌人的可视化表现。

  • destinationWaypoint: 存储下一个路点的引用。

update方法每帧都会被调用,它首先通过collisionWithCircle方法检查是否到达了目的路点。如果到达了,则前进到下一个路点,直到敌人到达终点,玩家也就受到伤害。接着,它根据敌人的行走速度,沿着一条直线移动精灵到达下一个路点。它通过以下算法:
①计算出从当前位置到目标位置的向量,然后将其长度设置为1(向量标准化)
②将移动速度乘以标准化向量,得到移动的距离,将它与当前坐标进行相加,得到新的坐标位置。
最后,draw方法在精灵上面简单的实现了一条血量条。它首先绘制一个红色背景,然后根据敌人的当前生命值用绿色进行覆盖血量条。
10.显示敌人。打开HelloWorldScene.cpp文件,添加头文件声明:

1
#include
"Enemy.h"

添加如下方法:

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
bool HelloWorld::loadWave()
{
   CCArray *waveData = CCArray::createWithContentsOfFile(
"Waves.plist");
if (wave >= waveData->count())
   {
return
false;
   }
   CCArray *currentWaveData = (CCArray*)waveData->objectAtIndex(wave);
   CCObject *pObject =
NULL;
   CCARRAY_FOREACH(currentWaveData, pObject)
   {
       CCDictionary* enemyData = (CCDictionary*)pObject;
       Enemy *enemy = Enemy::nodeWithTheGame(
this);
       _enemies->addObject(enemy);
       enemy->schedule(schedule_selector(Enemy::doActivate), ((CCString*)enemyData->objectForKey(
"spawnTime"))->floatValue());
   }
   wave++;
   ui_wave_lbl->setString(CCString::createWithFormat(
"WAVE: %d", wave)->getCString());
return
true;
}
void HelloWorld::enemyGotKilled()
{
//If there are no more enemies.
if (_enemies->count() <=
0)
   {
if (!
this->loadWave())
       {
           CCLog(
"You win!");
           CCDirector::sharedDirector()->replaceScene(CCTransitionSplitCols::create(
1, HelloWorld::scene()));
       }      
   }  
}
void HelloWorld::getHpDamage()
{
}

init函数里面,添加如下代码:

1
2
3
4
5
6
7
8
9
wave =
0;
ui_wave_lbl = CCLabelBMFont::create(CCString::createWithFormat(
"WAVE: %d", wave)->getCString(),
"font_red_14.fnt");
this->addChild(ui_wave_lbl,
10);
ui_wave_lbl->setPosition(ccp(
400, wins.height -
12));
ui_wave_lbl->setAnchorPoint(ccp(
0,
0.
5));
_enemies = CCArray::create();
_enemies->retain();
this->loadWave();

现在对上面的代码进行一些解释。最重要的部分是loadWave方法,它从Waves.plist文件读取数据。查看这个文件,可以看到它包含了3个数组,每个数组代表着一波敌人。第一个数组包含6个字典,每个字典定义了一个敌人。在本篇文章中,这个字典仅存储敌人的出现时间,但是也可用于定义敌人类型或者其他特殊属性,以区分不同的敌人。loadWave方法检查下一波应出现的敌人,根据波信息创建相应的敌人,并安排它们在规定的时间出现在屏幕上。enemyGotKilled方法检查当前屏幕上的敌人数量,如果已经没有敌人的话,那么就让下一波敌人出现。之后,还使用这个方法来判断玩家是否赢得了游戏。编译运行,敌人正向玩家基地前进,如下图所示:

1363254240_8329.png
10.炮塔***。每座塔进行检查是否有敌人出现在***范围之内,如果有的话,对敌人进行开火,直到以下两种情况之一发生:敌人移动出范围;敌人被消灭。那么炮塔就会寻找下一个敌人。打开Tower.h文件,添加以下代码:

1
class Enemy;

添加以下变量:

1
2
bool attacking;
Enemy *chosenEnemy;

打开Tower.cpp文件,添加头文件声明:

1
#include
"Enemy.h"

initWithTheGame函数开头if条件之后,添加如下代码:

1
chosenEnemy =
NULL;

添加以下方法:

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
53
54
55
56
57
58
void Tower::attackEnemy()
{
this->schedule(schedule_selector(Tower::shootWeapon), fireRate);
}
void Tower::chosenEnemyForAttack(Enemy *enemy)
{
   chosenEnemy =
NULL;
   chosenEnemy = enemy;
this->attackEnemy();
   enemy->getAttacked(
this);
}
void Tower::shootWeapon(
float dt)
{
   CCSprite *bullet = CCSprite::create(
"bullet.png");
   _theGame->addChild(bullet);
   bullet->setPosition(_mySprite->getPosition());
   bullet->runAction(CCSequence::create(
       CCMoveTo::create(
0.
1, chosenEnemy->getMySprite()->getPosition()),
       CCCallFunc::create(
this, callfunc_selector(Tower::damageEnemy)),
       CCCallFuncN::create(
this, callfuncN_selector(Tower::removeBullet)),
NULL));
}
void Tower::removeBullet(CCSprite *bullet)
{
   bullet->getParent()->removeChild(bullet,
true);
}
void Tower::damageEnemy()
{
if (chosenEnemy)
   {
       chosenEnemy->getDamaged(damage);
   }
}
void Tower::targetKilled()
{
if (chosenEnemy)
   {
       chosenEnemy =
NULL;
   }
this->unschedule(schedule_selector(Tower::shootWeapon));
}
void Tower::lostSightOfEnemy()
{
   chosenEnemy->gotLostSight(
this);
if (chosenEnemy)
   {
       chosenEnemy =
NULL;
   }
this->unschedule(schedule_selector(Tower::shootWeapon));
}

最后,更新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 Tower::update(
float dt)
{
if (chosenEnemy)
   {
//We make it turn to target the enemy chosen
       CCPoint normalized = ccpNormalize(ccp(chosenEnemy->getMySprite()->getPosition().x - _mySprite->getPosition().x,
           chosenEnemy->getMySprite()->getPosition().y - _mySprite->getPosition().y));
       _mySprite->setRotation(CC_RADIANS_TO_DEGREES(atan2(normalized.y, - normalized.x)) +
90);
if (!_theGame->collisionWithCircle(_mySprite->getPosition(), attackRange, chosenEnemy->getMySprite()->getPosition(),
1))
       {
this->lostSightOfEnemy();
       }
   }
else
   {
       CCObject *pObject =
NULL;
       CCARRAY_FOREACH(_theGame->getEnemies(), pObject)
       {
           Enemy *enemy = (Enemy*)pObject;
if (_theGame->collisionWithCircle(_mySprite->getPosition(), attackRange, enemy->getMySprite()->getPosition(),
1))
           {
this->chosenEnemyForAttack(enemy);
break;
           }          
       }
   }
}

打开Enemy.h文件,添加以下代码:

1
cocos2d::CCArray *attackedBy;

打开Enemy.cpp文件,在initWithTheGame函数开头if条件之后,添加如下代码:

1
2
attackedBy = CCArray::createWithCapacity(
5);
attackedBy->retain();

getRemoved函数开头,添加如下代码:

1
2
3
4
5
6
CCObject *pObject =
NULL;
CCARRAY_FOREACH(attackedBy, pObject)
{
   Tower *attacker = (Tower*)pObject;
   attacker->targetKilled();
}

添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Enemy::getAttacked(Tower* attacker)
{
   attackedBy->addObject(attacker);
}
void Enemy::gotLostSight(Tower* attacker)
{
   attackedBy->removeObject(attacker);
}
void Enemy::getDamaged(
int damage)
{
   currentHp -= damage;
if (currentHp <=
0)
   {
this->getRemoved();
   }
}

代码中最重要的部分是在Tower类的update方法。炮塔不断检查敌人是否在***范围内,如果是的话,炮塔将旋转朝向敌人,开火***。一个敌人一旦被标记为被***,将会调用方法让炮塔以***间隔发射×××。反过来,每个敌人都存储有向其***的炮塔列表,所以如果敌人被杀死了,那么炮塔就会被通知停止***。编译运行,放置几个炮塔在地图上,将会看到一旦敌人进入炮塔的***范围,炮塔就会向它们开火***,敌人的血量条就会减少,直到被消灭。如下图所示:

1363254317_9812.gif
11.显示玩家血量。打开HelloWorldScene.h文件,添加以下代码:

1
2
3
int playerHp;
cocos2d::CCLabelBMFont *ui_hp_lbl;
bool gameEnded;

变量playerHp表示玩家的生命值,CCLabelBMFont对象是一个标签,用来显示生命数值。gameEnded用来表示游戏是否结束。打开HelloWorldScene.cpp文件,在init函数里面,添加如下代码:

1
2
3
4
5
gameEnded =
false;
playerHp =
5;
ui_hp_lbl = CCLabelBMFont::create(CCString::createWithFormat(
"HP: %d", playerHp)->getCString(),
"font_red_14.fnt");
this->addChild(ui_hp_lbl,
10);
ui_hp_lbl->setPosition(ccp(
35, wins.height -
12));

添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void HelloWorld::getHpDamage()
{
   playerHp--;
   ui_hp_lbl->setString(CCString::createWithFormat(
"HP: %d", playerHp)->getCString());
if (playerHp <=
0)
   {
this->doGameOver();
   }
}
void HelloWorld::doGameOver()
{
if (!gameEnded)
   {
       gameEnded =
true;
       CCDirector::sharedDirector()->replaceScene(CCTransitionRotoZoom::create(
1, HelloWorld::scene()));
   }
}

添加的方法为减少玩家生命值,更新标签,并检查玩家生命是否耗尽,如果是的话,游戏就结束了。当敌人到达基地的时候,getHpDamage方法被调用。编译运行,让敌人到达基地,你将会看到玩家的生命在减少,直到游戏失败。如下图所示:

1363254337_8524.png
12.限制金币供应量。大多数游戏都实现了“零和”功能,建造每座炮塔需要一定的资源,并给玩家有限的资源进行分配。打开HelloWorldScene.h文件,添加如下代码:

1
2
int playerGold;
cocos2d::CCLabelBMFont *ui_gold_lbl;

就像显示生命数值一样,一个变量表示玩家的金币数,一个标签对象显示金币数值。打开HelloWorldScene.cpp文件,在init函数里面,添加如下代码:

1
2
3
4
5
playerGold =
1000;
ui_gold_lbl = CCLabelBMFont::create(CCString::createWithFormat(
"GOLD: %d", playerGold)->getCString(),
"font_red_14.fnt");
this->addChild(ui_gold_lbl,
10);
ui_gold_lbl->setPosition(ccp(
135, wins.height -
12));
ui_gold_lbl->setAnchorPoint(ccp(
0,
0.
5));

添加如下方法:

1
2
3
4
5
void HelloWorld::awardGold(
int gold)
{
   playerGold += gold;
   ui_gold_lbl->setString(CCString::createWithFormat(
"GOLD: %d", playerGold)->getCString());
}

替换canBuyTower方法,代码如下:

1
2
3
4
5
6
7
8
bool HelloWorld::canBuyTower()
{
if (playerGold - kTOWER_COST >=
0)
   {
return
true;
   }
return
false;
}

ccTouchesBegan函数里面,语句//We will spend our gold later.的后面,添加如下代码:

1
2
playerGold -= kTOWER_COST;
ui_gold_lbl->setString(CCString::createWithFormat(
"GOLD: %d", playerGold)->getCString());

上述的代码在玩家尝试放置炮塔时,检查是否有足够的金币。如果足够的话,炮塔就会放置上去,并从玩家的金币数中减去炮塔的费用。每次杀死敌人的时候也应该奖励玩家一些金币。打开Enemy.cpp文件,在getDamaged函数里面,if条件后面,添加如下语句:

1
_theGame->awardGold(
200);

编译运行,会看到不能随意的放置炮塔了,因为每个炮塔都要花费金币。当然,杀死敌人就可以获得金币奖励,这样就可以继续购买炮塔。这是一个很好的系统。如下图所示:
1363254363_7800.png
13.加入背景音乐和音效。打开HelloWorldScene.cpp文件,添加头文件声明:

1
#include
"SimpleAudioEngine.h"

init函数,if条件之后,添加如下代码:

1
CocosDenshion::SimpleAudioEngine::sharedEngine()->playBackgroundMusic(
"8bitDungeonLevel.mp3",
true);

ccTouchesBegan函数,添加一个新的Tower对象前,添加如下代码:

1
CocosDenshion::SimpleAudioEngine::sharedEngine()->playEffect(
"tower_place.wav");

getHpDamage函数里,添加如下代码:

1
CocosDenshion::SimpleAudioEngine::sharedEngine()->playEffect(
"life_lose.wav");

打开Enemy.cpp文件,添加头文件声明:

1
#include
"SimpleAudioEngine.h"

getDamaged函数里,添加如下代码:

1
CocosDenshion::SimpleAudioEngine::sharedEngine()->playEffect(
"laser_shoot.wav");

编译运行,现在游戏将有配乐,关闭掉调试绘制后,效果图:
1363254648_4871.gif

参考资料:

1.How To Make a Tower Defense Game  
2.钓龟岛保卫战-如何从零开始制作一款iOS塔防游戏(新)  
非常感谢以上资料,本例子源代码附加资源下载地址

iOS代码例子:"塔防游戏2.zip"  

如文章存在错误之处,欢迎指出,以便改正。