手游Flappy Bird升級版

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)模式,場景效果如下圖

對戰(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)用GameElementaddWaterPipe()方法,開啟定時器,每隔一段時間添加一個新的水光.并且將其放到一個容器里,并且判斷水管的位子,如果超過小鳥,隱藏金幣,播放音樂,增加用戶金幣數(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等功能,對項目有興趣的朋友可下載代碼研究下.如有問題,可留言或者私信我.

代碼下載地址(如果覺得有幫助,請點擊Star★)

代碼下載地址,記得Star★和Follow

小熊的技術(shù)博客

點擊鏈接我的博客,歡迎關(guān)注

小熊的新浪微博

我的新浪微博,歡迎關(guān)注

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容