MFC實現桌面版Flappy Bird

beautifulzzzz發表於2014-12-09

目錄

  • 開發背景
  • 開發語言及執行環境
  • 效果展示
  • 遊戲框架說明
  • 遊戲狀態及邏輯說明
  • 經典演算法說明
  • 重量級問題解決
  • 開發感想

一、開發背景:

flappy bird由一位來自越南河內的獨立遊戲開發者阮哈東開發,是一款形式簡易但難度極高的休閒遊戲。簡單但不粗糙的8位元畫素畫面、超級馬里奧遊戲中的水管、眼神有點呆滯的小鳥和幾朵白雲便構成了遊戲的一切。你需要不斷控制點選螢幕的頻率來調節小鳥的飛行高度和降落速度,讓小鳥順利地通過畫面右端的通道,如果你不小心擦碰到了通道的話,遊戲便宣告結束。

這款虐心的小遊戲一經推出,便引起火爆的下載。然後先後出現了各種平臺的移植開發:IOS平臺PC和手機版、採用HTML5+Canvas及Javascript技術來實現的Flappy Bird電腦版、以網頁html5+JS技術完全克隆了原版native app的Web App版、實現了在微信朋友圈和QQ空間中的無縫執行的微信/QQ空間版、WindowsPhone版….但是唯一沒有的是直接可在windows作業系統下的單機版,於是當時突發奇想,不如我來填補這個漏洞吧!

二、開發語言及執行環境:

此PC版採用C++的MFC技術在VS2012開發平臺下寫成,支援windows 7\8環境,XP不知道為啥不行~

三、效果展示:

   

四、遊戲框架說明:

整個遊戲除了由MFC遊戲基本框架CMyApp和CMainWindow外,這裡特別封裝了以下幾個類:

  • 1、  Bird類[專門處理鳥的飛行邏輯、碰撞檢測、音樂播放、貼圖]
  • 2、  PipeList類[內嵌Pipe類,並用CList建立一個Pipe連結串列,用來處理遊戲中管道的移動邏輯、碰撞檢測等]
  • 3、  Panel類[主要是計分板的動畫效果邏輯和計分板的計分邏輯,數字貼圖,金幣種類運算等]
  • 4、  Land類[主要處理陸地運動邏輯及貼圖]
  • 5、  Button類[主要處理按鈕的動畫效果、貼圖及響應]
  • 6、  Pic類[是圖片資源類,主要負責儲存、載入、全域性呼叫遊戲的圖片資源]

五、遊戲狀態及邏輯說明:

這款遊戲本身操作簡單、邏輯分明,大致可分為以下幾種狀態:

1、  初始態:基本上為靜態貼圖,只有鳥和陸地為簡單運動。由上往下依次為:

[數字:0]

[標誌:Get Ready!]

[圖示:操作方法]

[鳥:上下飛行]

[陸地:向左移動]

[背景:隨機晝夜]

2、  遊戲進行態:當點選一下螢幕,鳥、柱子被解封,陸地依然保持原來運動狀態,背景不變,這裡採用相對運動效果,其實背景是沒有運動的,而鳥也只是上下運動,根本就沒有向前飛一點!

[鳥:向上躍起,然後以豎直上拋的邏輯使鳥運動;同時,還要專門為鳥的姿態設計合理的旋轉函式]

[柱子:向柱子連結串列里加入新的柱子,並使連結串列裡的所有柱子開始向左移動,當柱子完全超出最左邊界時,將該柱子刪除;同樣的,當最後一個柱子到達某一特定距離時,向連結串列里加入一個新的柱子,這樣既保證了剛開始的柱子出現效果的真實、有趣性,又保證了資源的合理回收,提高演算法高效性]

[分數:當柱子到達鳥所在的位置時就要進行碰撞檢測,如果沒有碰撞且鳥跨過柱子,就讓分數+1,並響鈴]

[陸地:保持勻速運動邏輯,採用迴圈貼圖技術,產生無縫效果]

3、  死亡狀態:鳥的死亡狀態看似簡單,但是仔細分析並非如此。各種細節都要分別考慮:

