原文:http://www.cnblogs.com/cocos2d-x/archive/2012/06/03/2532932.html
在上一節中,我們使用經典FC遊戲《坦克大戰》的元素設計了一張地圖,來演示Tiled Map Editor工具的基本用法,並在cocos2d-x程序中完成了tmx地圖加載、查看以及動態修改地圖元素的功能。
這一節,我們將對示例5進一步擴充、完善,使其成爲能“玩”一下的遊戲。
爲了這個目標,我們需要做以下調整:
1)增加雙方坦克,我方一輛,敵方八輛。
2)坦克在地圖上行走時,需要完成基本的碰撞檢測,不可以穿牆而過。
3)坦克可以開炮,炮彈可以摧毀磚牆和敵對方坦克。
4)視角鎖定在我方坦克上,顯示區域跟隨我方坦克的移動而改變。
5)敵方(電腦)坦克擁有基本的AI來增加點兒樂趣。
增加坦克
我們的目標只是一個簡單的示例遊戲,所以我準備用同一個類來表示雙方坦克,通過一個布爾型變量來區分敵我。
1 class TankSprite : public CCSprite 2 { 3 public: 4 TankSprite(void); 5 virtual ~TankSprite(void); 6 7 bool bIsEnemy; 8 }
在地圖中漫遊
作爲一個“完整”的遊戲,我們需要實現在遊戲地圖中漫遊的功能。這包含兩方面的內容,下面我們來分別描述一下。
1)坦克精靈在地圖上的移動
遊戲講究一個代入感。是誰在玩遊戲?不是上帝,我只是一輛小小的坦克。當操作指令下達時,應該是一輛坦克去完成它,而不是通過上帝的眼睛來觀察。所以,現在需要完成的功能就是讓坦克在tmx地圖上移動。
要實現這樣的功能並不困難,因爲CCSprite精靈類爲我們準備好的大部分內容。我們要做的只是爲坦克增加當前的狀態、移動速度以及四個方向的移動函數。
1 // TankAction 是一個枚舉類型,用來表示坦克當期的狀態 2 TankAction kAct; 3 float moveStep; 4 void moveLeft(GameLayer *layer); 5 void moveRight(GameLayer *layer); 6 void moveUp(GameLayer *layer); 7 void moveDown(GameLayer *layer);
在實現四方向移動函數時,因爲坦克是tmx地圖的子節點,所以座標不需要太多計算。
1 void TankSprite::moveLeft(GameLayer *layer) 2 { 3 kAct = kLeft; 4 setRotation(-90.0f); 5 CCPoint tankPos = getPosition(); 6 tankPos.x -= moveStep; 7 CCSize tankSize = getTextureRect().size; 8 9 // 越界檢測 10 if (tankPos.x - layer->mapX < tankSize.width / 2) 11 setPosition(ccp(tankSize.width / 2, tankPos.y)); 12 else 13 setPosition(tankPos); 14 }
2)視口跟隨
完成上面的功能,坦克就可以在地圖上行走了。但是我們只能看見屏幕大小的地圖,坦克很容易就走到屏幕之外去了。我們不願意做一隻“井底之蛙”,眼睛要跟上坦克。
我們前面介紹過視口這個概念,它就是整個遊戲世界向我們打開的一扇窗子。而且,在示例5中我們也嘗試了移動視口來觀察整個遊戲地圖。我們現在要做的就是加一個視口跟隨的功能,讓視口跟隨主角(我方)坦克車移動,將合適的地圖區域展示給我們。
縱觀大多數人的設計,對於視口跟隨,一個普遍的做法是這樣的。
大部分時間都將主角精靈固定在視口中心,主角的移動是通過反方向移動背景地圖模擬的。只有當視口已經到達地圖邊緣,再沒有更多地圖供我們移動時,才移動主角本身。
句子有點兒拗口,沒理解的朋友請看下面的演示。
將上面的過程寫成代碼:
1 void GameLayer::setWorldPosition(void) 2 { 3 CCSize tankSize = tank->getTextureRect().size; 4 CCPoint tankPos = tank->getPosition(); 5 6 if (tankPos.x < (screenWidth - tankSize.width) / 2) 7 mapX = 0; 8 else if (tankPos.x > gameWorldWidth() - screenWidth / 2) 9 mapX = -gameWorldWidth(); 10 else 11 mapX = -(tankPos.x - screenWidth / 2 + tankSize.width / 2); 12 13 if (tankPos.y < (screenHeight - tankSize.height) / 2) 14 mapY = 0; 15 else if (tankPos.y > gameWorldHeight() - screenHeight / 2) 16 mapY = -gameWorldHeight(); 17 else 18 mapY = -(tankPos.y - screenHeight / 2 + tankSize.height / 2); 19 20 // 越界復位 21 if (mapX > 0) 22 mapX = 0; 23 if (mapY > 0) 24 mapY = 0; 25 if (mapX < -(gameWorldWidth() - screenWidth)) 26 mapX = -(gameWorldWidth() - screenWidth); 27 if (mapY < -(gameWorldHeight() - screenHeight)) 28 mapY = -(gameWorldHeight() - screenHeight); 29 gameWorld->setPosition(mapX, mapY); 30 }
碰撞檢測
坦克也不是說是無堅不摧的,所以遇到河啊什麼的,還是從橋上走方便一些。所以我們就得判斷是不是沒路了,是不是撞牆了,於是“碰撞檢測”的概念就引入進來了。
試想,有8輛敵方坦克正在地圖上游蕩,他們撞牆要檢測,互相之間也要檢測,他們偶爾還會發射炮彈,每個炮彈的飛行和爆炸也都需要檢測。而且這些都是併發進行的。哇!多麼混亂的一個場面啊!
呵呵,其實“碰撞檢測”並沒有大家想的那麼複雜,不是說要做碰撞檢測就要弄個物理引擎進來,只要不是在實現逼真的物理效果,我們完全可以用自己的方法來檢測碰撞。
大家知道,什麼同時啊,並行啊,這些統統都是理論上的,或者說是在一定精度範圍上的東西。即便是多核CPU,在共享同一資源時,它們也要分優先級的。所以,在小遊戲這類簡單的應用上,我們完全可以認爲,一次碰撞發生時,世界是靜止的。
這樣一來,我們只需要按照一定的優先級,順次爲每一個需要檢測碰撞的對象進行檢測即可。
對於坦克的行走來說,我們需要做的檢測只有地形一個因素,又因爲是在同一座標系下,事情灰常簡單。
考慮到坦克是有體積的,這裏取3個採樣點檢測碰撞,以避免其邊緣進入牆裏。
1 // 這是坦克左移時的碰撞檢測代碼 2 CCPoint nextPos = ccp(tankPos.x - tankSize.width / 2, tankPos.y); 3 unsigned int tid = layer->tileIDFromPosition(nextPos); 4 if (tid != 4) 5 return; 6 nextPos = ccp(tankPos.x - tankSize.width / 2, tankPos.y + tankSize.height / 4); 7 tid = layer->tileIDFromPosition(nextPos); 8 if (tid != 4) 9 return; 10 nextPos = ccp(tankPos.x - tankSize.width / 2, tankPos.y - tankSize.height / 4); 11 tid = layer->tileIDFromPosition(nextPos); 12 if (tid != 4) 13 return;
開火射擊
瞧,那輛破坦克搖頭擺尾地開過來了,讓我們幹掉他。Fire! BOOM...
好吧,既然你想開火,那就給每輛坦克都配上炮彈吧。
1 BulletSprite* BulletSprite::bulletWithLayer(GameLayer *layer) 2 { 3 BulletSprite *sprite = new BulletSprite(); 4 if (sprite && sprite->initWithFile("bullet.png", CCRectMake(0, 0, 16, 16))) 5 { 6 sprite->autorelease(); 7 layer->gameWorld->addChild(sprite, 100); 8 sprite->setIsVisible(false); 9 sprite->setGameLayer(layer); 10 return sprite; 11 } 12 CC_SAFE_DELETE(sprite); 13 return NULL; 14 }
雖然炮彈是坦克的配備,但是爲了方便座標計算,我們將炮彈歸爲地圖的子節點。換個思路想,炮彈發射後就跟坦克分離了,所以我們這麼設計也是可以接受的。
理所當然的,我們還需要爲每一輛坦克增加一個開火按鈕。
1 void TankSprite::onFire(GameLayer *layer) 2 { 3 CCPoint tankPos = getPosition(); 4 CCSize tankSize = getTextureRect().size; 5 CCPoint bulletPos, bulletTarget; 6 switch (kAct) 7 { 8 case kUp: 9 bulletPos = ccp(tankPos.x, tankPos.y + tankSize.height / 2); 10 bulletTarget = ccp(bulletPos.x, bulletPos.y + 1024); 11 break; 12 case kDown: 13 bulletPos = ccp(tankPos.x, tankPos.y - tankSize.height / 2); 14 bulletTarget = ccp(bulletPos.x, bulletPos.y - 1024); 15 break; 16 case kLeft: 17 bulletPos = ccp(tankPos.x - tankSize.width / 2, tankPos.y); 18 bulletTarget = ccp(bulletPos.x - 1024, bulletPos.y); 19 break; 20 case kRight: 21 bulletPos = ccp(tankPos.x + tankSize.width / 2, tankPos.y); 22 bulletTarget = ccp(bulletPos.x + 1024, bulletPos.y); 23 break; 24 default: 25 break; 26 } 27 bullet->setPosition(bulletPos); 28 bullet->setIsVisible(true); 29 CCShow *ac1 = CCShow::action(); 30 CCMoveTo *ac2 = CCMoveTo::actionWithDuration(5.0f, bulletTarget); 31 bullet->runAction(CCSequence::actions(ac1, ac2, NULL)); 32 // 啓動炮彈的碰撞檢測 33 this->schedule(schedule_selector(TankSprite::checkExplosion), 1.0f / 30.0f); 34 }
大家可以看到,炮彈發射是用系統內置的MoveTo動作模擬的,最後一行開啓每秒30次的炮彈碰撞檢測。檢測的方法與行走時的檢測類似,這裏不再重複。
小結
至此,一個簡單的坦克大戰演示就初步完成了。爲了給大家一個更形象的認識,上一張層結構示意圖。
本章示例代碼基於cocos2d-1.0.1-x-0.13.0-beta編寫。雖然cocos2d-2.0-rc0a-x-2.0已經發布了,但是變動比較大,主要是得換模板嚮導,我比較懶,所以本系列結束之前,我先不換版本了。
下載地址
http://files.cnblogs.com/cocos2d-x/ZYG006.rar