1. 程式人生 > >如何製作一個塔防遊戲 Cocos2d-x 2 0 4

如何製作一個塔防遊戲 Cocos2d-x 2 0 4

      本文實踐自 Pablo Ruiz 的文章《How To Make a Tower Defense Game》,文中使用Cocos2D,我在這裡使用Cocos2D-x 2.0.4進行學習和移植。在這篇文章,將會學習到如何製作一個塔防遊戲。在這當中,學習如何在設定的時間內出現一波波的敵人,使這些敵人沿著指定的路點前進,如何在地圖上指定的位置建立炮塔,如何使炮塔射擊敵人,如何視覺化除錯路點和炮塔的攻擊範圍。

步驟如下:
1.新建Cocos2d-win32工程,工程名為"TowerDefense",去除"Box2D"選項,勾選"Simple Audio Engine in Cocos Denshion

"選項;
2.下載本遊戲所需的資源,將資源放置"Resources"目錄下;

3.為場景新增背景圖片。開啟HelloWorldScene.cpp檔案,修改init函式,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  bool HelloWorld::init()
{
     bool bRet =  false;
     do 
    {
        CC_BREAK_IF(! CCLayer::init());
        
         this
->setTouchEnabled( true);
        CCSize wins = CCDirector::sharedDirector()->getWinSize();
        CCSprite *background = CCSprite::create( "Bg.png");
         this->addChild(background);
        background->setPosition(ccp(wins.width /  2, wins.height /  2));

        bRet =  true;
    }  while ( 0);
     return bRet;
}

通過放置的背景圖片,可以直觀的看出哪些地方允許玩家放置炮塔。編譯執行,如下圖所示:

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();
編譯執行,就可以看到道路兩側的方塊,這些是做為玩家炮塔的基座。如下圖所示:

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( 255255255255);
    ccDrawCircle(_mySprite->getPosition(), attackRange,  36030false);
#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已經設定了,那麼這個塔基已經有了炮塔,不能再新增一個新的了。
如果一切檢查都通過,那麼就建立一個新的炮塔,放置在塔基上,並將它新增到炮塔陣列中。編譯執行,觸控塔基,就可以看到炮塔放置上去了,並且它的周圍還有白色的圓圈顯示攻擊範圍,如下圖所示:

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( 02550255);
    ccDrawCircle(_myPosition,  636030false);
    ccDrawCircle(_myPosition,  236030false);

     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( 42035));
    _waypoints->addObject(waypoint1);

    Waypoint *waypoint2 = Waypoint::nodeWithTheGame( this, ccp( 3535));
    _waypoints->addObject(waypoint2);
    waypoint2->setNextWaypoint(waypoint1);

    Waypoint *waypoint3 = Waypoint::nodeWithTheGame( this, ccp( 35130));
    _waypoints->addObject(waypoint3);
    waypoint3->setNextWaypoint(waypoint2);

    Waypoint *waypoint4 = Waypoint::nodeWithTheGame( this, ccp( 445130));
    _waypoints->addObject(waypoint4);
    waypoint4->setNextWaypoint(waypoint3);

    Waypoint *waypoint5 = Waypoint::nodeWithTheGame( this, ccp( 445220));
    _waypoints->addObject(waypoint5);
    waypoint5->setNextWaypoint(waypoint4);

    Waypoint *waypoint6 = Waypoint::nodeWithTheGame( this, ccp(- 40220));
    _waypoints->addObject(waypoint6);
    waypoint6->setNextWaypoint(waypoint5);
}

init函式,新增如下程式碼:

1   this->addWaypoints();
編譯執行,效果如下圖所示:

在地圖上有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( thistrue);
    _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( 25500255));

    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( 02550255));

    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);