[直接撞地態:中止所有運動邏輯,同時留一定的時間間隔,產生畫面轉換的質感]

[高撞柱子態:旋轉為垂直態,然後自由落體;撞擊時發出聲音,然後發出墜落的聲音,同時進行碰撞檢測,碰到陸地中止一切運動,進行時間停留]

[低撞柱子態:和高撞柱子態的區別是,墜落的時間少了,音樂沒有完頁面就跳轉了,所以要控制時間停留長度,產生高仿的效果]

4、  死亡之後態:鳥撞地之後要有一定的時間逗留防止頁面跳轉過快不舒服的感覺。接下來首先貼上game_over的圖示,然後計分板從下往上飛來,接著開始計分並張貼是否為新紀錄和是否獲得金牌之類的,最後貼上兩個按鈕等待響應。

[陸地:停止運動]

[圖示:展示Game_Over]

[計分板:動畫效果,從下往上飛來並帶有音效,當飛到指定位置時開始從0累計得分,並統計是否為新紀錄和是否獲得相應的獎牌]

[按鈕:靜態貼圖,但是相應的時候有上下振動的效果]

六、經典演算法說明:

1、 ON_WM_TIMER:時間訊息對映:

主要控制全域性邏輯運算的時間程式,根據當前的狀態做相應的邏輯運算;同時邏輯運算也會對全域性的遊戲狀態進行改變,實現全域性操控邏輯實現:(與此相同的draw函式這裡就不再詳細介紹)

 1 void CMainWindow::OnTimer(UINT nTimerID){
 2     switch(nTimerID){
 3     case bird_time:
 4         if(game_state==before_game)bird.logic(before_game,game_state);//開始前
 5         break;
 6     case land_time:
 7         if(game_state==before_game){//開始前
 8             land.logic();//路
 9         }else if(game_state==during_game){//遊戲中
10             if(bird.state!=bird_delay)land.logic();//路
11             bird.logic(1,game_state);//鳥正常運動
12             if(bird.state!=bird_delay)pipe.logic(goals,bird,game_state);//管道
13         }else if(game_state==dying_game){//失敗中
14             bird.logic(2,game_state);//垂直下落
15         }else if(game_state==end_game){//顯示game-over+計分板+2個按鈕
16             if(panel.state==finish)button.logic(game_state);
17             if(last_state>=10)panel.logic(goals,best_goals);
18         }else if(game_state==start_game){//重新開始
19             restart();
20             game_state=before_game;
21         }
22         break;
23     default:break;
24     }
25     draw();
26 }

2、 ON_WM_LEFTBUTTONDOWN:滑鼠左鍵按下監聽對映:

每次單擊滑鼠左鍵相應該函式,然後該函式根據不同的遊戲狀態做出不同的邏輯操作:①、[當遊戲處於0態,即:遊戲開始之前時,點選滑鼠,狀態改為1態,柱子加入開始移動,鳥躍起開始飛翔][當處於遊戲態時:每次點選鳥都會躍起];②、[當處於結束態時:按鈕等待滑鼠按動,並根據區域做出判斷是否按了按鈕,按了哪一個]

 1 void CMainWindow::OnLButtonDown(UINT nFlags, CPoint point){
 2     if(game_state==0){
 3         game_state=1;
 4         pipe.add();
 5         bird.jump();
 6     }else if(game_state==1){
 7         bird.jump();
 8     }else if(game_state==3){
 9         button.click(point);
10     }
11 }

3、PipeList::logic柱子邏輯函式,包括碰撞檢測!

