如何製作一個塔防遊戲 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 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; |
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); |
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: 炮塔再次攻擊敵人的時間間隔。
6.讓玩家新增炮塔。開啟 HelloWorldScene.cpp檔案,加入以下標頭檔案宣告:
1 | #include "Tower.h" |
1 | _towers->release(); |
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__ |
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(); } |
8.建立路點列表。開啟HelloWorldScene.h檔案,新增以下程式碼:
1 | CC_SYNTHESIZE_RETAIN(cocos2d::CCArray*, _waypoints, Waypoints); |
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(); |
在地圖上有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(); |
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__ |
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: 儲存下一個路點的引用。
①計算出從當前位置到目標位置的向量,然後將其長度設定為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); |