2016年,與我而言是不平凡的一年,甚至是煎熬的一年,個中滋味,唯有自己體會.
2017如期而至,和過去的自己告別,開始新的生活.
關(guān)于項目(代碼下載鏈接,最底部點擊GitHub)
本次開源項目為Flappy Bird升級版,支持單人游戲,雙人對戰(zhàn)游戲,死亡復(fù)活,以及多種樣式的水管.大大增加了游戲的可玩性.
開發(fā)難度也相對簡單,適合對cocos2d-x以及C++有一定了解的朋友.
項目使用c++,基于cocos2d-x-x引擎開發(fā),開發(fā)工具為Xcode8.1,支持Android,iOS以及wp系統(tǒng).
項目導(dǎo)入方式:創(chuàng)建一個大于3.0版本的cocos工程,將Class
目錄以及Resources
目錄替換為下載代碼里的倆個文件夾.
除了Xcode以使用其他開發(fā)工具開會報錯,將項目里倆個.mm
后綴的文件改為.cpp
并且刪除#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
后的代碼即可.
cocos2d-x號稱一套代碼可多平臺運行,但實際開發(fā)中,還是會針對不同平臺做相應(yīng)的處理,如調(diào)用各平臺自帶的api等,iOS里的GameCenter以及按需求調(diào)用不同平臺的控件等等,筆者采用Xcode開發(fā)的,為了調(diào)用iOS的api做了相應(yīng)的修改,使用其他開發(fā)工具的同學(xué)修改回來即可.
選擇游戲模式場景(SelectPlayer)
這個場景比較簡單,由倆個Button,背景Sprite,移動的陸地,標題以及小鳥構(gòu)成.效果如下圖:
Button的創(chuàng)建
由于Button是cocos GUI中擴展的類,使用前需導(dǎo)入ui/CocosGUI.h
,并且引入using namespace ui
的命名空間.
// 設(shè)置button的圖片
auto btn = Button::create(pressImageName, "", "", TextureResType::PLIST);
auto btnW = btn->getContentSize().width;
auto margin = kWinSizeWidth * 0.02;
btn->setAnchorPoint(Vec2(0, 0));
btn->setTag(tag);
btn->cocos2d::Node::setPosition(tag == 10 ? kWinSizeWidth * 0.5 - btnW -margin : kWinSizeWidth * 0.5 + margin, postion.y);
btn->addTouchEventListener(CC_CALLBACK_2(SelectPlayer::buttonTouchCallback, this));
addChild(btn);
TextureResType作用
- 在游戲開發(fā)過程中,對于圖片的需求量會很大.為了解決大量圖片造成安裝包過大的問題.UI人員通常都會將很多張圖片合成一張pvr.ccz格式或者png格式的圖片,并配有一個與之對應(yīng)的Plist文件用于讀取不同圖片的Rect.如果圖片采用了上面的方式,需要將TextureResType設(shè)置為PLIST.如果是單獨的一張圖片,設(shè)為LOCAL即可.
給Button添加點擊事件,并且設(shè)置Button的Tag,以便在Button的點擊事件中判斷進入單人模式還是對戰(zhàn)模式.代碼如下
void SelectPlayer::buttonTouchCallback(Ref *sender, Widget::TouchEventType type)
{
if (type == Widget::TouchEventType::ENDED) {
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("swoosh.caf");
auto btn = static_cast<Button *>(sender);
auto type = OnePlayer;
if (btn->getTag() == 10) {
type = OnePlayer;
} else if (btn->getTag() == 11) {
type = TwoPlayer;
}
// cut to GameScene
Director::getInstance()->replaceScene(TransitionFade::create(0.25, Game::createScene(type), Color3B(255, 255, 255)));
}
}
標題和Bird以及陸地的動畫效果
標題與Bird都是采用Sprite加載的圖片緩存,放到對應(yīng)的位置,Land為單獨封裝的類,重寫Layer的onEnterTransitionDidFinish
,實現(xiàn)以下代碼執(zhí)行對應(yīng)的動畫.
void SelectPlayer::onEnterTransitionDidFinish()
{
Layer::onEnterTransitionDidFinish();
// floor move animation
float durtion = 4;
auto move = MoveTo::create(durtion, Vec2(-kWinSizeWidth, 0));
auto moveEnd = CallFuncN::create([this](Node *){
land->setPosition(Vec2(0, 0));
});
land->runAction(RepeatForever::create(Sequence::create(move, moveEnd, NULL)));
// bird fly anim
float durtion1 = 1.5;
auto moveUp = MoveTo::create(durtion1, Vec2(title->getPosition().x, title->getPosition().y + 15));
auto moveDown = MoveTo::create(durtion1, Vec2(title->getPosition().x, title->getPosition().y - 15));
title->runAction(RepeatForever::create(Sequence::create(moveUp, moveDown, NULL)));
auto birdMoveUp = MoveTo::create(durtion1, Vec2(bird->getPosition().x, bird->getPosition().y + 15));
auto bireMoveDown = MoveTo::create(durtion1, Vec2(bird->getPosition().x, bird->getPosition().y - 15));
auto birdMove = Sequence::create(birdMoveUp, bireMoveDown, NULL);
cocos2d::Vector<SpriteFrame *>frams;
for (int i = 0; i < 3; i++) {
char name[30];
sprintf(name, "fly_0_%d.png", i);
auto sf = SpriteFrameCache::getInstance()->getSpriteFrameByName(name);
frams.pushBack(sf);
}
Animation *birdA = Animation::createWithSpriteFrames(frams);
birdA->setDelayPerUnit(0.1);
birdA->setRestoreOriginalFrame(true);
Animate *anma = Animate::create(birdA);
bird->runAction(RepeatForever::create(anma));
bird->runAction(RepeatForever::create(birdMove));
}
場景切換的動畫效果
cocos2d-x中提供了30多種場景切換的動畫,使用起來非常方便,文檔中注釋的也很清楚,這里就不一一介紹動畫效果了,開發(fā)者可根據(jù)項目的需求自行選擇.
當然,有時候框架內(nèi)自帶的轉(zhuǎn)場效果會無法滿足需求.也是可以自定義轉(zhuǎn)場動畫效果的.需要自定義一個繼承至Scene的類當做中間的動畫即可,如有需求的朋友隨意點開一個場景切換的源代碼進行閱讀,原理比較簡單.如有問題可以私信我交流.
游戲場景(Game)
Game場景中提供了倆種游戲模式,一種是單人游戲,場景效果如下圖
對戰(zhàn)模式,場景效果如下圖
通過圖像可以對比出,倆種模式有許多相同元素,定義一個游戲模式枚舉PlayerType
,并且給在創(chuàng)建場景的時傳入.
通過判斷不同游戲模式,區(qū)分添加不同的元素.邏輯并不復(fù)雜,這里對UI的布局就不仔細講了.有興趣的朋友可通過閱讀源碼研究.(為了方便小伙伴閱讀,項目布局使用純代碼開發(fā),實際開發(fā)過程中可以使用CocosStudio或者CocosCreator來進行布局,簡單快速)
物理引擎的使用
項目中物理引擎使用的是cocos2d-x自帶的PhysicsWord,對比Box2D使用更加簡單,唯一的缺點是當物體與剛體碰撞速度過快時,有時候物體會穿透剛體,解決的辦法目前是在update方法里,手寫代碼來對位置進行判斷.這里Box2D做的相對好一些,可以設(shè)置剛體的bullet
屬性.
物理世界的添加也非常簡單,在創(chuàng)建Scene時候調(diào)用createWithPhysics
就好,代碼如下
Scene* Game::createScene(PlayerType playerType)
{
// 添加物理引擎
Scene *scene = Scene::createWithPhysics();
// 設(shè)置PhysicsBody的邊緣可見,調(diào)試用
scene->getPhysicsWorld()->setDebugDrawMask(PhysicsWorld::DEBUGDRAW_ALL);
// set Gravity acceleration
scene->getPhysicsWorld()->setGravity(Vec2(0, -1400));
auto gameLayer = Game::createGameLayer(playerType);
if (gameLayer) {
scene->addChild(gameLayer);
}
// addPhysicsEdge
auto edge = PhysicsEdge::create();
scene->addChild(edge);
return scene;
}
小鳥類的封裝(Bird)
小鳥作為游戲的重要元素,為了方便場景內(nèi)的調(diào)用,把小鳥單獨封裝起來,并且提供下列方法
typedef std::function<void ()> AnimEnd;
enum BirdColor{
Red = 0,
Blue,
Yellow,
Random
};
class Bird:public Sprite {
public:
BirdColor color;
static Bird* createBird(BirdColor ExcludeColor = Random);
void startFlyAnimation();
void stopFlyAnimation();
void stopFlyAndRotatoAnimation();
void stopRatatoAnimation();
AnimEnd _end;
void startFallAnimation(AnimEnd animEnd);
void startShakeAnimation(int birdNum);
void click();
void birdResurrection(Vec2 position);
private:
void stopShakeAnimation();
bool isShakeing;
};
需要注意的是,在init方法里,給Bird類直接綁定上一個PhysicsBody,并且設(shè)置setDynamic為true,以便檢測后面與其他游戲元素的碰撞.
小鳥有三種顏色,每次游戲隨機顏色,當雙人對戰(zhàn)模式時,為了保證倆只鳥顏色的不同,需將第一只鳥的顏色排除.具體代碼如下.
Bird* Bird::createBird(BirdColor ExcludeColor)
{
auto bird = new Bird();
auto randColor = BirdColor(BirdColor(arc4random_uniform(3)));
while (randColor == ExcludeColor) {
randColor = BirdColor(BirdColor(arc4random_uniform(3)));
}
char name[30];
sprintf(name, "fly_%d_0.png", randColor);
if (bird && bird->initWithSpriteFrameName(name)) {
bird->color = randColor;
bird->addComponent(PhysicsBody::createBox(bird->getContentSize()));
bird->getPhysicsBody()->setDynamic(true);
bird->getPhysicsBody()->setEnabled(false);
bird->getPhysicsBody()->setRotationEnable(false);
bird->getPhysicsBody()->setVelocityLimit(500);
bird->getPhysicsBody()->addMoment(-1);
bird->getPhysicsBody()->setContactTestBitmask(1);
bird->autorelease();
} else {
delete bird;
bird = nullptr;
}
return bird;
}
水管(WaterPipe)
為了增加游戲的可玩性,這里將水管分為三種類型,定義一個枚舉,如下代碼
enum WaterPipeType {
Normal = 0,
Move,
Plant,
};
- Normal : 正常樣式的水管,由上下倆個水管組成,中間還有一個金幣構(gòu)成.出現(xiàn)幾率為80%.
- Move : 與Normal相同,不含有金幣圖片,會上下移動的水管.出現(xiàn)幾率為10%.
- Plant : 含有一棵食人花的水管,只有一個水管并且方向隨機,食人花會上下移動.出現(xiàn)幾率為10%.
在水管的初始化方法里,隨機生成水管的類型,并且根據(jù)不同類型,添加不同的水管元素.如果是Move或者Plant類型的,需要在水管的onEnterTransitionDidFinish
方法內(nèi),執(zhí)行對應(yīng)元素的動畫.同樣,給水管以及食人花都綁定剛體,并且設(shè)置setContactTestBitmask
,用于檢測與小鳥的碰撞.
- 關(guān)于PhysicsBody的
setCollisionBitmask
,setContactTestBitmask
,setCategoryBitmask
三個屬性是用于設(shè)置哪個剛體可以和哪個剛體發(fā)生碰撞的.這里就不過多介紹,網(wǎng)上資料很多并且很詳細,喜歡深入研究的小伙伴可自己查閱.
數(shù)字圖片文字(LabelAtlas)
游戲開發(fā)中,經(jīng)常會使用一些由UI給的數(shù)字圖片做為字體的文字樣式,如圖所示
這種Label使用圖片作為文字,該類直接使用圖片初始化文字對象。比如以下代碼:
auto numTexture = TextureCache().addImage("small_number_iphone.png");
auto picH = numTexture->getContentSize().height;
auto picW = numTexture->getContentSize().width;
int coinCount = GameDataManager::getInstance()->getAllCoinCount();
_goldCoinCount = LabelAtlas::create(to_string(coinCount), "small_number_iphone.png", picW * 0.1, picH, '0');
_goldCoinCount->setAnchorPoint(Vec2(0, 0.5));
_goldCoinCount->setPosition(_goldCoin->getPosition().x + 10, _goldCoin->getPosition().y);
addChild(_goldCoinCount, 4);
需要注意的是,這種樣式的圖片是有要求的,圖片中的十個數(shù)字要按照ASCII順序排列,我們要設(shè)置第一個字符的ASCII編碼,這樣,Cocos2d-x就可以直接計算出不同字符對應(yīng)的圖形了。
自此,游戲內(nèi)的相關(guān)元素都已添加完畢.
游戲邏輯代碼
點擊事件的監(jiān)聽:在Game類的init方法中,添加對屏幕點擊以及剛體碰撞的監(jiān)聽事件.并且設(shè)置監(jiān)聽的回調(diào)方法
// add onTouchLisner
auto lisner = EventListenerTouchAllAtOnce::create();
lisner->onTouchesBegan = CC_CALLBACK_2(Game::onTouchesBegan, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(lisner, this);
- 點擊事件的回調(diào),根據(jù)不同的游戲類型,做不同的處理,當是雙人模式的時候,判斷點擊位置處于屏幕的哪一邊,并且執(zhí)行對應(yīng)Bird的click()方法.
這里需要注意一點的是,cocos2d-x中,iOS默認是將多點觸控關(guān)閉的,需要在AppController.mm類中,將[eaglView setMultipleTouchEnabled:YES];
void Game::onTouchesBegan(const std::vector<cocos2d::Touch *> &touches, cocos2d::Event *event)
{
if (_gameOver) return;
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("flap.caf");
if (!_gameIsStarting) {
_gameIsStarting = true;
startGame();
if (_playerType == PlayerType::TwoPlayer) {
_bird1->click();
_bird2->click();
return;
}
}
if (_playerType == PlayerType::OnePlayer) {
_bird1->click();
} else {
// 對戰(zhàn)模式 判斷點擊的點在屏幕哪邊
for (int i = 0; i < touches.size(); i++) {
auto touch = touches[i];
if (touch->getLocation().x <= kWinSizeWidth * 0.5) {
_bird1->click();
} else {
_bird2->click();
}
}
}
}
Bird的click方法里,給bird一個向上的速度,并且執(zhí)行鳥頭向上旋轉(zhuǎn)然后又向下旋轉(zhuǎn)的動畫.如代碼所示:
void Bird::click()
{
stopShakeAnimation();
getPhysicsBody()->setVelocity(Vec2(0, 600));
// click bird rotate action
stopActionByTag(kBirdRotatoTag);
auto rotaUp = RotateTo::create(0.1, -40);
auto rotaDown = RotateTo::create(2.5, 80);
auto seqe = Sequence::create(rotaUp, rotaDown, NULL);
seqe->setTag(kBirdRotatoTag);
this->runAction(seqe);
}
水管的出現(xiàn):為了方便代碼的管理,這里將水管,陸地等元素添加到了GameElement
中,并將GameElement
添加到Game
上.當游戲開始時候,會出現(xiàn)等待動畫,如圖
當玩家點擊屏幕后,調(diào)用GameElement
的addWaterPipe()
方法,開啟定時器,每隔一段時間添加一個新的水光.并且將其放到一個容器里,并且判斷水管的位子,如果超過小鳥,隱藏金幣,播放音樂,增加用戶金幣數(shù)并且將超出屏幕的水管移除.代碼如下
void GameElement::update(float dt)
{
if (_birdUnrivalled) {
_unrivalledIndex--;
if (_unrivalledIndex <= 0) {
_birdUnrivalled = false;
_unTimeEnd();
}
}
_index++;
if (_index == 135) {
// add water pipe
_index = 0;
auto wp = WaterPipe::createWaterPipe(_wpColor, _wpHeight, playType == OnePlayer);
wp->setPosition(kWinSizeWidth, 0);
addChild(wp);
_waterPipes.pushBack(wp);
}
for (auto it = _waterPipes.begin(); it != _waterPipes.end(); it ++) {
WaterPipe *wp = *it;
if (_birdUnrivalled) {
wp->setChildPhysicsBodyEnabled(false);
} else {
wp->setChildPhysicsBodyEnabled(true);
}
// move wp
wp->setPosition(Vec2(wp->getPosition().x - 3, wp->getPosition().y));
if (wp->_coin->isVisible()) {
// remove coin
if (wp->getPosition().x < _birdX) {
wp->_coin->setVisible(false);
// play sound effect
if (playType == OnePlayer) {
GameDataManager::getInstance()->addCoin();
_goldCoinCount->setString(to_string(GameDataManager::getInstance()->getAllCoinCount()));
}
_passNum->setString(to_string(++_passIndex));
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("coin.aif");
}
}
if (wp->getPosition().x < 0 - wp->getContentSize().width) {
// remove wp
_waterPipes.eraseObject(wp);
wp->removeFromParent();
}
}
}
碰撞事件的檢測:同樣也在Game類的init方法中,添加剛體碰撞的監(jiān)聽事件.并且設(shè)置監(jiān)聽的回調(diào)方法
// add Contact listener
auto contactLisner = EventListenerPhysicsContact::create();
contactLisner->onContactBegin = CC_CALLBACK_1(Game::onContactBegan, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(contactLisner, this);
碰撞Action,需要注意參與碰撞的元素需要設(shè)置setContactTestBitmask
.
bool Game::onContactBegan(cocos2d::PhysicsContact &cat)
{
if (_gameOver || _birdUnrivalled) return true;
// 判斷是不是鳥
if (_playerType == OnePlayer) {
if (!(cat.getShapeA()->getBody()->getNode()->getTag() == kBird1Tag || cat.getShapeB()->getBody()->getNode()->getTag() == kBird1Tag)) {
return true;
}
} else {
if (!(cat.getShapeA()->getBody()->getNode()->getTag() == kBird1Tag || cat.getShapeB()->getBody()->getNode()->getTag() == kBird1Tag || cat.getShapeB()->getBody()->getNode()->getTag() == kBird2Tag || cat.getShapeB()->getBody()->getNode()->getTag() == kBird2Tag)) {
return true;
}
}
_gameOver = true;
CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("hit.caf");
showFlashLightAnimation();
// Contact object stopGame
if (_playerType == OnePlayer) {
// stop Fly and rota
_bird1->stopFlyAndRotatoAnimation();
// start Fall
_bird1->startFallAnimation([this](){
// show revival view
auto tipsLayer = TipsLayer::createTipsLayer(_resCount);
this->addChild(tipsLayer, 4);
tipsLayer->showResurrectionTipsView([this](){
// Resurrection
_bird1->birdResurrection(Vec2(kWinSizeWidth * 0.25, kWinSizeHeight * 0.5));
_bird1->click();
_elementLayer->_goldCoinCount->setString(to_string(GameDataManager::getInstance()->getAllCoinCount()));
_elementLayer->birdResurrection([this](){
_birdUnrivalled = false;
_bird1->setOpacity(255);
});
_resCount++;
_gameOver = false;
_birdUnrivalled = true;
}, [this](){
_elementLayer->hiddenAllLabel();
}, _elementLayer->getPassScore(), [](){
// player again
Director::getInstance()->replaceScene(TransitionFade::create(0.25, Game::createScene(OnePlayer), Color3B(255, 255, 255)));
}, [](){
// share
#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/ZhongTaoTian"]];
#endif
});
});
} else {
bool firstWin = false;
// 判斷是哪一個鳥被撞了
if (cat.getShapeA()->getBody()->getNode()->getTag() == kBird1Tag || cat.getShapeB()->getBody()->getNode()->getTag() == kBird1Tag) {
_bird1->stopFlyAndRotatoAnimation();
_bird2->stopRatatoAnimation();
} else if (cat.getShapeB()->getBody()->getNode()->getTag() == kBird2Tag || cat.getShapeB()->getBody()->getNode()->getTag() == kBird2Tag) {
_bird2->stopFlyAndRotatoAnimation();
_bird1->stopRatatoAnimation();
firstWin = true;
}
_bird1->getPhysicsBody()->removeFromWorld();
_bird2->getPhysicsBody()->removeFromWorld();
auto fallBird = firstWin ? _bird2 : _bird1;
fallBird->startFallAnimation([this, firstWin](){
_elementLayer->hiddenAllLabel();
auto tipsLayer = TipsLayer::createTipsLayer(0, false);
this->addChild(tipsLayer, 4);
tipsLayer->showVsResultTipsView([](){
// player again
Director::getInstance()->replaceScene(TransitionFade::create(0.25, Game::createScene(TwoPlayer), Color3B(255, 255, 255)));
}, [](){
}, _elementLayer->getPassScore(), firstWin);
});
}
_elementLayer->stopGame();
_bird1->getPhysicsBody()->setEnabled(false);
return true;
}
首先判斷碰撞的元素是否含有小鳥,如果兩個物體都不是,直接return,反之,停止游戲,這時候會出現(xiàn)兩種情況,如果是單人模式,進入是否選擇復(fù)活界面,如果是雙人對戰(zhàn)模式,直接顯示哪一邊贏了.如圖
單人模式時,當玩家點擊復(fù)活,會出現(xiàn)幾秒的無敵模式,此刻將水管以及陸地的剛體可編輯屬性取消,無敵時間到了,再添加回來就可以了.
所有的提示都單獨封裝了一個TipsLayer類,提供以下接口方便調(diào)用.
typedef std::function<void ()> Callback;
class TipsLayer:public Layer {
public:
virtual bool init(int resCount, bool isOnePlayer);
static TipsLayer* createTipsLayer(int resCount, bool isOnePlayer = true);
void showResurrectionTipsView(Callback yesBtnClick, Callback noBtnClick, int score, Callback playAgain, Callback share);
void showVsResultTipsView(Callback okBtnClick, Callback shareBtnClick, int score, bool isLeftWin);
至此,項目的主要邏輯已經(jīng)沒什么了,其他細節(jié)功能,如暫停,如何調(diào)用iOS和Java的API等功能,對項目有興趣的朋友可下載代碼研究下.如有問題,可留言或者私信我.