為了簡化起見,我把音訊播放的部分刪去了:這裡是遍歷整個連結串列,對於每一個柱子,由上到下每一個if為:①、[判斷鳥是否正好穿越一個柱子,如果是則分數加1];②、[判斷柱子是否出界,超出就不把該柱子放回連結串列,相當於刪除];③、[鳥與地面的碰撞檢測];④、[鳥與柱子的碰撞檢測]⑤、[最後一個if是判斷最後一個柱子是否到達指定位置,如果到達就向連結串列尾部加入一個新的柱子,從而保證了柱子連續且間距統一]

 1 //---------------------------------------------------------------
 2 void PipeList::logic(int &goals,Bird &bird,int &game_state){//邏輯函式
 3     int count=pipe.GetCount();
 4     for(int i=0;i<count;i++){
 5         Pipe temp=pipe.GetHead();
 6         pipe.RemoveHead();
 7         temp.logic();
 8         if(temp.pos_x==64){
 9             goals+=1;
10         }
11         if(temp.pos_x>=-70)pipe.AddTail(temp);
12         //碰撞檢測
13         if(23+bird.y+48-$d>400){//與地面
14             bird.y=400-230-48+$d;
15             bird.stop();
16             game_state=2;
17         }else if(!(65+48-$d < temp.pos_x || temp.pos_x+52<65+$d)){//與柱子
18             if(!(230+bird.y+$d > temp.pos_y+320 && temp.pos_y+420 > 230+bird.y+48-$d)){
19                 game_state=2;//表示碰撞,遊戲結束;
20             }
21         }
22     }
23     if((pipe.GetTail()).pos_x<=140){
24         Pipe temp;
25         pipe.AddTail(temp);
26     }
27 }//---------------------------------------------------------------

4、 Bird::logic鳥的運動邏輯,包括所有運動狀態(絕密演算法!!!)

同樣的為了簡單我也把音訊部分的程式碼刪去了。此函式是分別將鳥的運動的各個狀態做分別處理:①、[開始前:採用正弦函式波動飛行同時改變翅膀狀態];②、[正常飛行時:又把鳥的運動狀態劃分為向上、向下、旋轉、停留四個狀態分別處理];③、[下落死亡狀態:這裡用了一個輔助時間變數,控制幀動畫播放]

 1 //---------------------------------------------------------------
 2 void Bird::logic(int ID,int &game_state){
 3     if(ID==0){//開始前
 4         y=4*sin(Time*PI);
 5         Time+=0.25;
 6         fly_state=(fly_state+1)%3;
 7     }else if(ID==1){//正常
 8         switch(state){
 9         case state_up:
10             v+=a;
11             y+=v;
12             dis_state--;
13             if(dis_state==0){
14                 state=state_turn;
15                 Time=0;
16             }
17             break;
18         case state_turn:
19             v+=a;
20             y+=v;
21             if(230+y+48-$d>=400){
22                 y=400-230-48+$d;
23                 stop();
24                 game_state=3;
25             }
26             dis_state++;
27             if(dis_state==1 && Time<=0.4){
28                 Time+=0.1;
29                 dis_state=0;
30             }
31             if(dis_state==6){
32                 state=state_down;
33             }
34             break;
35         case state_down:
36             v+=a;
37             y+=v;
38             if(delay==0 && 230+y+48-$d>=400){
39                 y=400-230-48+$d;
40                 stop();
41                 state=state_delay;
42             }
43             break;
44         case state_delay:
45             delay++;
46             if(delay==8){game_state=3;}
47             break;
48         default:break;
49         }
50         if(dis_state!=6)fly_state=(fly_state+1)%3;
51     }else if(ID==2){//下落
52         delay++;
53         if(delay==8){//撞擊聲延時
54         }
55         if(delay<60){//下落運算
56             y+=v;
57             v+=a;
58             if(dis_state!=6)dis_state++;
59             if(230+y+48-$d>=400){//撞地檢測
60                 y=400-230-48+$d;
61                 stop();
62                 if(dis_state==6){delay=60;}
63             }
64         }else if(delay==66){//墜地後延時
65             game_state=3;
66         }
67     }
68 }//---------------------------------------------------------------

5、Button::按鈕識別和按鈕動畫邏輯實現:

