cocos2dx社區裏有個系列博客完整地複製原版flappybird的所有特性,不過那個代碼寫得比較複雜,新手學習起來有點捉摸不透,這裏我寫了個簡單的版本。演示如下:
創建項目
VS2013+cocos2dx 3.2創建win32項目,由於只是學習,所以沒有編譯爲安卓、ios或者WP平臺的可執行文件。
最終的項目工程結構如下:
很簡單,只有三個類,預加載類,遊戲主場景類,應用代理類,新手剛入門喜歡將很多東西都寫在儘量少的類裏面。
遊戲設計
遊戲結構如下,遊戲包含預加載場景和主場景,主場景中包含背景、小鳥、管道和各種UI界面。開發步驟
1,素材收集從apk文件裏提取出來一些圖片和音頻,並用TexturePatcher拼成大圖,導出plist文件。
2,預加載場景
新建一個LoadingScene,在裏面添加一張啓動圖片,通過異步加載紋理並回調的方式把所有圖片素材、小鳥幀動畫以及音頻文件都加入到緩存,加載完畢後跳轉到遊戲主場景。
//添加加載回調函數,用異步加載紋理
Director::getInstance()->getTextureCache()->addImageAsync("game.png", CC_CALLBACK_1(LoadingScene::loadingCallBack, this));
void LoadingScene::loadingCallBack(Texture2D *texture)
{
//預加載幀緩存紋理
SpriteFrameCache::getInstance()->addSpriteFramesWithFile("game.plist", texture);
//預加載幀動畫
auto birdAnimation = Animation::create();
birdAnimation->setDelayPerUnit(0.2f);
birdAnimation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName("bird1.png"));
birdAnimation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName("bird2.png"));
birdAnimation->addSpriteFrame(SpriteFrameCache::getInstance()->getSpriteFrameByName("bird3.png"));
AnimationCache::getInstance()->addAnimation(birdAnimation, "birdAnimation"); //將小鳥動畫添加到動畫緩存
//預加載音效
SimpleAudioEngine::getInstance()->preloadEffect("die.mp3");
SimpleAudioEngine::getInstance()->preloadEffect("hit.mp3");
SimpleAudioEngine::getInstance()->preloadEffect("point.mp3");
SimpleAudioEngine::getInstance()->preloadEffect("swooshing.mp3");
SimpleAudioEngine::getInstance()->preloadEffect("wing.mp3");
//加載完畢跳轉到遊戲場景
auto gameScene = GameScene::createScene();
TransitionScene *transition = TransitionFade::create(0.5f, gameScene);
Director::getInstance()->replaceScene(transition);
}
3,遊戲主場景
3.1,背景和logo
用圖片精靈即可
//添加遊戲背景
Sprite *backGround = Sprite::createWithSpriteFrameName("bg.png");
backGround->setPosition(visibleOrigin.x + visibleSize.width / 2, visibleOrigin.y + visibleSize.height / 2);
this->addChild(backGround);
//logo
auto gameLogo = Sprite::createWithSpriteFrameName("bird_logo.png");
gameLogo->setPosition(visibleOrigin.x + visibleSize.width / 2, visibleOrigin.y + visibleSize.height / 2+100);
gameLogo->setName("logo");
this->addChild(gameLogo);
logo在遊戲開始後要隱藏掉。3.2,小鳥
3.3,地板
//小鳥
birdSprite = Sprite::create();
birdSprite->setPosition(visibleOrigin.x + visibleSize.width / 3, visibleOrigin.y + visibleSize.height / 2);
this->addChild(birdSprite);
auto birdAnim = Animate::create(AnimationCache::getInstance()->animationByName("birdAnimation"));
birdSprite->runAction(RepeatForever::create(birdAnim)); //揮翅動畫
auto up = MoveBy::create(0.4f, Point(0, 8));
auto upBack = up->reverse();
if (gameStatus == GAME_READY)
{
swingAction = RepeatForever::create(Sequence::create(up, upBack, NULL));
birdSprite->runAction(swingAction); //上下晃動動畫
}
在準備界面下除了有扇翅膀的動作,還有上下浮動的動作。3.3,地板
地板的左移是用兩張錯位的地板圖片循環左移實現的。需要用到自定義調度器,注意調節移動速度。
//添加兩個land
land1 = Sprite::createWithSpriteFrameName("land.png");
land1->setAnchorPoint(Point::ZERO);
land1->setPosition(Point::ZERO);
this->addChild(land1, 10); //置於最頂層
land2 = Sprite::createWithSpriteFrameName("land.png");
land2->setAnchorPoint(Point::ZERO);
land2->setPosition(Point::ZERO);
this->addChild(land2, 10);
Size visibleSize = Director::getInstance()->getVisibleSize();
//兩個圖片循環移動
land1->setPositionX(land1->getPositionX() - 1.0f);
land2->setPositionX(land1->getPositionX() + land1->getContentSize().width - 2.0f);
if (land2->getPositionX() <= 0)
land1->setPosition(Point::ZERO);
3.4,水管一組水管由上下2半根組成,用Node包起來,弄個vector容器添加兩組管道,每次出現在屏幕中的管子只有兩組,當一組消失在屏幕範圍內則重設置其橫座標,需要提前計算好各種間距或者高度。
//同屏幕出現的只有兩根管子,放到容器裏面,上下綁定爲一根
for (int i = 0; i < 2; i++)
{
auto visibleSize = Director::getInstance()->getVisibleSize();
Sprite *pipeUp = Sprite::createWithSpriteFrameName("pipe_up.png");
Sprite *pipeDown = Sprite::createWithSpriteFrameName("pipe_down.png");
Node *singlePipe = Node::create();
//給上管綁定剛體
auto pipeUpBody = PhysicsBody::createBox(pipeUp->getContentSize());
pipeUpBody->setDynamic(false);
pipeUpBody->setContactTestBitmask(1);
pipeUp->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
pipeUp->setPhysicsBody(pipeUpBody);
//給兩個管子分開設置剛體,可以留出中間的空隙使得小鳥通過
//給下管綁定剛體
auto pipeDownBody = PhysicsBody::createBox(pipeDown->getContentSize());
pipeDownBody->setDynamic(false);
pipeDownBody->setContactTestBitmask(1);
pipeDown->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
pipeDown->setPhysicsBody(pipeDownBody);
pipeUp->setPosition(0, PIPE_HEIGHT + PIPE_SPACE);
singlePipe->addChild(pipeUp);
singlePipe->addChild(pipeDown); //pipeDown默認加到(0,0),上下合併,此時singlePipe以下面的管子中心爲錨點
singlePipe->setPosition(i*PIPE_INTERVAL + WAIT_DISTANCE, getRandomHeight() ); //設置初始高度
singlePipe->setName("newPipe");
this->addChild(singlePipe); //把兩個管子都加入到層
pipes.pushBack(singlePipe); //兩個管子先後添加到容器
}
//管子滾動
for (auto &singlePipe : pipes)
{
singlePipe->setPositionX(singlePipe->getPositionX() - 1.0f);
if (singlePipe->getPositionX() < -PIPE_WIDTH/2)
{
singlePipe->setPositionX(visibleSize.width+PIPE_WIDTH/2);
singlePipe->setPositionY(getRandomHeight());
singlePipe->setName("newPipe"); //每次重設一根管子,標爲new
}
}
3.5,加入物理世界cocos2dx 3.0後引入了自帶的物理引擎,用法和box2D等差不多。
物理世界初始化
gameScene->getPhysicsWorld()->setGravity(Vec2(0, -900)); //設置重力場,重力加速度可以根據手感改小點
gameLayer->setPhysicWorld(gameScene->getPhysicsWorld()); //綁定物理世界
小鳥綁定剛體//小鳥綁定剛體
auto birdBody = PhysicsBody::createCircle(BIRD_RADIUS); //將小鳥當成一個圓,懶得弄精確的輪廓線了
birdBody->setDynamic(true); //設置爲可以被物理場所作用而動作
birdBody->setContactTestBitmask(1); //必須設置這項爲1才能檢測到不同的物體碰撞
birdBody->setGravityEnable(false); //設置是否被重力影響,準備畫面中不受重力影響
birdSprite->setPhysicsBody(birdBody); //爲小鳥設置剛體
地板綁定剛體//設置地板剛體
Node *groundNode = Node::create();
auto groundBody = PhysicsBody::createBox(Size(visibleSize.width, land1->getContentSize().height));
groundBody->setDynamic(false);
groundBody->setContactTestBitmask(1);
groundNode->setAnchorPoint(Vec2::ANCHOR_MIDDLE); //物理引擎中的剛體只允許結點錨點設置爲中心
groundNode->setPhysicsBody(groundBody);
groundNode->setPosition(visibleOrigin.x+visibleSize.width/2,land1->getContentSize().height/2);
this->addChild(groundNode);
管道設置剛體,上下半根分別設置,留出中間的縫隙//給上管綁定剛體
auto pipeUpBody = PhysicsBody::createBox(pipeUp->getContentSize());
pipeUpBody->setDynamic(false);
pipeUpBody->setContactTestBitmask(1);
pipeUp->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
pipeUp->setPhysicsBody(pipeUpBody);
//給兩個管子分開設置剛體,可以留出中間的空隙使得小鳥通過
//給下管綁定剛體
auto pipeDownBody = PhysicsBody::createBox(pipeDown->getContentSize());
pipeDownBody->setDynamic(false);
pipeDownBody->setContactTestBitmask(1);
pipeDown->setAnchorPoint(Vec2::ANCHOR_MIDDLE);
pipeDown->setPhysicsBody(pipeDownBody);
碰撞檢測現在層的init裏面的事件分發器中加入碰撞偵聽
//添加碰撞監測
auto contactListener = EventListenerPhysicsContact::create();
contactListener->onContactBegin = CC_CALLBACK_1(GameScene::onContactBegin, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(contactListener, this);
//碰撞監測
bool GameScene::onContactBegin(const PhysicsContact& contact)
{
if (gameStatus == GAME_OVER) //當遊戲結束後不再監控碰撞
return false;
gameOver();
return true;
}
3.6,觸摸檢測//觸摸監聽
bool GameScene::onTouchBegan(Touch *touch, Event *event)
3.7,控制小鳥由準備模式變到遊戲開始模式後,觸摸屏幕會給小鳥一個向上的速度,寫在觸摸檢測裏面
birdSprite->getPhysicsBody()->setVelocity(Vec2(0, 250)); //給一個向上的初速度
小鳥的旋轉角度與縱向速度有關,寫在update()裏
//小鳥的旋轉
auto curVelocity = birdSprite->getPhysicsBody()->getVelocity();
birdSprite->setRotation(-curVelocity.y*0.1 - 20); //根據豎直方向的速度算出旋轉角度,逆時針爲負
開始後啓動各種定時器
//遊戲開始
void GameScene::gameStart()
{
gameStatus = GAME_START;
score = 0;//重置分數
scoreLabel->setString(String::createWithFormat("%d", score)->getCString());
this->getChildByName("logo")->setVisible(false); //logo消失
scoreLabel->setVisible(true); //計分開始
this->scheduleUpdate();//啓動默認更新
this->schedule(schedule_selector(GameScene::scrollLand), 0.01f); //啓動管子和地板滾動
birdSprite->stopAction(swingAction); //遊戲開始後停止上下浮動
birdSprite->getPhysicsBody()->setGravityEnable(true); //開始受重力作用
}
3.9,計分和數據存儲在默認的update()函數裏對得分進行判斷和更新,通過默認xml存儲歷史分數
//當遊戲開始時,判斷得分,這個其實也可以寫在其他地方,比如管子滾動的更新函數裏面或者觸摸監測裏面
if (gameStatus == GAME_START)
{
for (auto &pipe : pipes)
{
if (pipe->getName() == "newPipe") //新來一根管子就判斷
{
if (pipe->getPositionX() < birdSprite->getPositionX())
{
score++;
scoreLabel->setString(String::createWithFormat("%d", score)->getCString());
SimpleAudioEngine::getInstance()->playEffect("point.mp3");
pipe->setName("passed"); //標記已經過掉的管子
}
}
}
}
4.0,遊戲結束//遊戲結束
void GameScene::gameOver()
{
gameStatus = GAME_OVER;
//獲取歷史數據
bestScore = UserDefault::getInstance()->getIntegerForKey("BEST");
if (score > bestScore)
{
bestScore = score; //更新最好分數
UserDefault::getInstance()->setIntegerForKey("BEST", bestScore);
}
SimpleAudioEngine::getInstance()->playEffect("hit.mp3");
//遊戲結束後停止地板和管道的滾動
this->unschedule(schedule_selector(GameScene::scrollLand));
}
結束後比較當前分數和歷史分數,以便更新。4.1,音頻
音效文件已經加入到緩存,在適當的地方加上全局音頻控制器播放音效即可
SimpleAudioEngine::getInstance()->playEffect("hit.mp3");
4.2,記分板遊戲結束後滑入記分板,並顯示重玩按鈕。
//加入記分板和重玩菜單
void GameScene::gamePanelAppear()
{
Size size = Director::getInstance()->getVisibleSize();
Vec2 origin = Director::getInstance()->getVisibleOrigin();
//用node將gameoverlogo和記分板綁在一起
Node *gameOverPanelNode = Node::create();
auto gameOverLabel = Sprite::createWithSpriteFrameName("gameover.png");
gameOverPanelNode->addChild(gameOverLabel);
auto panel = Sprite::createWithSpriteFrameName("board.PNG");//注意這裏是大寫PNG,原圖片用什麼後綴這裏就用什麼,區分大小寫
gameOverLabel->setPositionY(panel->getContentSize().height); //設置一下座標
gameOverPanelNode->addChild(panel);
//記分板上添加兩個分數
auto curScoreTTF = LabelTTF::create(String::createWithFormat("%d", score)->getCString(), "Arial", 20);
curScoreTTF->setPosition(panel->getContentSize().width-40, panel->getContentSize().height-45);
curScoreTTF->setColor(Color3B(255, 0, 0));
panel->addChild(curScoreTTF);
auto bestScoreTTF = LabelTTF::create(String::createWithFormat("%d", bestScore)->getCString(), "Arial", 20);
bestScoreTTF->setPosition(panel->getContentSize().width - 40, panel->getContentSize().height - 90);
bestScoreTTF->setColor(Color3B(0, 255, 0));
panel->addChild(bestScoreTTF);
this->addChild(gameOverPanelNode);
gameOverPanelNode->setPosition(origin.x + size.width / 2, origin.y + size.height );
//滑入動畫
gameOverPanelNode->runAction(MoveTo::create(0.5f, Vec2(origin.x + size.width / 2, origin.y + size.height / 2)));
SimpleAudioEngine::getInstance()->playEffect("swooshing.mp3");
//添加菜單
MenuItemImage *restartItem = MenuItemImage::create("start_btn.png", "start_btn_pressed.png", this,menu_selector(GameScene::gameRetart));
auto menu = CCMenu::createWithItem(restartItem);
menu->setPosition(origin.x + size.width / 2, 150);
this->addChild(menu);
}
//遊戲重新開始
void GameScene::gameRetart(Ref *sender)
{
//重新回到初始畫面
auto gameScene = GameScene::createScene();
Director::getInstance()->replaceScene(gameScene); //這裏懶得加特效了,直接轉場
}
效果圖:
源代碼
csdn下載:MyFlappyBird