cocos2dx實例開發之flappybird(入門版)

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,小鳥
//小鳥
	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);  //根據豎直方向的速度算出旋轉角度,逆時針爲負
3.8,遊戲開始
開始後啓動各種定時器
//遊戲開始
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
github下載:MyFlappyBird
還有很多要完善的地方,比如沒有加入圖片數字以及社交分享等等。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章