通過滑鼠所在的點判斷是否在按鈕所在的矩形區域內來判斷是否點了該按鈕:

 1 //---------------------------------------------------------------
 2 void Button::click(CPoint &point){
 3     if(point.x>=25 && point.x<=25+116 && point.y>=340 && point.y<=340+70){
 4         kind=play;
 5         move=true;
 6     }else if(point.x>=155 && point.x<=155+116 && point.y>=340 && point.y<=340+70){
 7         kind=score;
 8         move=true;
 9         LoadFromResource(IDR_HTML1);
10     }else kind=none;
11 }//---------------------------------------------------------------
12 //這裡主要解決顫動效果實現及按鈕狀態復原:
13 //---------------------------------------------------------------
14 void Button::logic(int &game_state){
15     if(kind==play){//顫動控制
16         if(move==true){
17             play_y=2;
18             move=false;
19         }else{
20             play_y=0;
21             kind=none;
22             game_state=4;
23         }
24     }else if(kind==score){
25         if(move==true){
26             score_y=2;
27             move=false;
28         }else{
29             score_y=0;
30             kind=none;
31         }
32     }
33 }//---------------------------------------------------------------

七、重量級問題解決:

1、 飛翔弧度、旋轉狀態難題:

正如前面的鳥的飛翔邏輯程式碼所示:鳥的飛翔過程並不是簡單的自由上拋就能解決的;我通過大量實驗發現必須把這個過程分為上面介紹的4步,然後每一步用更加詳細的數學公式計算鳥的執行邏輯[因為此處我們必須考慮鳥的旋轉效果和鳥的速度同步,所以這才是難點所在]。因為上面已經詳細說明了,這裡就不再重複,但這是一大難點!

2、 混音效果、完美封裝處理:

本來音樂播放只要用PlaySound函式一句話就能產生音樂播放效果,但是當全部節點都放好音樂時,發現當鳥正好越過柱子發出加分的鈴聲時和鳥飛翔的聲音無法混合播放,而是出現了嚴重的打斷效果!導致聽起來很不舒服。難道只有重新用Direct-X來處理混音嗎?想想就冒汗….畢竟遊戲已經接近尾聲了,沒必要再推翻MFC框架而用Direct-X來吧!於是發現用用2個不同的函式可以解決這個問題,即:其他部分不變還是用PlaySound函式,而分數增加的音樂用mciSendString函式來播放可以解決問題。

但是mciSendString只能加在特定路徑下的音訊,無法處理資原始檔下的wav檔案,這該怎麼辦呢?難道要放棄資源的全封裝效果?那多不好,於是還是被我解決了!我採用的思路是:把資原始檔讀到一箇中間虛擬檔案,然後把該中間檔案載入金mciSendString就可以啦!下面是如何讀取資原始檔並轉為中間檔案的函式:

注:PlaySound(MAKEINTRESOURCE(ID),AfxGetResourceHandle(),SND_RESOURCE|SND_ASYNC);

 1 //---------------------------------------------------------------
 2 bool ExtractResource(LPCTSTR strDstFile, LPCTSTR strResType, LPCTSTR strResName)
 3 {
 4     // 建立檔案
 5     HANDLE hFile = ::CreateFile(strDstFile, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);
 6     if (hFile == INVALID_HANDLE_VALUE)
 7         return false;
 8 
 9     // 查詢資原始檔中、載入資源到記憶體、得到資源大小
10     HRSRC    hRes    = ::FindResource(NULL, strResName, strResType);
11     HGLOBAL    hMem    = ::LoadResource(NULL, hRes);
12     DWORD    dwSize    = ::SizeofResource(NULL, hRes);
13     
14     // 寫入檔案
15     DWORD dwWrite = 0;      // 返回寫入位元組
16     ::WriteFile(hFile, hMem, dwSize, &dwWrite, NULL);
17     ::CloseHandle(hFile);
18 
19     return true;
20 }//--------------------------------------------------------------

3、 建立分享、視窗截圖技術:

其實已經解決上面幾個問題已經仿的差不多啦,但是還不完美!於是開始著手解決那個分享按鈕[要知道這對MFC來說難度不亞於不用引擎來做影像處理!]可是這並不代表問題不可解。先不說,先看看效果!

知道難度了吧!這是分享按鈕自動建立的網頁,然後還有圖片資訊,下載連結[這樣才會吸引更多的人玩]由於這裡涉及到非基礎MFC知識,這裡只提示一下:用到的技術是HTML+JS技術[也就是網頁程式設計+指令碼設計]

相關文章