MFC實(shí)現(xiàn)桌面版Flappy Bird
一、開發(fā)背景:
flappy bird由一位來自越南河內(nèi)的獨(dú)立游戲開發(fā)者阮哈東開發(fā),是一款形式簡易但難度極高的休閑游戲。簡單但不粗糙的8比特像素畫面、超級馬里奧游戲中的水管、眼神有點(diǎn)呆滯的小鳥和幾朵白云便構(gòu)成了游戲的一切。你需要不斷控制點(diǎn)擊屏幕的頻率來調(diào)節(jié)小鳥的飛行高度和降落速度,讓小鳥順利地通過畫面右端的通道,如果你不小心擦碰到了通道的話,游戲便宣告結(jié)束。
這款虐心的小游戲一經(jīng)推出,便引起火爆的下載。然后先后出現(xiàn)了各種平臺的移植開發(fā):IOS平臺PC和手機(jī)版、采用HTML5+Canvas及Javascript技術(shù)來實(shí)現(xiàn)的Flappy Bird電腦版、以網(wǎng)頁html5+JS技術(shù)完全克隆了原版native app的Web App版、實(shí)現(xiàn)了在微信朋友圈和QQ空間中的無縫運(yùn)行的微信/QQ空間版、WindowsPhone版….但是唯一沒有的是直接可在windows操作系統(tǒng)下的單機(jī)版,于是當(dāng)時突發(fā)奇想,不如我來填補(bǔ)這個漏洞吧!
二、開發(fā)語言及運(yùn)行環(huán)境:
此PC版采用C++的MFC技術(shù)在VS2012開發(fā)平臺下寫成,支持windows 7\8環(huán)境,XP不知道為啥不行~
三、效果展示:
四、游戲框架說明:
整個游戲除了由MFC游戲基本框架CMyApp和CMainWindow外,這里特別封裝了以下幾個類:
- 1、 Bird類[專門處理鳥的飛行邏輯、碰撞檢測、音樂播放、貼圖]
- 2、 PipeList類[內(nèi)嵌Pipe類,并用CList創(chuàng)建一個Pipe鏈表,用來處理游戲中管道的移動邏輯、碰撞檢測等]
- 3、 Panel類[主要是計分板的動畫效果邏輯和計分板的計分邏輯,數(shù)字貼圖,金幣種類運(yùn)算等]
- 4、 Land類[主要處理陸地運(yùn)動邏輯及貼圖]
- 5、 Button類[主要處理按鈕的動畫效果、貼圖及響應(yīng)]
- 6、 Pic類[是圖片資源類,主要負(fù)責(zé)存儲、加載、全局調(diào)用游戲的圖片資源]
五、游戲狀態(tài)及邏輯說明:
這款游戲本身操作簡單、邏輯分明,大致可分為以下幾種狀態(tài):
1、 初始態(tài):基本上為靜態(tài)貼圖,只有鳥和陸地為簡單運(yùn)動。由上往下依次為:
- [數(shù)字:0]
- [標(biāo)志:Get Ready!]
- [圖標(biāo):操作方法]
- [鳥:上下飛行]
- [陸地:向左移動]
- [背景:隨機(jī)晝夜]
2、 游戲進(jìn)行態(tài):當(dāng)點(diǎn)擊一下屏幕,鳥、柱子被解封,陸地依然保持原來運(yùn)動狀態(tài),背景不變,這里采用相對運(yùn)動效果,其實(shí)背景是沒有運(yùn)動的,而鳥也只是上下運(yùn)動,根本就沒有向前飛一點(diǎn)!
- [鳥:向上躍起,然后以豎直上拋的邏輯使鳥運(yùn)動;同時,還要專門為鳥的姿態(tài)設(shè)計合理的旋轉(zhuǎn)函數(shù)]
- [柱子:向柱子鏈表里加入新的柱子,并使鏈表里的所有柱子開始向左移動,當(dāng)柱子完全超出最左邊界時,將該柱子刪除;同樣的,當(dāng)最后一個柱子到達(dá)某一特定距離時,向鏈表里加入一個新的柱子,這樣既保證了剛開始的柱子出現(xiàn)效果的真實(shí)、有趣性,又保證了資源的合理回收,提高算法高效性]
- [分?jǐn)?shù):當(dāng)柱子到達(dá)鳥所在的位置時就要進(jìn)行碰撞檢測,如果沒有碰撞且鳥跨過柱子,就讓分?jǐn)?shù)+1,并響鈴]
- [陸地:保持勻速運(yùn)動邏輯,采用循環(huán)貼圖技術(shù),產(chǎn)生無縫效果]
3、 死亡狀態(tài):鳥的死亡狀態(tài)看似簡單,但是仔細(xì)分析并非如此。各種細(xì)節(jié)都要分別考慮:
- [直接撞地態(tài):中止所有運(yùn)動邏輯,同時留一定的時間間隔,產(chǎn)生畫面轉(zhuǎn)換的質(zhì)感]
- [高撞柱子態(tài):旋轉(zhuǎn)為垂直態(tài),然后自由落體;撞擊時發(fā)出聲音,然后發(fā)出墜落的聲音,同時進(jìn)行碰撞檢測,碰到陸地中止一切運(yùn)動,進(jìn)行時間停留]
- [低撞柱子態(tài):和高撞柱子態(tài)的區(qū)別是,墜落的時間少了,音樂沒有完頁面就跳轉(zhuǎn)了,所以要控制時間停留長度,產(chǎn)生高仿的效果]
4、 死亡之后態(tài):鳥撞地之后要有一定的時間逗留防止頁面跳轉(zhuǎn)過快不舒服的感覺。接下來首先貼上game_over的圖標(biāo),然后計分板從下往上飛來,接著開始計分并張貼是否為新紀(jì)錄和是否獲得金牌之類的,最后貼上兩個按鈕等待響應(yīng)。
- [陸地:停止運(yùn)動]
- [圖標(biāo):展示Game_Over]
- [計分板:動畫效果,從下往上飛來并帶有音效,當(dāng)飛到指定位置時開始從0累計得分,并統(tǒng)計是否為新紀(jì)錄和是否獲得相應(yīng)的獎牌]
- [按鈕:靜態(tài)貼圖,但是相應(yīng)的時候有上下振動的效果]
#p#
六、經(jīng)典算法說明:
1、 ON_WM_TIMER:時間消息映射:
主要控制全局邏輯運(yùn)算的時間進(jìn)程,根據(jù)當(dāng)前的狀態(tài)做相應(yīng)的邏輯運(yùn)算;同時邏輯運(yùn)算也會對全局的游戲狀態(tài)進(jìn)行改變,實(shí)現(xiàn)全局操控邏輯實(shí)現(xiàn):(與此相同的draw函數(shù)這里就不再詳細(xì)介紹)
- void CMainWindow::OnTimer(UINT nTimerID){
- switch(nTimerID){
- case bird_time:
- if(game_state==before_game)bird.logic(before_game,game_state);//開始前
- break;
- case land_time:
- if(game_state==before_game){//開始前
- land.logic();//路
- }else if(game_state==during_game){//游戲中
- if(bird.state!=bird_delay)land.logic();//路
- bird.logic(1,game_state);//鳥正常運(yùn)動
- if(bird.state!=bird_delay)pipe.logic(goals,bird,game_state);//管道
- }else if(game_state==dying_game){//失敗中
- bird.logic(2,game_state);//垂直下落
- }else if(game_state==end_game){//顯示game-over+計分板+2個按鈕
- if(panel.state==finish)button.logic(game_state);
- if(last_state>=10)panel.logic(goals,best_goals);
- }else if(game_state==start_game){//重新開始
- restart();
- game_state=before_game;
- }
- break;
- default:break;
- }
- draw();
- }
2、 ON_WM_LEFTBUTTONDOWN:鼠標(biāo)左鍵按下監(jiān)聽映射:
每次單擊鼠標(biāo)左鍵相應(yīng)該函數(shù),然后該函數(shù)根據(jù)不同的游戲狀態(tài)做出不同的邏輯操作:①、[當(dāng)游戲處于0態(tài),即:游戲開始之前時,點(diǎn)擊鼠標(biāo),狀態(tài)改為1態(tài),柱子加入開始移動,鳥躍起開始飛翔][當(dāng)處于游戲態(tài)時:每次點(diǎn)擊鳥都會躍起];②、[當(dāng)處于結(jié)束態(tài)時:按鈕等待鼠標(biāo)按動,并根據(jù)區(qū)域做出判斷是否按了按鈕,按了哪一個]
- void CMainWindow::OnLButtonDown(UINT nFlags, CPoint point){
- if(game_state==0){
- game_state=1;
- pipe.add();
- bird.jump();
- }else if(game_state==1){
- bird.jump();
- }else if(game_state==3){
- button.click(point);
- }
- }
3、PipeList::logic柱子邏輯函數(shù),包括碰撞檢測!
為了簡化起見,我把音頻播放的部分刪去了:這里是遍歷整個鏈表,對于每一個柱子,由上到下每一個if為:①、[判斷鳥是否正好穿越一個柱子,如果是則分?jǐn)?shù)加1];②、[判斷柱子是否出界,超出就不把該柱子放回鏈表,相當(dāng)于刪除];③、[鳥與地面的碰撞檢測];④、[鳥與柱子的碰撞檢測]⑤、[最后一個if是判斷最后一個柱子是否到達(dá)指定位置,如果到達(dá)就向鏈表尾部加入一個新的柱子,從而保證了柱子連續(xù)且間距統(tǒng)一]
- //---------------------------------------------------------------
- void PipeList::logic(int &goals,Bird &bird,int &game_state){//邏輯函數(shù)
- int count=pipe.GetCount();
- for(int i=0;i<count;i++){
- Pipe temp=pipe.GetHead();
- pipe.RemoveHead();
- temp.logic();
- if(temp.pos_x==64){
- goals+=1;
- }
- if(temp.pos_x>=-70)pipe.AddTail(temp);
- //碰撞檢測
- if(23+bird.y+48-$d>400){//與地面
- bird.y=400-230-48+$d;
- bird.stop();
- game_state=2;
- }else if(!(65+48-$d < temp.pos_x || temp.pos_x+52<65+$d)){//與柱子
- if(!(230+bird.y+$d > temp.pos_y+320 && temp.pos_y+420 > 230+bird.y+48-$d)){
- game_state=2;//表示碰撞,游戲結(jié)束;
- }
- }
- }
- if((pipe.GetTail()).pos_x<=140){
- Pipe temp;
- pipe.AddTail(temp);
- }
- }//---------------------------------------------------------------
4、 Bird::logic鳥的運(yùn)動邏輯,包括所有運(yùn)動狀態(tài)(絕密算法?。。。?/strong>
同樣的為了簡單我也把音頻部分的代碼刪去了。此函數(shù)是分別將鳥的運(yùn)動的各個狀態(tài)做分別處理:①、[開始前:采用正弦函數(shù)波動飛行同時改變翅膀狀態(tài)];②、[正常飛行時:又把鳥的運(yùn)動狀態(tài)劃分為向上、向下、旋轉(zhuǎn)、停留四個狀態(tài)分別處理];③、[下落死亡狀態(tài):這里用了一個輔助時間變量,控制幀動畫播放]
- //---------------------------------------------------------------
- void Bird::logic(int ID,int &game_state){
- if(ID==0){//開始前
- y=4*sin(Time*PI);
- Time+=0.25;
- fly_state=(fly_state+1)%3;
- }else if(ID==1){//正常
- switch(state){
- case state_up:
- v+=a;
- y+=v;
- dis_state--;
- if(dis_state==0){
- state=state_turn;
- Time=0;
- }
- break;
- case state_turn:
- v+=a;
- y+=v;
- if(230+y+48-$d>=400){
- y=400-230-48+$d;
- stop();
- game_state=3;
- }
- dis_state++;
- if(dis_state==1 && Time<=0.4){
- Time+=0.1;
- dis_state=0;
- }
- if(dis_state==6){
- state=state_down;
- }
- break;
- case state_down:
- v+=a;
- y+=v;
- if(delay==0 && 230+y+48-$d>=400){
- y=400-230-48+$d;
- stop();
- state=state_delay;
- }
- break;
- case state_delay:
- delay++;
- if(delay==8){game_state=3;}
- break;
- default:break;
- }
- if(dis_state!=6)fly_state=(fly_state+1)%3;
- }else if(ID==2){//下落
- delay++;
- if(delay==8){//撞擊聲延時
- }
- if(delay<60){//下落運(yùn)算
- y+=v;
- v+=a;
- if(dis_state!=6)dis_state++;
- if(230+y+48-$d>=400){//撞地檢測
- y=400-230-48+$d;
- stop();
- if(dis_state==6){delay=60;}
- }
- }else if(delay==66){//墜地后延時
- game_state=3;
- }
- }
- }//---------------------------------------------------------------
5、Button::按鈕識別和按鈕動畫邏輯實(shí)現(xiàn):
通過鼠標(biāo)所在的點(diǎn)判斷是否在按鈕所在的矩形區(qū)域內(nèi)來判斷是否點(diǎn)了該按鈕:
- //---------------------------------------------------------------
- void Button::click(CPoint &point){
- if(point.x>=25 && point.x<=25+116 && point.y>=340 && point.y<=340+70){
- kind=play;
- move=true;
- }else if(point.x>=155 && point.x<=155+116 && point.y>=340 && point.y<=340+70){
- kind=score;
- move=true;
- LoadFromResource(IDR_HTML1);
- }else kind=none;
- }//---------------------------------------------------------------
- //這里主要解決顫動效果實(shí)現(xiàn)及按鈕狀態(tài)復(fù)原:
- //---------------------------------------------------------------
- void Button::logic(int &game_state){
- if(kind==play){//顫動控制
- if(move==true){
- play_y=2;
- move=false;
- }else{
- play_y=0;
- kind=none;
- game_state=4;
- }
- }else if(kind==score){
- if(move==true){
- score_y=2;
- move=false;
- }else{
- score_y=0;
- kind=none;
- }
- }
- }//---------------------------------------------------------------
七、重量級問題解決:
1、 飛翔弧度、旋轉(zhuǎn)狀態(tài)難題:
正如前面的鳥的飛翔邏輯代碼所示:鳥的飛翔過程并不是簡單的自由上拋就能解決的;我通過大量實(shí)驗(yàn)發(fā)現(xiàn)必須把這個過程分為上面介紹的4步,然后每一步用更加詳細(xì)的數(shù)學(xué)公式計算鳥的運(yùn)行邏輯[因?yàn)榇颂幬覀儽仨毧紤]鳥的旋轉(zhuǎn)效果和鳥的速度同步,所以這才是難點(diǎn)所在]。因?yàn)樯厦嬉呀?jīng)詳細(xì)說明了,這里就不再重復(fù),但這是一大難點(diǎn)!
2、 混音效果、完美封裝處理:
本來音樂播放只要用PlaySound函數(shù)一句話就能產(chǎn)生音樂播放效果,但是當(dāng)全部節(jié)點(diǎn)都放好音樂時,發(fā)現(xiàn)當(dāng)鳥正好越過柱子發(fā)出加分的鈴聲時和鳥飛翔的聲音無法混合播放,而是出現(xiàn)了嚴(yán)重的打斷效果!導(dǎo)致聽起來很不舒服。難道只有重新用Direct-X來處理混音嗎?想想就冒汗….畢竟游戲已經(jīng)接近尾聲了,沒必要再推翻MFC框架而用Direct-X來吧!于是發(fā)現(xiàn)用用2個不同的函數(shù)可以解決這個問題,即:其他部分不變還是用PlaySound函數(shù),而分?jǐn)?shù)增加的音樂用mciSendString函數(shù)來播放可以解決問題。
但是mciSendString只能加在特定路徑下的音頻,無法處理資源文件下的wav文件,這該怎么辦呢?難道要放棄資源的全封裝效果?那多不好,于是還是被我解決了!我采用的思路是:把資源文件讀到一個中間虛擬文件,然后把該中間文件加載金mciSendString就可以啦!下面是如何讀取資源文件并轉(zhuǎn)為中間文件的函數(shù):
注:PlaySound(MAKEINTRESOURCE(ID),AfxGetResourceHandle(),SND_RESOURCE|SND_ASYNC);
- //---------------------------------------------------------------
- bool ExtractResource(LPCTSTR strDstFile, LPCTSTR strResType, LPCTSTR strResName)
- {
- // 創(chuàng)建文件
- HANDLE hFile = ::CreateFile(strDstFile, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);
- if (hFile == INVALID_HANDLE_VALUE)
- return false;
- // 查找資源文件中、加載資源到內(nèi)存、得到資源大小
- HRSRC hRes = ::FindResource(NULL, strResName, strResType);
- HGLOBAL hMem = ::LoadResource(NULL, hRes);
- DWORD dwSize = ::SizeofResource(NULL, hRes);
- // 寫入文件
- DWORD dwWrite = 0; // 返回寫入字節(jié)
- ::WriteFile(hFile, hMem, dwSize, &dwWrite, NULL);
- ::CloseHandle(hFile);
- return true;
- }//--------------------------------------------------------------
3、 創(chuàng)建分享、窗口截屏技術(shù):
其實(shí)已經(jīng)解決上面幾個問題已經(jīng)仿的差不多啦,但是還不完美!于是開始著手解決那個分享按鈕[要知道這對MFC來說難度不亞于不用引擎來做圖像處理!]可是這并不代表問題不可解。先不說,先看看效果!
知道難度了吧!這是分享按鈕自動創(chuàng)建的網(wǎng)頁,然后還有圖片信息,下載鏈接[這樣才會吸引更多的人玩]由于這里涉及到非基礎(chǔ)MFC知識,這里只提示一下:用到的技術(shù)是HTML+JS技術(shù)[也就是網(wǎng)頁編程+腳本設(shè)計]
八、開發(fā)感想:
實(shí)踐出真知,通過開發(fā)這款簡單的像素游戲,遇到了很多問題,也學(xué)到了很多,如今將近一年后拿出來還覺得當(dāng)時做的這個是一個小奇跡~雖然這一年里也做了不少好玩的軟件、神奇的硬件、以及一些軟硬結(jié)合的小東西,但是都沒有這個讓人感覺充實(shí)。仔細(xì)想想,我覺得之所以它能讓開發(fā)它的人如此留念,很大一部分原因是因?yàn)閷λ姆磸?fù)斟酌修改與追求完美的過程中所積淀的解決問題、享受成果的樂趣吧!如今大三上也快GAME OVER了。這一年可能太過于浮躁,一方面想施展下身手、另一方面又技藝不精,會的挺多但都淺嘗輒止,好的想法要很長時間才能實(shí)現(xiàn),遇到優(yōu)化又不能靜下心來,整天忙忙碌碌基本1~2點(diǎn)休息,可是很少出這種精致的作品~時間很快,暑假實(shí)習(xí)過后留在大學(xué)里的日子就不多啦,且行且珍惜~